norn-cli 2.2.2 → 2.4.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 (113) hide show
  1. package/.claude/settings.local.json +18 -0
  2. package/.claude/skills/norn-social-campaign/SKILL.md +70 -0
  3. package/CHANGELOG.md +22 -1
  4. package/LICENSE +20 -29
  5. package/README.md +32 -1
  6. package/demos/nornenv-region-refactor/README.md +64 -0
  7. package/demos/nornenv-showcase/README.md +62 -0
  8. package/demos/nornenv-showcase/norn.config.json +16 -0
  9. package/demos/nornenv-showcase/showcase.norn +70 -0
  10. package/demos/nornenv-showcase/showcase.nornapi +26 -0
  11. package/demos/nornenv-showcase/showcase.nornsql +20 -0
  12. package/dist/cli.js +564 -54
  13. package/out/apiResponseIntellisenseCache.js +394 -0
  14. package/out/assertionRunner.js +567 -0
  15. package/out/cacheDir.js +136 -0
  16. package/out/chatParticipant.js +763 -0
  17. package/out/cli/colors.js +127 -0
  18. package/out/cli/formatters/assertion.js +102 -0
  19. package/out/cli/formatters/index.js +23 -0
  20. package/out/cli/formatters/response.js +106 -0
  21. package/out/cli/formatters/summary.js +246 -0
  22. package/out/cli/redaction.js +237 -0
  23. package/out/cli/reporters/html.js +689 -0
  24. package/out/cli/reporters/index.js +22 -0
  25. package/out/cli/reporters/junit.js +226 -0
  26. package/out/codeLensProvider.js +351 -0
  27. package/out/compareContentProvider.js +85 -0
  28. package/out/completionProvider.js +3739 -0
  29. package/out/contractAssertionSummary.js +225 -0
  30. package/out/contractDecorationProvider.js +243 -0
  31. package/out/coverageCalculator.js +879 -0
  32. package/out/coveragePanel.js +597 -0
  33. package/out/debug/breakpointResolver.js +84 -0
  34. package/out/debug/breakpoints.js +52 -0
  35. package/out/debug/nornDebugAdapter.js +166 -0
  36. package/out/debug/nornDebugSession.js +613 -0
  37. package/out/debug/sequenceLocationIndex.js +77 -0
  38. package/out/debug/types.js +3 -0
  39. package/out/deepClone.js +21 -0
  40. package/out/diagnosticProvider.js +2554 -0
  41. package/out/environmentParser.js +736 -0
  42. package/out/environmentProvider.js +544 -0
  43. package/out/environmentTemplates.js +146 -0
  44. package/out/errors/formatError.js +113 -0
  45. package/out/errors/nornError.js +29 -0
  46. package/out/formUrlEncoded.js +89 -0
  47. package/out/httpClient.js +348 -0
  48. package/out/httpRuntimeOptions.js +16 -0
  49. package/out/importErrors.js +31 -0
  50. package/out/inlayHintResolver.js +70 -0
  51. package/out/jsonFileReader.js +323 -0
  52. package/out/mcpClient.js +193 -0
  53. package/out/mcpConfig.js +184 -0
  54. package/out/mcpToolIntellisenseCache.js +96 -0
  55. package/out/mcpToolSchema.js +50 -0
  56. package/out/nornConfig.js +132 -0
  57. package/out/nornHoverProvider.js +124 -0
  58. package/out/nornInlayHintsProvider.js +191 -0
  59. package/out/nornPrompt.js +755 -0
  60. package/out/nornSqlParser.js +286 -0
  61. package/out/nornapiHoverProvider.js +135 -0
  62. package/out/nornapiInlayHintsProvider.js +94 -0
  63. package/out/nornapiParser.js +324 -0
  64. package/out/nornenvCodeActionProvider.js +101 -0
  65. package/out/nornenvDecorationProvider.js +239 -0
  66. package/out/nornenvFoldingProvider.js +63 -0
  67. package/out/nornenvHoverProvider.js +114 -0
  68. package/out/nornenvInlayHintsProvider.js +99 -0
  69. package/out/nornenvLanguageModel.js +187 -0
  70. package/out/nornenvRegionRefactor.js +267 -0
  71. package/out/nornsqlHoverProvider.js +95 -0
  72. package/out/nornsqlInlayHintsProvider.js +114 -0
  73. package/out/parser.js +839 -0
  74. package/out/pathAccess.js +28 -0
  75. package/out/postmanImportPanel.js +732 -0
  76. package/out/postmanImportPlanner.js +1155 -0
  77. package/out/postmanImportSidebarView.js +532 -0
  78. package/out/quotedString.js +35 -0
  79. package/out/requestPreparation.js +179 -0
  80. package/out/requestValidation.js +146 -0
  81. package/out/responsePanel.js +7754 -0
  82. package/out/schemaGenerator.js +562 -0
  83. package/out/scriptRunner.js +419 -0
  84. package/out/secrets/cliSecrets.js +415 -0
  85. package/out/secrets/crypto.js +105 -0
  86. package/out/secrets/envFileSecrets.js +177 -0
  87. package/out/secrets/keyStore.js +259 -0
  88. package/out/sequenceDeclaration.js +15 -0
  89. package/out/sequenceRunner.js +3590 -0
  90. package/out/sqlAdapterRunner.js +122 -0
  91. package/out/sqlBuiltInAdapters.js +604 -0
  92. package/out/sqlConfig.js +184 -0
  93. package/out/starterCatalog.js +554 -0
  94. package/out/stringUtils.js +25 -0
  95. package/out/swaggerBodyIntellisenseCache.js +114 -0
  96. package/out/swaggerParser.js +464 -0
  97. package/out/testProvider.js +767 -0
  98. package/out/theoryCaseLoader.js +113 -0
  99. package/out/validationCache.js +211 -0
  100. package/package.json +38 -11
  101. package/.kanbn/index.md +0 -31
  102. package/.kanbn/tasks/book-first-mentor-session.md +0 -13
  103. package/.kanbn/tasks/decide-what-success-in-a-pilot-looks-like.md +0 -9
  104. package/.kanbn/tasks/do-5-customer-conversations.md +0 -9
  105. package/.kanbn/tasks/finalise-the-one-line-pitch.md +0 -11
  106. package/.kanbn/tasks/interview-script.md +0 -49
  107. package/.kanbn/tasks/make-a-list-of-10-people-to-speak-to.md +0 -11
  108. package/.kanbn/tasks/prepare-your-customer-interview-questions.md +0 -11
  109. package/.kanbn/tasks/recruit-2/342/200/2233-pilot-users.md +0 -9
  110. package/.kanbn/tasks/refine-your-pitch.md +0 -9
  111. package/.kanbn/tasks/use-the-shiplight-website-as-a-template-to-improve-norn-website.md +0 -9
  112. package/.kanbn/tasks/write-down-repeated-wording.md +0 -9
  113. package/.kanbn/tasks/write-the-one-pager.md +0 -27
package/dist/cli.js CHANGED
@@ -127575,12 +127575,20 @@ function stringifyRequestValue(value) {
127575
127575
  }
127576
127576
  return String(value);
127577
127577
  }
127578
+ function shouldResolveBareRequestValue(value, variables) {
127579
+ if (!Object.prototype.hasOwnProperty.call(variables, value)) {
127580
+ return false;
127581
+ }
127582
+ const envScope = variables["$env"];
127583
+ const isEnvOnlyValue = envScope && typeof envScope === "object" && Object.prototype.hasOwnProperty.call(envScope, value) && variables[value] === envScope[value];
127584
+ return !isEnvOnlyValue;
127585
+ }
127578
127586
  function resolveRequestValueExpression(value, variables) {
127579
- if (variables[value] !== void 0) {
127587
+ if (shouldResolveBareRequestValue(value, variables)) {
127580
127588
  return stringifyRequestValue(variables[value]);
127581
127589
  }
127582
127590
  const pathMatch2 = value.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(\.\w[\w-]*(?:\.\w[\w-]*|\[\d+\])*|\[\d+\](?:\.\w[\w-]*|\[\d+\])*)$/);
127583
- if (pathMatch2 && variables[pathMatch2[1]] !== void 0) {
127591
+ if (pathMatch2 && shouldResolveBareRequestValue(pathMatch2[1], variables)) {
127584
127592
  const nestedValue = getNestedPathValue(variables[pathMatch2[1]], pathMatch2[2].replace(/^\./, ""));
127585
127593
  if (nestedValue !== void 0) {
127586
127594
  return stringifyRequestValue(nestedValue);
@@ -127832,7 +127840,7 @@ function resolveEnvironmentTemplateValue(variableName, variables, secretNames =
127832
127840
  kind: "scope",
127833
127841
  variableName: name,
127834
127842
  reference: referenceName,
127835
- message: `.nornenv template reference '${referenceName}' is environment-specific. Values inside [env:...] sections can only reference common variables.`
127843
+ message: `.nornenv template reference '${referenceName}' is not in scope. Values inside [env:...] sections can reference common variables and inherited ancestor variables only.`
127836
127844
  });
127837
127845
  }
127838
127846
  if (resolved.secret) {
@@ -132675,24 +132683,58 @@ function listCachedSecretKeyIds(targetPath) {
132675
132683
  // src/environmentParser.ts
132676
132684
  var ENV_FILENAME = ".nornenv";
132677
132685
  var importRegex = /^import\s+["']?(.+?)["']?\s*$/;
132678
- var envRegex = /^\[env:([a-zA-Z_][a-zA-Z0-9_-]*)\]$/;
132686
+ var sectionHeaderRegex = /^\[(env|template):([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s+extends\s+([^\]]+?))?\]\s*$/;
132687
+ var identifierRegex = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
132679
132688
  var varRegex = /^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
132680
132689
  var connectionStringRegex = /^connectionString\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
132681
132690
  var secretRegex = /^secret\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
132682
132691
  var secretConnectionStringRegex = /^secret\s+connectionString\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
132692
+ function parseExtendsClause(clause) {
132693
+ if (!clause) {
132694
+ return [];
132695
+ }
132696
+ const seen = /* @__PURE__ */ new Set();
132697
+ const names = [];
132698
+ for (const raw of clause.split(",")) {
132699
+ const name = raw.trim();
132700
+ if (!name || !identifierRegex.test(name) || seen.has(name)) {
132701
+ continue;
132702
+ }
132703
+ seen.add(name);
132704
+ names.push(name);
132705
+ }
132706
+ return names;
132707
+ }
132683
132708
  function parseEnvFile(content, sourceFilePath) {
132684
132709
  const lines = content.split("\n");
132685
132710
  const config2 = {
132686
132711
  common: {},
132687
132712
  environments: [],
132713
+ templates: [],
132688
132714
  secretNames: /* @__PURE__ */ new Set(),
132689
132715
  secretValues: /* @__PURE__ */ new Map(),
132690
132716
  imports: [],
132691
132717
  misplacedImports: [],
132692
132718
  secretDeclarations: []
132693
132719
  };
132694
- let currentEnv = null;
132720
+ let currentSection = null;
132721
+ let currentKind = null;
132695
132722
  let seenContent = false;
132723
+ const assignVariable = (varName, varValue) => {
132724
+ if (currentSection) {
132725
+ currentSection.variables[varName] = varValue;
132726
+ } else {
132727
+ config2.common[varName] = varValue;
132728
+ }
132729
+ };
132730
+ const buildSecretDeclaration = (varName, varValue, lineNumber) => ({
132731
+ name: varName,
132732
+ value: varValue,
132733
+ envName: currentKind === "env" ? currentSection?.name : void 0,
132734
+ templateName: currentKind === "template" ? currentSection?.name : void 0,
132735
+ lineNumber,
132736
+ filePath: sourceFilePath
132737
+ });
132696
132738
  for (let i = 0; i < lines.length; i++) {
132697
132739
  const trimmed = lines[i].trim();
132698
132740
  if (!trimmed || trimmed.startsWith("#")) {
@@ -132711,13 +132753,21 @@ function parseEnvFile(content, sourceFilePath) {
132711
132753
  continue;
132712
132754
  }
132713
132755
  seenContent = true;
132714
- const envMatch = trimmed.match(envRegex);
132715
- if (envMatch) {
132716
- currentEnv = {
132717
- name: envMatch[1],
132718
- variables: {}
132719
- };
132720
- config2.environments.push(currentEnv);
132756
+ const sectionMatch = trimmed.match(sectionHeaderRegex);
132757
+ if (sectionMatch) {
132758
+ const kind = sectionMatch[1];
132759
+ const name = sectionMatch[2];
132760
+ const parents = parseExtendsClause(sectionMatch[3]);
132761
+ if (kind === "env") {
132762
+ const env3 = { name, variables: {}, parents };
132763
+ config2.environments.push(env3);
132764
+ currentSection = env3;
132765
+ } else {
132766
+ const template = { name, variables: {}, parents };
132767
+ config2.templates.push(template);
132768
+ currentSection = template;
132769
+ }
132770
+ currentKind = kind;
132721
132771
  continue;
132722
132772
  }
132723
132773
  const secretConnectionStringMatch = trimmed.match(secretConnectionStringRegex);
@@ -132725,40 +132775,20 @@ function parseEnvFile(content, sourceFilePath) {
132725
132775
  const profileName = secretConnectionStringMatch[1];
132726
132776
  const varName = `${profileName}_connectionString`;
132727
132777
  const varValue = secretConnectionStringMatch[2].trim();
132728
- config2.secretDeclarations.push({
132729
- name: varName,
132730
- value: varValue,
132731
- envName: currentEnv?.name,
132732
- lineNumber: i,
132733
- filePath: sourceFilePath
132734
- });
132778
+ config2.secretDeclarations.push(buildSecretDeclaration(varName, varValue, i));
132735
132779
  config2.secretNames.add(varName);
132736
132780
  config2.secretValues.set(varName, varValue);
132737
- if (currentEnv) {
132738
- currentEnv.variables[varName] = varValue;
132739
- } else {
132740
- config2.common[varName] = varValue;
132741
- }
132781
+ assignVariable(varName, varValue);
132742
132782
  continue;
132743
132783
  }
132744
132784
  const secretMatch = trimmed.match(secretRegex);
132745
132785
  if (secretMatch) {
132746
132786
  const varName = secretMatch[1];
132747
132787
  const varValue = secretMatch[2].trim();
132748
- config2.secretDeclarations.push({
132749
- name: varName,
132750
- value: varValue,
132751
- envName: currentEnv?.name,
132752
- lineNumber: i,
132753
- filePath: sourceFilePath
132754
- });
132788
+ config2.secretDeclarations.push(buildSecretDeclaration(varName, varValue, i));
132755
132789
  config2.secretNames.add(varName);
132756
132790
  config2.secretValues.set(varName, varValue);
132757
- if (currentEnv) {
132758
- currentEnv.variables[varName] = varValue;
132759
- } else {
132760
- config2.common[varName] = varValue;
132761
- }
132791
+ assignVariable(varName, varValue);
132762
132792
  continue;
132763
132793
  }
132764
132794
  const connectionStringMatch = trimmed.match(connectionStringRegex);
@@ -132766,22 +132796,14 @@ function parseEnvFile(content, sourceFilePath) {
132766
132796
  const profileName = connectionStringMatch[1];
132767
132797
  const varName = `${profileName}_connectionString`;
132768
132798
  const varValue = connectionStringMatch[2].trim();
132769
- if (currentEnv) {
132770
- currentEnv.variables[varName] = varValue;
132771
- } else {
132772
- config2.common[varName] = varValue;
132773
- }
132799
+ assignVariable(varName, varValue);
132774
132800
  continue;
132775
132801
  }
132776
132802
  const varMatch = trimmed.match(varRegex);
132777
132803
  if (varMatch) {
132778
132804
  const varName = varMatch[1];
132779
132805
  const varValue = varMatch[2].trim();
132780
- if (currentEnv) {
132781
- currentEnv.variables[varName] = varValue;
132782
- } else {
132783
- config2.common[varName] = varValue;
132784
- }
132806
+ assignVariable(varName, varValue);
132785
132807
  }
132786
132808
  }
132787
132809
  return config2;
@@ -132910,6 +132932,11 @@ function registerVariableOrigins(config2, filePath, origins) {
132910
132932
  origins.set(`env:${env3.name}:${varName}`, { filePath, line: -1, varName });
132911
132933
  }
132912
132934
  }
132935
+ for (const template of config2.templates) {
132936
+ for (const varName of Object.keys(template.variables)) {
132937
+ origins.set(`template:${template.name}:${varName}`, { filePath, line: -1, varName });
132938
+ }
132939
+ }
132913
132940
  }
132914
132941
  function mergeConfigs(target, source, targetFilePath, sourceFilePath, variableOrigins, errors) {
132915
132942
  for (const [varName, varValue] of Object.entries(source.common)) {
@@ -132932,8 +132959,14 @@ function mergeConfigs(target, source, targetFilePath, sourceFilePath, variableOr
132932
132959
  for (const sourceEnv of source.environments) {
132933
132960
  let targetEnv = target.environments.find((e) => e.name === sourceEnv.name);
132934
132961
  if (!targetEnv) {
132935
- targetEnv = { name: sourceEnv.name, variables: {} };
132962
+ targetEnv = { name: sourceEnv.name, variables: {}, parents: [...sourceEnv.parents] };
132936
132963
  target.environments.push(targetEnv);
132964
+ } else {
132965
+ for (const parent of sourceEnv.parents) {
132966
+ if (!targetEnv.parents.includes(parent)) {
132967
+ targetEnv.parents.push(parent);
132968
+ }
132969
+ }
132937
132970
  }
132938
132971
  for (const [varName, varValue] of Object.entries(sourceEnv.variables)) {
132939
132972
  const key = `env:${sourceEnv.name}:${varName}`;
@@ -132952,6 +132985,35 @@ function mergeConfigs(target, source, targetFilePath, sourceFilePath, variableOr
132952
132985
  }
132953
132986
  }
132954
132987
  }
132988
+ for (const sourceTemplate of source.templates) {
132989
+ let targetTemplate = target.templates.find((t) => t.name === sourceTemplate.name);
132990
+ if (!targetTemplate) {
132991
+ targetTemplate = { name: sourceTemplate.name, variables: {}, parents: [...sourceTemplate.parents] };
132992
+ target.templates.push(targetTemplate);
132993
+ } else {
132994
+ for (const parent of sourceTemplate.parents) {
132995
+ if (!targetTemplate.parents.includes(parent)) {
132996
+ targetTemplate.parents.push(parent);
132997
+ }
132998
+ }
132999
+ }
133000
+ for (const [varName, varValue] of Object.entries(sourceTemplate.variables)) {
133001
+ const key = `template:${sourceTemplate.name}:${varName}`;
133002
+ const existing = variableOrigins.get(key);
133003
+ if (existing) {
133004
+ const sourceLabel = toDisplayPath(sourceFilePath, targetFilePath);
133005
+ const existingLabel = toDisplayPath(existing.filePath, targetFilePath);
133006
+ errors.push({
133007
+ message: `Duplicate variable '${varName}' in [template:${sourceTemplate.name}] section. Found in '${existingLabel}' and '${sourceLabel}'.`,
133008
+ filePath: sourceFilePath,
133009
+ line: -1
133010
+ });
133011
+ } else {
133012
+ targetTemplate.variables[varName] = varValue;
133013
+ variableOrigins.set(key, { filePath: sourceFilePath, line: -1, varName });
133014
+ }
133015
+ }
133016
+ }
132955
133017
  for (const name of source.secretNames) {
132956
133018
  target.secretNames.add(name);
132957
133019
  }
@@ -132983,6 +133045,90 @@ function loadAndResolveEnvFile(filePath) {
132983
133045
  result.secretErrors.push(...resolveEncryptedSecretValues(result.config, filePath));
132984
133046
  return result;
132985
133047
  }
133048
+ function findExtendsNode(name, config2) {
133049
+ const env3 = config2.environments.find((e) => e.name === name);
133050
+ if (env3) {
133051
+ return { node: env3, kind: "env" };
133052
+ }
133053
+ const template = config2.templates.find((t) => t.name === name);
133054
+ if (template) {
133055
+ return { node: template, kind: "template" };
133056
+ }
133057
+ return void 0;
133058
+ }
133059
+ function findExtendsTemplate(name, config2) {
133060
+ const template = config2.templates.find((t) => t.name === name);
133061
+ return template ? { node: template, kind: "template" } : void 0;
133062
+ }
133063
+ function resolveEffectiveEnvVariables(envName, config2) {
133064
+ return Object.fromEntries(
133065
+ Array.from(resolveEffectiveEnvVariableDetails(envName, config2).entries()).map(([name, detail]) => [name, detail.value])
133066
+ );
133067
+ }
133068
+ function resolveInheritedVariableDetails(nodeName, config2) {
133069
+ const inherited = /* @__PURE__ */ new Map();
133070
+ const visited = /* @__PURE__ */ new Set();
133071
+ const stack = /* @__PURE__ */ new Set();
133072
+ const self2 = findExtendsNode(nodeName, config2);
133073
+ if (!self2) {
133074
+ return inherited;
133075
+ }
133076
+ const walk = (name) => {
133077
+ if (visited.has(name) || stack.has(name)) {
133078
+ return;
133079
+ }
133080
+ const found = findExtendsTemplate(name, config2);
133081
+ if (!found) {
133082
+ return;
133083
+ }
133084
+ stack.add(name);
133085
+ for (const parent of found.node.parents) {
133086
+ walk(parent);
133087
+ }
133088
+ for (const [varName, value] of Object.entries(found.node.variables)) {
133089
+ inherited.set(varName, {
133090
+ name: varName,
133091
+ value,
133092
+ sourceKind: found.kind,
133093
+ sourceName: found.node.name,
133094
+ inherited: true
133095
+ });
133096
+ }
133097
+ visited.add(name);
133098
+ stack.delete(name);
133099
+ };
133100
+ for (const parent of self2.node.parents) {
133101
+ walk(parent);
133102
+ }
133103
+ return inherited;
133104
+ }
133105
+ function resolveEffectiveEnvVariableDetails(envName, config2) {
133106
+ const details = /* @__PURE__ */ new Map();
133107
+ for (const [name, value] of Object.entries(config2.common)) {
133108
+ details.set(name, {
133109
+ name,
133110
+ value,
133111
+ sourceKind: "common",
133112
+ inherited: true
133113
+ });
133114
+ }
133115
+ for (const [name, detail] of resolveInheritedVariableDetails(envName, config2)) {
133116
+ details.set(name, detail);
133117
+ }
133118
+ const env3 = config2.environments.find((e) => e.name === envName);
133119
+ if (env3) {
133120
+ for (const [name, value] of Object.entries(env3.variables)) {
133121
+ details.set(name, {
133122
+ name,
133123
+ value,
133124
+ sourceKind: "env",
133125
+ sourceName: env3.name,
133126
+ inherited: false
133127
+ });
133128
+ }
133129
+ }
133130
+ return details;
133131
+ }
132986
133132
  function resolveEncryptedSecretValues(config2, entryFilePath) {
132987
133133
  const errors = [];
132988
133134
  for (const declaration of config2.secretDeclarations) {
@@ -133035,6 +133181,11 @@ function resolveEncryptedSecretValues(config2, entryFilePath) {
133035
133181
  if (env3) {
133036
133182
  env3.variables[declaration.name] = plaintext;
133037
133183
  }
133184
+ } else if (declaration.templateName) {
133185
+ const template = config2.templates.find((t) => t.name === declaration.templateName);
133186
+ if (template) {
133187
+ template.variables[declaration.name] = plaintext;
133188
+ }
133038
133189
  } else {
133039
133190
  config2.common[declaration.name] = plaintext;
133040
133191
  }
@@ -133052,7 +133203,7 @@ var import_process = require("process");
133052
133203
  // src/secrets/envFileSecrets.ts
133053
133204
  var fs16 = __toESM(require("fs"));
133054
133205
  var path16 = __toESM(require("path"));
133055
- var envRegex2 = /^\s*\[env:([a-zA-Z_][a-zA-Z0-9_-]*)\]\s*$/;
133206
+ var envRegex = /^\s*\[env:([a-zA-Z_][a-zA-Z0-9_-]*)\]\s*$/;
133056
133207
  var secretConnectionStringRegex2 = /^(\s*secret\s+connectionString\s+)([a-zA-Z_][a-zA-Z0-9_]*)(\s*=\s*)(.+)$/;
133057
133208
  var secretRegex2 = /^(\s*secret\s+)([a-zA-Z_][a-zA-Z0-9_]*)(\s*=\s*)(.+)$/;
133058
133209
  function splitContentLines(content) {
@@ -133074,7 +133225,7 @@ function extractSecretLines(content, filePath) {
133074
133225
  if (!trimmed || trimmed.startsWith("#")) {
133075
133226
  continue;
133076
133227
  }
133077
- const envMatch = trimmed.match(envRegex2);
133228
+ const envMatch = trimmed.match(envRegex);
133078
133229
  if (envMatch) {
133079
133230
  currentEnv = envMatch[1];
133080
133231
  continue;
@@ -133619,6 +133770,294 @@ function splitImportResolutionErrors(errors) {
133619
133770
  return { blockingErrors, warningErrors };
133620
133771
  }
133621
133772
 
133773
+ // src/nornenvLanguageModel.ts
133774
+ var SECTION_HEADER_REGEX = /^\s*\[(env|template):([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s+extends\s+([^\]]+))?\]\s*$/i;
133775
+ function parseParentList(clause) {
133776
+ if (!clause) {
133777
+ return [];
133778
+ }
133779
+ return clause.split(",").map((parent) => parent.trim()).filter((parent) => /^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(parent));
133780
+ }
133781
+ function parseNornenvDocumentModel(text) {
133782
+ const sections = [];
133783
+ const declarations = [];
133784
+ const lines = text.split("\n");
133785
+ let currentSection;
133786
+ for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
133787
+ const line2 = lines[lineNumber];
133788
+ const sectionMatch = line2.trim().match(SECTION_HEADER_REGEX);
133789
+ if (sectionMatch) {
133790
+ currentSection = {
133791
+ kind: sectionMatch[1].toLowerCase(),
133792
+ name: sectionMatch[2],
133793
+ parents: parseParentList(sectionMatch[3]),
133794
+ lineNumber
133795
+ };
133796
+ sections.push(currentSection);
133797
+ continue;
133798
+ }
133799
+ const connectionMatch = line2.match(/^(\s*)(secret\s+)?connectionString\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$/i);
133800
+ if (connectionMatch) {
133801
+ const displayName = connectionMatch[3];
133802
+ const name = `${displayName}_connectionString`;
133803
+ const nameStart = line2.indexOf(displayName);
133804
+ const value = connectionMatch[4];
133805
+ const valueStart = line2.length - value.length;
133806
+ declarations.push({
133807
+ name,
133808
+ displayName,
133809
+ value,
133810
+ secret: Boolean(connectionMatch[2]),
133811
+ sectionKind: currentSection?.kind,
133812
+ sectionName: currentSection?.name,
133813
+ lineNumber,
133814
+ nameStart,
133815
+ nameEnd: nameStart + displayName.length,
133816
+ valueStart,
133817
+ valueEnd: line2.length
133818
+ });
133819
+ continue;
133820
+ }
133821
+ const variableMatch = line2.match(/^(\s*)(secret|var)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$/i);
133822
+ if (variableMatch) {
133823
+ const name = variableMatch[3];
133824
+ const nameStart = line2.indexOf(name);
133825
+ const value = variableMatch[4];
133826
+ const valueStart = line2.length - value.length;
133827
+ declarations.push({
133828
+ name,
133829
+ displayName: name,
133830
+ value,
133831
+ secret: variableMatch[2].toLowerCase() === "secret",
133832
+ sectionKind: currentSection?.kind,
133833
+ sectionName: currentSection?.name,
133834
+ lineNumber,
133835
+ nameStart,
133836
+ nameEnd: nameStart + name.length,
133837
+ valueStart,
133838
+ valueEnd: line2.length
133839
+ });
133840
+ }
133841
+ }
133842
+ return { sections, declarations };
133843
+ }
133844
+
133845
+ // src/nornenvRegionRefactor.ts
133846
+ var MATRIX_ENV_NAME_REGEX = /^([a-zA-Z][a-zA-Z0-9-]*)_([a-zA-Z][a-zA-Z0-9-]*)$/;
133847
+ var MIN_STAGES = 2;
133848
+ var MIN_REGIONS = 2;
133849
+ var MIN_CELLS = 3;
133850
+ function isConnectionStringVar(name) {
133851
+ return name.endsWith("_connectionString");
133852
+ }
133853
+ function inferDeclarationKind(name) {
133854
+ return isConnectionStringVar(name) ? "connectionString" : "var";
133855
+ }
133856
+ function inferDisplayName(name) {
133857
+ return isConnectionStringVar(name) ? name.slice(0, -"_connectionString".length) : name;
133858
+ }
133859
+ function declarationKind(declaration) {
133860
+ return declaration.name.endsWith("_connectionString") && declaration.displayName !== declaration.name ? "connectionString" : "var";
133861
+ }
133862
+ function buildDeclarationMap(text) {
133863
+ const model = parseNornenvDocumentModel(text);
133864
+ const byEnv = /* @__PURE__ */ new Map();
133865
+ for (const declaration of model.declarations) {
133866
+ if (declaration.sectionKind !== "env" || !declaration.sectionName) {
133867
+ continue;
133868
+ }
133869
+ if (!byEnv.has(declaration.sectionName)) {
133870
+ byEnv.set(declaration.sectionName, /* @__PURE__ */ new Map());
133871
+ }
133872
+ byEnv.get(declaration.sectionName).set(declaration.name, declaration);
133873
+ }
133874
+ return byEnv;
133875
+ }
133876
+ function getCellValue(cell, name, declarationsByEnv, config2) {
133877
+ const value = cell.env.variables[name];
133878
+ if (value === void 0) {
133879
+ return void 0;
133880
+ }
133881
+ const declaration = declarationsByEnv.get(cell.envName)?.get(name);
133882
+ return {
133883
+ value,
133884
+ secret: declaration?.secret ?? config2.secretNames.has(name),
133885
+ kind: declaration ? declarationKind(declaration) : inferDeclarationKind(name),
133886
+ displayName: declaration?.displayName ?? inferDisplayName(name)
133887
+ };
133888
+ }
133889
+ function classifyByAxis(name, cells, axisKeys, getAxisKey, declarationsByEnv, config2) {
133890
+ const byKey = /* @__PURE__ */ new Map();
133891
+ let presentCells = 0;
133892
+ for (const key of axisKeys) {
133893
+ const axisCells = cells.filter((cell) => getAxisKey(cell) === key);
133894
+ const presentValues = axisCells.map((cell) => getCellValue(cell, name, declarationsByEnv, config2)).filter((value) => value !== void 0);
133895
+ if (presentValues.length === 0) {
133896
+ continue;
133897
+ }
133898
+ presentCells += presentValues.length;
133899
+ if (presentValues.length !== axisCells.length) {
133900
+ return void 0;
133901
+ }
133902
+ const first = presentValues[0];
133903
+ if (presentValues.some((value) => value.value !== first.value)) {
133904
+ return void 0;
133905
+ }
133906
+ byKey.set(key, {
133907
+ value: first.value,
133908
+ secret: presentValues.some((value) => value.secret),
133909
+ kind: first.kind,
133910
+ displayName: first.displayName
133911
+ });
133912
+ }
133913
+ return byKey.size > 0 && presentCells >= 2 ? byKey : void 0;
133914
+ }
133915
+ function detectRegionPattern(config2, text) {
133916
+ const cells = [];
133917
+ for (const env3 of config2.environments) {
133918
+ const match = env3.name.match(MATRIX_ENV_NAME_REGEX);
133919
+ if (!match) {
133920
+ continue;
133921
+ }
133922
+ if (env3.parents.length > 0) {
133923
+ return void 0;
133924
+ }
133925
+ cells.push({ stage: match[1], region: match[2], envName: env3.name, env: env3 });
133926
+ }
133927
+ const stages = Array.from(new Set(cells.map((c) => c.stage)));
133928
+ const regions = Array.from(new Set(cells.map((c) => c.region)));
133929
+ if (stages.length < MIN_STAGES || regions.length < MIN_REGIONS || cells.length < MIN_CELLS) {
133930
+ return void 0;
133931
+ }
133932
+ if ((/* @__PURE__ */ new Set([...stages, ...regions])).size !== stages.length + regions.length) {
133933
+ return void 0;
133934
+ }
133935
+ const existingTemplates = new Set(config2.templates.map((template) => template.name));
133936
+ if ([...stages, ...regions].some((name) => existingTemplates.has(name))) {
133937
+ return void 0;
133938
+ }
133939
+ const allVarNames = /* @__PURE__ */ new Set();
133940
+ for (const cell of cells) {
133941
+ for (const name of Object.keys(cell.env.variables)) {
133942
+ allVarNames.add(name);
133943
+ }
133944
+ }
133945
+ let liftedToStage = 0;
133946
+ let liftedToRegion = 0;
133947
+ let leafSpecific = 0;
133948
+ let skippedConnectionStrings = 0;
133949
+ const assignments = [];
133950
+ const declarationsByEnv = buildDeclarationMap(text);
133951
+ for (const name of allVarNames) {
133952
+ if (isConnectionStringVar(name)) {
133953
+ skippedConnectionStrings++;
133954
+ }
133955
+ const stageValues = isConnectionStringVar(name) ? void 0 : classifyByAxis(name, cells, stages, (cell) => cell.stage, declarationsByEnv, config2);
133956
+ const regionValues = isConnectionStringVar(name) ? void 0 : classifyByAxis(name, cells, regions, (cell) => cell.region, declarationsByEnv, config2);
133957
+ if (stageValues) {
133958
+ assignments.push({ name, axis: "stage", byKey: stageValues });
133959
+ liftedToStage++;
133960
+ } else if (regionValues) {
133961
+ assignments.push({ name, axis: "region", byKey: regionValues });
133962
+ liftedToRegion++;
133963
+ } else {
133964
+ const byKey = /* @__PURE__ */ new Map();
133965
+ for (const cell of cells) {
133966
+ const value = getCellValue(cell, name, declarationsByEnv, config2);
133967
+ if (value !== void 0) {
133968
+ byKey.set(cell.envName, value);
133969
+ }
133970
+ }
133971
+ if (byKey.size > 0) {
133972
+ assignments.push({ name, axis: "leaf", byKey });
133973
+ leafSpecific++;
133974
+ }
133975
+ }
133976
+ }
133977
+ const model = parseNornenvDocumentModel(text);
133978
+ const matrixNames = new Set(cells.map((c) => c.envName));
133979
+ const matrixSectionLines = model.sections.filter((s) => s.kind === "env" && matrixNames.has(s.name)).map((s) => s.lineNumber).sort((a, b) => a - b);
133980
+ if (matrixSectionLines.length === 0) {
133981
+ return void 0;
133982
+ }
133983
+ const firstLine = matrixSectionLines[0];
133984
+ const lastMatrixHeaderLine = matrixSectionLines[matrixSectionLines.length - 1];
133985
+ const sectionsInReplacementRange = model.sections.filter(
133986
+ (section) => section.lineNumber >= firstLine && section.lineNumber <= lastMatrixHeaderLine
133987
+ );
133988
+ if (sectionsInReplacementRange.some((section) => section.kind !== "env" || !matrixNames.has(section.name))) {
133989
+ return void 0;
133990
+ }
133991
+ const lineCount = text.split("\n").length;
133992
+ const sectionsAfter = model.sections.filter((s) => s.lineNumber > lastMatrixHeaderLine).map((s) => s.lineNumber).sort((a, b) => a - b);
133993
+ const lastLine = sectionsAfter.length > 0 ? sectionsAfter[0] - 1 : lineCount - 1;
133994
+ return {
133995
+ stages,
133996
+ regions,
133997
+ cells,
133998
+ assignments,
133999
+ replaceRange: { startLine: firstLine, endLine: lastLine },
134000
+ summary: { liftedToStage, liftedToRegion, leafSpecific, skippedConnectionStrings }
134001
+ };
134002
+ }
134003
+ function generateRegionRefactor(pattern) {
134004
+ const blocks = [];
134005
+ const fmtVar = (assignment, value) => {
134006
+ if (value.kind === "connectionString") {
134007
+ const secretPrefix = value.secret ? "secret " : "";
134008
+ return ` ${secretPrefix}connectionString ${value.displayName} = ${value.value}`;
134009
+ }
134010
+ const keyword = value.secret ? "secret" : "var";
134011
+ return ` ${keyword} ${assignment.name} = ${value.value}`;
134012
+ };
134013
+ for (const stage of pattern.stages) {
134014
+ const stageVars = pattern.assignments.filter((a) => a.axis === "stage" && a.byKey.has(stage));
134015
+ if (stageVars.length === 0) {
134016
+ continue;
134017
+ }
134018
+ const lines = [`[template:${stage}]`];
134019
+ for (const v of stageVars) {
134020
+ lines.push(fmtVar(v, v.byKey.get(stage)));
134021
+ }
134022
+ blocks.push(lines.join("\n"));
134023
+ }
134024
+ for (const region of pattern.regions) {
134025
+ const regionVars = pattern.assignments.filter((a) => a.axis === "region" && a.byKey.has(region));
134026
+ if (regionVars.length === 0) {
134027
+ continue;
134028
+ }
134029
+ const lines = [`[template:${region}]`];
134030
+ for (const v of regionVars) {
134031
+ lines.push(fmtVar(v, v.byKey.get(region)));
134032
+ }
134033
+ blocks.push(lines.join("\n"));
134034
+ }
134035
+ for (const cell of pattern.cells) {
134036
+ const leafVars = pattern.assignments.filter((a) => a.axis === "leaf" && a.byKey.has(cell.envName));
134037
+ const header = `[env:${cell.envName} extends ${cell.stage}, ${cell.region}]`;
134038
+ if (leafVars.length === 0) {
134039
+ blocks.push(header);
134040
+ continue;
134041
+ }
134042
+ const lines = [header];
134043
+ for (const v of leafVars) {
134044
+ lines.push(fmtVar(v, v.byKey.get(cell.envName)));
134045
+ }
134046
+ blocks.push(lines.join("\n"));
134047
+ }
134048
+ return blocks.join("\n\n");
134049
+ }
134050
+ function applyRegionRefactorToText(text, pattern) {
134051
+ const lines = text.split("\n");
134052
+ const startLine = pattern.replaceRange.startLine;
134053
+ const endLine = Math.min(pattern.replaceRange.endLine, lines.length - 1);
134054
+ const replacementLines = generateRegionRefactor(pattern).split("\n");
134055
+ lines.splice(startLine, endLine - startLine + 1, ...replacementLines);
134056
+ const result = lines.join("\n");
134057
+ return text.endsWith("\n") && !result.endsWith("\n") ? `${result}
134058
+ ` : result;
134059
+ }
134060
+
133622
134061
  // src/cli.ts
133623
134062
  function handleImportResolutionErrors(errors, colors) {
133624
134063
  const { blockingErrors, warningErrors } = splitImportResolutionErrors(errors);
@@ -133653,7 +134092,7 @@ function resolveEnvironmentForPath(targetPath, selectedEnv) {
133653
134092
  console.error(`Fix .nornenv import errors before running tests.`);
133654
134093
  process.exit(1);
133655
134094
  }
133656
- const variables = { ...envConfig.common };
134095
+ let variables = { ...envConfig.common };
133657
134096
  const secretNames = new Set(envConfig.secretNames);
133658
134097
  const secretValues = new Map(envConfig.secretValues);
133659
134098
  const availableEnvironments = envConfig.environments.map((e) => e.name);
@@ -133672,9 +134111,9 @@ function resolveEnvironmentForPath(targetPath, selectedEnv) {
133672
134111
  };
133673
134112
  }
133674
134113
  targetEnvVariables = targetEnv.variables;
133675
- Object.assign(variables, targetEnvVariables);
133676
- for (const [name, value] of Object.entries(targetEnvVariables)) {
133677
- if (secretNames.has(name)) {
134114
+ variables = resolveEffectiveEnvVariables(selectedEnv, envConfig);
134115
+ for (const [name, value] of Object.entries(variables)) {
134116
+ if (secretNames.has(name) && !secretValues.has(name)) {
133678
134117
  secretValues.set(name, value);
133679
134118
  }
133680
134119
  }
@@ -133733,7 +134172,9 @@ function parseArgs(args) {
133733
134172
  noRedact: false,
133734
134173
  tagFilters: [],
133735
134174
  tagsFilter: [],
133736
- insecure: false
134175
+ insecure: false,
134176
+ refactorRegionPattern: false,
134177
+ writeRefactor: false
133737
134178
  };
133738
134179
  for (let i = 0; i < args.length; i++) {
133739
134180
  const arg = args[i];
@@ -133751,6 +134192,10 @@ function parseArgs(args) {
133751
134192
  options.timeout = parseInt(args[++i], 10) * 1e3;
133752
134193
  } else if (arg === "--insecure") {
133753
134194
  options.insecure = true;
134195
+ } else if (arg === "--refactor-region-pattern" || arg === "--refactor-nornenv-region-pattern") {
134196
+ options.refactorRegionPattern = true;
134197
+ } else if (arg === "--write") {
134198
+ options.writeRefactor = true;
133754
134199
  } else if (arg === "--no-fail") {
133755
134200
  options.failOnError = false;
133756
134201
  } else if (arg === "--no-redact") {
@@ -133806,6 +134251,9 @@ Options:
133806
134251
  -o, --output-dir <dir> Output directory for reports (auto-generates timestamped files)
133807
134252
  --tag <filter> Filter sequences by tag (AND logic, can be repeated)
133808
134253
  --tags <filters> Filter sequences by tags (OR logic, comma-separated)
134254
+ --refactor-region-pattern
134255
+ Refactor a flat .nornenv STAGE_REGION matrix to templates
134256
+ --write Apply --refactor-region-pattern instead of printing result
133809
134257
  -h, --help Show this help message
133810
134258
 
133811
134259
  Report Generation:
@@ -133849,11 +134297,62 @@ Examples:
133849
134297
  norn api-tests.norn --html report.html # Generate HTML report (explicit)
133850
134298
  norn api-tests.norn --insecure # Allow self-signed/local TLS certs
133851
134299
  norn api-tests.norn --no-redact # Show all data (no redaction)
134300
+ norn .nornenv --refactor-region-pattern # Print refactored .nornenv
134301
+ norn .nornenv --refactor-region-pattern --write
133852
134302
  norn secrets keygen --name team-main # Generate shared key and cache locally
133853
134303
  norn secrets import-key --kid team-main # Save shared key from your vault
133854
134304
  norn secrets audit . # Fail if plaintext secrets are committed
133855
134305
  `);
133856
134306
  }
134307
+ function formatRegionPatternSummary(pattern) {
134308
+ const { liftedToStage, liftedToRegion, leafSpecific, skippedConnectionStrings } = pattern.summary;
134309
+ const lines = [
134310
+ `Detected ${pattern.cells.length} envs across ${pattern.stages.length} stages x ${pattern.regions.length} regions.`,
134311
+ `Lifted ${liftedToStage} vars to stage templates (${pattern.stages.join(", ")}).`,
134312
+ `Lifted ${liftedToRegion} vars to region templates (${pattern.regions.join(", ")}).`,
134313
+ `Kept ${leafSpecific} vars leaf-specific.`
134314
+ ];
134315
+ if (skippedConnectionStrings > 0) {
134316
+ lines.push(`Kept ${skippedConnectionStrings} connection-string var${skippedConnectionStrings === 1 ? "" : "s"} in leaf envs.`);
134317
+ }
134318
+ return lines;
134319
+ }
134320
+ function runNornenvRegionRefactor(filePath, options) {
134321
+ const content = fs19.readFileSync(filePath, "utf-8");
134322
+ const config2 = parseEnvFile(content, filePath);
134323
+ const pattern = detectRegionPattern(config2, content);
134324
+ if (!pattern) {
134325
+ if (options.output === "json") {
134326
+ console.log(JSON.stringify({ success: false, changed: false, error: "No region pattern detected" }, null, 2));
134327
+ } else {
134328
+ console.error("No region pattern detected in this .nornenv file.");
134329
+ }
134330
+ process.exit(1);
134331
+ }
134332
+ const refactored = applyRegionRefactorToText(content, pattern);
134333
+ if (options.writeRefactor) {
134334
+ fs19.writeFileSync(filePath, refactored, "utf-8");
134335
+ }
134336
+ if (options.output === "json") {
134337
+ console.log(JSON.stringify({
134338
+ success: true,
134339
+ changed: refactored !== content,
134340
+ file: filePath,
134341
+ summary: pattern.summary,
134342
+ stages: pattern.stages,
134343
+ regions: pattern.regions,
134344
+ output: options.writeRefactor ? void 0 : refactored
134345
+ }, null, 2));
134346
+ } else if (options.writeRefactor) {
134347
+ for (const line2 of formatRegionPatternSummary(pattern)) {
134348
+ console.log(line2);
134349
+ }
134350
+ console.log(`Refactored ${filePath}`);
134351
+ } else {
134352
+ console.log(refactored);
134353
+ }
134354
+ process.exit(0);
134355
+ }
133857
134356
  async function runSingleRequest(fileContent, variables, cookieJar, apiDefinitions, filePath, envContext) {
133858
134357
  const lines = fileContent.split("\n");
133859
134358
  const requestLines = [];
@@ -134018,6 +134517,17 @@ async function main() {
134018
134517
  process.exit(1);
134019
134518
  }
134020
134519
  const isDirectory = fs19.statSync(inputPath).isDirectory();
134520
+ if (options.writeRefactor && !options.refactorRegionPattern) {
134521
+ console.error("Error: --write can only be used with --refactor-region-pattern");
134522
+ process.exit(1);
134523
+ }
134524
+ if (options.refactorRegionPattern) {
134525
+ if (isDirectory) {
134526
+ console.error("Error: --refactor-region-pattern requires a specific .nornenv file, not a directory");
134527
+ process.exit(1);
134528
+ }
134529
+ runNornenvRegionRefactor(inputPath, options);
134530
+ }
134021
134531
  let filesToRun;
134022
134532
  if (isDirectory) {
134023
134533
  filesToRun = discoverNornFiles(inputPath);