norn-cli 2.3.0 → 2.5.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.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +30 -1
- package/demos/nornenv-region-refactor/README.md +64 -0
- package/dist/cli.js +577 -78
- package/package.json +18 -4
- package/.claude/settings.local.json +0 -18
package/AGENTS.md
CHANGED
|
@@ -63,8 +63,8 @@ Skills should capture lessons learned and patterns discovered during implementat
|
|
|
63
63
|
- no docs changes were needed because the release is only bug fixes, refactors, or design/styling work
|
|
64
64
|
|
|
65
65
|
The Test Verification agent runs:
|
|
66
|
-
- `npm
|
|
67
|
-
- `
|
|
66
|
+
- `npm test` (must pass compile/lint, Extension Host editor diagnostics, Test Explorer support, and runtime-negative automation)
|
|
67
|
+
- `npm run test:regression` (local CLI Regression suite; all tests must pass)
|
|
68
68
|
|
|
69
69
|
The Website Documentation Review agent:
|
|
70
70
|
- reviews release changes in `/Users/petercrest/Worktable/Projects/vsApi`
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the "Norn" extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [2.5.0] - 2026-05-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Verbatim string literals** — added C#-style `@"..."` strings for Norn values, assertions, regex patterns, request URLs, sequence arguments/defaults, SQL arguments, schema paths, assertion messages, and `@data` values. Backslashes are literal, doubled quotes decode to a single quote, and `{{...}}` tokens inside verbatim strings are not interpolated.
|
|
9
|
+
- **Escape-aware highlighting** — `.norn` syntax highlighting now distinguishes decoded escapes from ordinary backslash text, including escaped quotes/backslashes in normal strings, regex escapes in `matches` patterns, doubled quotes in verbatim strings, and valid JSON escapes in request bodies.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **Regex assertion authoring** — `matches` patterns are now guided toward quoted string patterns, with `@"..."` recommended for regexes that contain many backslashes or literal quotes.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Quoted string handling** — escaped quotes and backslashes are decoded consistently across variables, request URLs, run arguments, sequence defaults, SQL arguments, `@data` values, `matchesSchema` paths, and assertion messages while preserving unknown escapes such as `\A` as literal text.
|
|
16
|
+
- **Editor assistance with verbatim strings** — diagnostics, inlay hints, and hover providers no longer treat `{{...}}` inside verbatim strings as interpolated Norn variables.
|
|
17
|
+
- **Sequence tag diagnostics** — `@smoke`, `@team(...)`, and other sequence tags are recognized by editor diagnostics again instead of being reported as unknown statements.
|
|
18
|
+
- **Failure output expected values** — CLI, HTML/JUnit reports, and the response panel now prefer the resolved expected value for failed assertions instead of falling back to the raw expression text.
|
|
19
|
+
- **Test Explorer environment resolution** — Test Explorer now uses the only available `.nornenv` environment for isolated single-env fixtures when no active environment has been selected, fixing nested regression cases such as encrypted env imports.
|
|
20
|
+
|
|
21
|
+
### Testing
|
|
22
|
+
- **Editor diagnostics automation** — added an Extension Host suite that discovers `tests/EditorDiagnostics/valid` and `tests/EditorDiagnostics/negative`, keeping clean fixtures at zero diagnostics and intentional error fixtures at exact expected counts.
|
|
23
|
+
- **Runtime negative automation** — converted former manual runtime error cases into `tests/RuntimeNegative` scenarios that assert CLI exit codes and stable error guidance.
|
|
24
|
+
- **Test Explorer automation** — added Extension Host checks for Test Explorer fixture exclusion patterns and single-env `.nornenv` resolution.
|
|
25
|
+
- **Pre-release verification** — added `npm run test:prerelease` and updated release verification to run Extension Host automation plus the local CLI Regression suite.
|
|
26
|
+
- **Test suite cleanup** — moved reusable `.nornenv` import and contract fixtures into Regression, moved API coverage fixtures into `tests/ApiCoverage`, and removed stale scratch/demo test folders.
|
|
27
|
+
|
|
28
|
+
## [2.4.0] - 2026-05-25
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **Per-folder and persisted active environment** — the active env is now tracked per `.nornenv` file (keyed by absolute path) and persisted to the workspace memento. In monorepos each folder remembers its own selection, and selections survive VS Code restart. The status bar shows the env for the current editor's nearest `.nornenv`. Stale entries (deleted `.nornenv` files) are pruned on load.
|
|
32
|
+
- **Region-pattern refactor** — when a `.nornenv` contains a flat `[env:STAGE_REGION]` matrix (e.g. `dev_us`, `dev_uk`, `prod_us`, `prod_uk`), the top of the file shows a "Refactor N envs into S+R templates" CodeLens. Clicking it classifies each variable by axis (stage / region / leaf-specific) and lifts shared values into `[template:STAGE]` + `[template:REGION]` blocks, leaving only true leaf-specific overrides in the env sections. Missing values and `connectionString` declarations stay leaf-specific, per-declaration `secret` keywords are preserved, and the same generator is available in the CLI via `--refactor-region-pattern` / `--write`. The VS Code refactor is one `WorkspaceEdit` so a single Cmd+Z reverses it.
|
|
33
|
+
|
|
5
34
|
## [2.3.0] - 2026-05-20
|
|
6
35
|
|
|
7
36
|
### Added
|
|
@@ -422,7 +451,7 @@ All notable changes to the "Norn" extension will be documented in this file.
|
|
|
422
451
|
|
|
423
452
|
### Added
|
|
424
453
|
- **Regression Fixture (Coverage Scope)**:
|
|
425
|
-
- Added `tests/
|
|
454
|
+
- Added `tests/ApiCoverage/coverage-scope/template-version.nornapi` and `tests/ApiCoverage/coverage-scope/template-version-covered.norn` to lock in template-path coverage behavior.
|
|
426
455
|
|
|
427
456
|
## [1.4.1] - 2026-02-21
|
|
428
457
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Region-pattern refactor showcase
|
|
2
|
+
|
|
3
|
+
A flat `[env:STAGE_REGION]` matrix that the Norn extension can refactor into the templates + extends shape in one click.
|
|
4
|
+
|
|
5
|
+
## How to use
|
|
6
|
+
|
|
7
|
+
1. Open [.nornenv](./.nornenv) in VS Code.
|
|
8
|
+
2. A `$(sparkle) Refactor 4 envs into 2+2 templates` CodeLens appears at the top of the file.
|
|
9
|
+
3. Click it. A confirmation dialog summarises what will change:
|
|
10
|
+
- 2 vars (`baseUrl`, `apiKey`) lifted to stage templates (`dev`, `prod`)
|
|
11
|
+
- 2 vars (`dbHost`, `bucket`) lifted to region templates (`us`, `uk`)
|
|
12
|
+
- 1 var (`failoverHost`) kept as leaf-specific on `prod_uk`
|
|
13
|
+
4. Confirm. The flat 4-env matrix is replaced with templates + extending envs:
|
|
14
|
+
|
|
15
|
+
```nornenv
|
|
16
|
+
[template:dev]
|
|
17
|
+
var baseUrl = https://dev.example.com
|
|
18
|
+
var apiKey = dev-key-123
|
|
19
|
+
|
|
20
|
+
[template:prod]
|
|
21
|
+
var baseUrl = https://api.example.com
|
|
22
|
+
secret apiKey = prod-key-789
|
|
23
|
+
|
|
24
|
+
[template:us]
|
|
25
|
+
var dbHost = db.us.example.com
|
|
26
|
+
var bucket = data-us
|
|
27
|
+
|
|
28
|
+
[template:uk]
|
|
29
|
+
var dbHost = db.uk.example.com
|
|
30
|
+
var bucket = data-uk
|
|
31
|
+
|
|
32
|
+
[env:dev_us extends dev, us]
|
|
33
|
+
|
|
34
|
+
[env:dev_uk extends dev, uk]
|
|
35
|
+
|
|
36
|
+
[env:prod_us extends prod, us]
|
|
37
|
+
|
|
38
|
+
[env:prod_uk extends prod, uk]
|
|
39
|
+
var failoverHost = api-failover.uk.example.com
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
5. The `secret` keyword on `apiKey` is preserved when the var is lifted to a template.
|
|
43
|
+
|
|
44
|
+
## CLI
|
|
45
|
+
|
|
46
|
+
The same generator is available from the local CLI:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node ./dist/cli.js demos/nornenv-region-refactor/.nornenv --refactor-region-pattern
|
|
50
|
+
node ./dist/cli.js demos/nornenv-region-refactor/.nornenv --refactor-region-pattern --write
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## When the refactor fires
|
|
54
|
+
|
|
55
|
+
The CodeLens appears only when **all** of these are true:
|
|
56
|
+
- At least 2 stages × 2 regions
|
|
57
|
+
- At least 3 populated cells
|
|
58
|
+
- All matrix envs are flat (no existing `extends` clause)
|
|
59
|
+
|
|
60
|
+
It does not touch envs that already use `extends`, envs with names that don't match the `STAGE_REGION` shape, or `connectionString` declarations (those are left in their leaves — refactor manually).
|
|
61
|
+
|
|
62
|
+
## Cmd+Z
|
|
63
|
+
|
|
64
|
+
Everything goes through a single `WorkspaceEdit`, so a single Cmd+Z undoes the whole refactor.
|
package/dist/cli.js
CHANGED
|
@@ -101832,6 +101832,119 @@ var path19 = __toESM(require("path"));
|
|
|
101832
101832
|
// src/parser.ts
|
|
101833
101833
|
var path = __toESM(require("path"));
|
|
101834
101834
|
|
|
101835
|
+
// src/quotedString.ts
|
|
101836
|
+
function decodeQuotedStringLiteral(literal2) {
|
|
101837
|
+
if (isVerbatimStringLiteral(literal2)) {
|
|
101838
|
+
return decodeVerbatimStringLiteral(literal2);
|
|
101839
|
+
}
|
|
101840
|
+
if (!isQuotedStringLiteral(literal2)) {
|
|
101841
|
+
return literal2;
|
|
101842
|
+
}
|
|
101843
|
+
const quoteChar = literal2[0];
|
|
101844
|
+
const inner = literal2.slice(1, -1);
|
|
101845
|
+
let decoded = "";
|
|
101846
|
+
for (let i = 0; i < inner.length; i++) {
|
|
101847
|
+
const char = inner[i];
|
|
101848
|
+
if (char !== "\\" || i === inner.length - 1) {
|
|
101849
|
+
decoded += char;
|
|
101850
|
+
continue;
|
|
101851
|
+
}
|
|
101852
|
+
const nextChar = inner[i + 1];
|
|
101853
|
+
if (nextChar === "\\" || nextChar === quoteChar) {
|
|
101854
|
+
decoded += nextChar;
|
|
101855
|
+
i++;
|
|
101856
|
+
continue;
|
|
101857
|
+
}
|
|
101858
|
+
decoded += char;
|
|
101859
|
+
}
|
|
101860
|
+
return decoded;
|
|
101861
|
+
}
|
|
101862
|
+
function isQuotedStringLiteral(literal2) {
|
|
101863
|
+
if (isVerbatimStringLiteral(literal2)) {
|
|
101864
|
+
return true;
|
|
101865
|
+
}
|
|
101866
|
+
if (literal2.length < 2) {
|
|
101867
|
+
return false;
|
|
101868
|
+
}
|
|
101869
|
+
const quoteChar = literal2[0];
|
|
101870
|
+
return (quoteChar === '"' || quoteChar === "'") && literal2[literal2.length - 1] === quoteChar;
|
|
101871
|
+
}
|
|
101872
|
+
function isVerbatimStringLiteral(literal2) {
|
|
101873
|
+
return literal2.length >= 3 && literal2.startsWith('@"') && literal2.endsWith('"');
|
|
101874
|
+
}
|
|
101875
|
+
function mapOutsideVerbatimStrings(text, transform2) {
|
|
101876
|
+
let output2 = "";
|
|
101877
|
+
let segmentStart = 0;
|
|
101878
|
+
let inStandardQuote = false;
|
|
101879
|
+
let standardQuoteChar = "";
|
|
101880
|
+
let escapeNext = false;
|
|
101881
|
+
for (let i = 0; i < text.length; i++) {
|
|
101882
|
+
const char = text[i];
|
|
101883
|
+
const nextChar = i + 1 < text.length ? text[i + 1] : "";
|
|
101884
|
+
if (inStandardQuote) {
|
|
101885
|
+
if (escapeNext) {
|
|
101886
|
+
escapeNext = false;
|
|
101887
|
+
continue;
|
|
101888
|
+
}
|
|
101889
|
+
if (char === "\\") {
|
|
101890
|
+
escapeNext = true;
|
|
101891
|
+
continue;
|
|
101892
|
+
}
|
|
101893
|
+
if (char === standardQuoteChar) {
|
|
101894
|
+
inStandardQuote = false;
|
|
101895
|
+
standardQuoteChar = "";
|
|
101896
|
+
}
|
|
101897
|
+
continue;
|
|
101898
|
+
}
|
|
101899
|
+
if (char === "@" && nextChar === '"') {
|
|
101900
|
+
const end = findVerbatimStringEnd(text, i);
|
|
101901
|
+
output2 += transform2(text.substring(segmentStart, i));
|
|
101902
|
+
output2 += text.substring(i, end);
|
|
101903
|
+
segmentStart = end;
|
|
101904
|
+
i = end - 1;
|
|
101905
|
+
continue;
|
|
101906
|
+
}
|
|
101907
|
+
if (char === '"' || char === "'") {
|
|
101908
|
+
inStandardQuote = true;
|
|
101909
|
+
standardQuoteChar = char;
|
|
101910
|
+
escapeNext = false;
|
|
101911
|
+
}
|
|
101912
|
+
}
|
|
101913
|
+
output2 += transform2(text.substring(segmentStart));
|
|
101914
|
+
return output2;
|
|
101915
|
+
}
|
|
101916
|
+
function findVerbatimStringEnd(text, start) {
|
|
101917
|
+
let i = start + 2;
|
|
101918
|
+
while (i < text.length) {
|
|
101919
|
+
const char = text[i];
|
|
101920
|
+
const nextChar = i + 1 < text.length ? text[i + 1] : "";
|
|
101921
|
+
if (char === '"' && nextChar === '"') {
|
|
101922
|
+
i += 2;
|
|
101923
|
+
continue;
|
|
101924
|
+
}
|
|
101925
|
+
if (char === '"') {
|
|
101926
|
+
return i + 1;
|
|
101927
|
+
}
|
|
101928
|
+
i++;
|
|
101929
|
+
}
|
|
101930
|
+
return text.length;
|
|
101931
|
+
}
|
|
101932
|
+
function decodeVerbatimStringLiteral(literal2) {
|
|
101933
|
+
const inner = literal2.slice(2, -1);
|
|
101934
|
+
let decoded = "";
|
|
101935
|
+
for (let i = 0; i < inner.length; i++) {
|
|
101936
|
+
const char = inner[i];
|
|
101937
|
+
const nextChar = i + 1 < inner.length ? inner[i + 1] : "";
|
|
101938
|
+
if (char === '"' && nextChar === '"') {
|
|
101939
|
+
decoded += '"';
|
|
101940
|
+
i++;
|
|
101941
|
+
continue;
|
|
101942
|
+
}
|
|
101943
|
+
decoded += char;
|
|
101944
|
+
}
|
|
101945
|
+
return decoded;
|
|
101946
|
+
}
|
|
101947
|
+
|
|
101835
101948
|
// src/nornapiParser.ts
|
|
101836
101949
|
function extractPathParameters(path20) {
|
|
101837
101950
|
const params = [];
|
|
@@ -102003,8 +102116,8 @@ function parseApiRequest(requestContent, endpoints, headerGroups) {
|
|
|
102003
102116
|
const headerMatch = line2.match(/^([a-zA-Z][a-zA-Z0-9\-]*)\s*:\s*(.+)$/);
|
|
102004
102117
|
if (headerMatch) {
|
|
102005
102118
|
let headerValue = headerMatch[2].trim();
|
|
102006
|
-
if (
|
|
102007
|
-
headerValue = headerValue
|
|
102119
|
+
if (isQuotedStringLiteral(headerValue)) {
|
|
102120
|
+
headerValue = decodeQuotedStringLiteral(headerValue);
|
|
102008
102121
|
}
|
|
102009
102122
|
inlineHeaders[headerMatch[1]] = headerValue;
|
|
102010
102123
|
continue;
|
|
@@ -102049,8 +102162,8 @@ function parseParamTokens(paramsStr) {
|
|
|
102049
102162
|
return tokens;
|
|
102050
102163
|
}
|
|
102051
102164
|
function unquote(value) {
|
|
102052
|
-
if (
|
|
102053
|
-
return value
|
|
102165
|
+
if (isQuotedStringLiteral(value)) {
|
|
102166
|
+
return decodeQuotedStringLiteral(value);
|
|
102054
102167
|
}
|
|
102055
102168
|
return value;
|
|
102056
102169
|
}
|
|
@@ -102367,14 +102480,21 @@ function getNestedPathValue(obj, path20) {
|
|
|
102367
102480
|
function stripInlineComment(line2) {
|
|
102368
102481
|
let inSingleQuote = false;
|
|
102369
102482
|
let inDoubleQuote = false;
|
|
102483
|
+
let inVerbatimString = false;
|
|
102370
102484
|
for (let index = 0; index < line2.length; index++) {
|
|
102371
102485
|
const char = line2[index];
|
|
102372
102486
|
const previousChar = index > 0 ? line2[index - 1] : "";
|
|
102373
|
-
|
|
102487
|
+
const nextChar = index + 1 < line2.length ? line2[index + 1] : "";
|
|
102488
|
+
if (inDoubleQuote && inVerbatimString && char === '"' && nextChar === '"') {
|
|
102489
|
+
index++;
|
|
102490
|
+
continue;
|
|
102491
|
+
}
|
|
102492
|
+
if (!inVerbatimString && previousChar === "\\") {
|
|
102374
102493
|
continue;
|
|
102375
102494
|
}
|
|
102376
102495
|
if (char === '"' && !inSingleQuote) {
|
|
102377
102496
|
inDoubleQuote = !inDoubleQuote;
|
|
102497
|
+
inVerbatimString = inDoubleQuote && previousChar === "@";
|
|
102378
102498
|
} else if (char === "'" && !inDoubleQuote) {
|
|
102379
102499
|
inSingleQuote = !inSingleQuote;
|
|
102380
102500
|
} else if (char === "#" && !inSingleQuote && !inDoubleQuote) {
|
|
@@ -102491,8 +102611,8 @@ function extractFileLevelVariables(text) {
|
|
|
102491
102611
|
if (isRuntimeComputedVariableValue(value)) {
|
|
102492
102612
|
continue;
|
|
102493
102613
|
}
|
|
102494
|
-
if (
|
|
102495
|
-
value = value
|
|
102614
|
+
if (isQuotedStringLiteral(value)) {
|
|
102615
|
+
value = decodeQuotedStringLiteral(value);
|
|
102496
102616
|
}
|
|
102497
102617
|
variables[match[1]] = value;
|
|
102498
102618
|
}
|
|
@@ -102598,8 +102718,8 @@ function parserHttpRequest(text, variables = {}) {
|
|
|
102598
102718
|
requestLine = cleanedLine;
|
|
102599
102719
|
const [method, ...urlParts] = requestLine.split(" ");
|
|
102600
102720
|
let url3 = urlParts.join(" ");
|
|
102601
|
-
if (
|
|
102602
|
-
url3 = url3
|
|
102721
|
+
if (isQuotedStringLiteral(url3)) {
|
|
102722
|
+
url3 = decodeQuotedStringLiteral(url3);
|
|
102603
102723
|
}
|
|
102604
102724
|
const headers = {};
|
|
102605
102725
|
let bodyStartIndex = -1;
|
|
@@ -109661,34 +109781,6 @@ function validateAgainstSchemaObjectDetailed(value, schema) {
|
|
|
109661
109781
|
}
|
|
109662
109782
|
}
|
|
109663
109783
|
|
|
109664
|
-
// src/quotedString.ts
|
|
109665
|
-
function decodeQuotedStringLiteral(literal2) {
|
|
109666
|
-
if (literal2.length < 2) {
|
|
109667
|
-
return literal2;
|
|
109668
|
-
}
|
|
109669
|
-
const quoteChar = literal2[0];
|
|
109670
|
-
if (quoteChar !== '"' && quoteChar !== "'" || literal2[literal2.length - 1] !== quoteChar) {
|
|
109671
|
-
return literal2;
|
|
109672
|
-
}
|
|
109673
|
-
const inner = literal2.slice(1, -1);
|
|
109674
|
-
let decoded = "";
|
|
109675
|
-
for (let i = 0; i < inner.length; i++) {
|
|
109676
|
-
const char = inner[i];
|
|
109677
|
-
if (char !== "\\" || i === inner.length - 1) {
|
|
109678
|
-
decoded += char;
|
|
109679
|
-
continue;
|
|
109680
|
-
}
|
|
109681
|
-
const nextChar = inner[i + 1];
|
|
109682
|
-
if (nextChar === "\\" || nextChar === quoteChar) {
|
|
109683
|
-
decoded += nextChar;
|
|
109684
|
-
i++;
|
|
109685
|
-
continue;
|
|
109686
|
-
}
|
|
109687
|
-
decoded += char;
|
|
109688
|
-
}
|
|
109689
|
-
return decoded;
|
|
109690
|
-
}
|
|
109691
|
-
|
|
109692
109784
|
// src/assertionRunner.ts
|
|
109693
109785
|
function isAssertCommand(line2) {
|
|
109694
109786
|
return /^assert\s+/i.test(line2.trim());
|
|
@@ -109704,17 +109796,25 @@ function parseAssertCommand(line2) {
|
|
|
109704
109796
|
const pipeIndex = findUnquotedPipe(content);
|
|
109705
109797
|
if (pipeIndex !== -1) {
|
|
109706
109798
|
message = content.substring(pipeIndex + 1).trim();
|
|
109707
|
-
if (
|
|
109799
|
+
if (isQuotedStringLiteral(message)) {
|
|
109800
|
+
const messageWasVerbatim = isVerbatimStringLiteral(message);
|
|
109708
109801
|
message = decodeQuotedStringLiteral(message);
|
|
109802
|
+
if (messageWasVerbatim) {
|
|
109803
|
+
return parseAssertContent(content.substring(0, pipeIndex).trim(), message, true);
|
|
109804
|
+
}
|
|
109709
109805
|
}
|
|
109710
109806
|
content = content.substring(0, pipeIndex).trim();
|
|
109711
109807
|
}
|
|
109808
|
+
return parseAssertContent(content, message, false);
|
|
109809
|
+
}
|
|
109810
|
+
function parseAssertContent(content, message, messageIsVerbatim) {
|
|
109712
109811
|
const existsMatch = content.match(/^(.+?)\s+(exists|!exists)$/i);
|
|
109713
109812
|
if (existsMatch) {
|
|
109714
109813
|
return {
|
|
109715
109814
|
leftExpr: existsMatch[1].trim(),
|
|
109716
109815
|
operator: existsMatch[2].toLowerCase(),
|
|
109717
|
-
message
|
|
109816
|
+
message,
|
|
109817
|
+
messageIsVerbatim
|
|
109718
109818
|
};
|
|
109719
109819
|
}
|
|
109720
109820
|
const binaryOperators = [
|
|
@@ -109738,7 +109838,8 @@ function parseAssertCommand(line2) {
|
|
|
109738
109838
|
leftExpr: binaryMatch[1].trim(),
|
|
109739
109839
|
operator: op,
|
|
109740
109840
|
rightExpr: binaryMatch[2].trim(),
|
|
109741
|
-
message
|
|
109841
|
+
message,
|
|
109842
|
+
messageIsVerbatim
|
|
109742
109843
|
};
|
|
109743
109844
|
}
|
|
109744
109845
|
}
|
|
@@ -109748,22 +109849,29 @@ function findUnquotedPipe(str) {
|
|
|
109748
109849
|
let inQuote = false;
|
|
109749
109850
|
let quoteChar = "";
|
|
109750
109851
|
let escapeNext = false;
|
|
109852
|
+
let inVerbatimString = false;
|
|
109751
109853
|
for (let i = 0; i < str.length; i++) {
|
|
109752
109854
|
const char = str[i];
|
|
109855
|
+
if (inQuote && inVerbatimString && char === '"' && str[i + 1] === '"') {
|
|
109856
|
+
i++;
|
|
109857
|
+
continue;
|
|
109858
|
+
}
|
|
109753
109859
|
if (escapeNext) {
|
|
109754
109860
|
escapeNext = false;
|
|
109755
109861
|
continue;
|
|
109756
109862
|
}
|
|
109757
|
-
if (inQuote && char === "\\") {
|
|
109863
|
+
if (inQuote && !inVerbatimString && char === "\\") {
|
|
109758
109864
|
escapeNext = true;
|
|
109759
109865
|
continue;
|
|
109760
109866
|
}
|
|
109761
109867
|
if ((char === '"' || char === "'") && !inQuote) {
|
|
109762
109868
|
inQuote = true;
|
|
109763
109869
|
quoteChar = char;
|
|
109870
|
+
inVerbatimString = char === '"' && i > 0 && str[i - 1] === "@";
|
|
109764
109871
|
} else if (char === quoteChar && inQuote) {
|
|
109765
109872
|
inQuote = false;
|
|
109766
109873
|
quoteChar = "";
|
|
109874
|
+
inVerbatimString = false;
|
|
109767
109875
|
} else if (char === "|" && !inQuote) {
|
|
109768
109876
|
return i;
|
|
109769
109877
|
}
|
|
@@ -109875,8 +109983,9 @@ function resolveValue(expr, responses, variables, getValueByPath2, responseIndex
|
|
|
109875
109983
|
}
|
|
109876
109984
|
return { value: void 0, error: `Variable {{${varName}}} is not defined` };
|
|
109877
109985
|
}
|
|
109878
|
-
if (
|
|
109879
|
-
|
|
109986
|
+
if (isQuotedStringLiteral(trimmed)) {
|
|
109987
|
+
const decoded = decodeQuotedStringLiteral(trimmed);
|
|
109988
|
+
return { value: isVerbatimStringLiteral(trimmed) ? decoded : substituteVariables(decoded, variables) };
|
|
109880
109989
|
}
|
|
109881
109990
|
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
109882
109991
|
return { value: parseFloat(trimmed) };
|
|
@@ -109949,7 +110058,7 @@ function areValuesEqual(leftValue, rightValue) {
|
|
|
109949
110058
|
}
|
|
109950
110059
|
function evaluateAssertion(assertion, responses, variables, getValueByPath2, responseIndexToVariable, basePath) {
|
|
109951
110060
|
const leftResult = resolveValue(assertion.leftExpr, responses, variables, getValueByPath2, responseIndexToVariable);
|
|
109952
|
-
const message = assertion.message === void 0 ? void 0 : substituteVariables(assertion.message, variables);
|
|
110061
|
+
const message = assertion.message === void 0 ? void 0 : assertion.messageIsVerbatim ? assertion.message : substituteVariables(assertion.message, variables);
|
|
109953
110062
|
const expression = formatExpression(assertion, variables);
|
|
109954
110063
|
const buildFailureContext = () => ({
|
|
109955
110064
|
responseIndex: leftResult.responseIndex,
|
|
@@ -110032,10 +110141,14 @@ function evaluateAssertion(assertion, responses, variables, getValueByPath2, res
|
|
|
110032
110141
|
}
|
|
110033
110142
|
if (assertion.operator === "matchesSchema") {
|
|
110034
110143
|
let schemaPath = assertion.rightExpr;
|
|
110035
|
-
|
|
110036
|
-
|
|
110144
|
+
let schemaPathIsVerbatim = false;
|
|
110145
|
+
if (isQuotedStringLiteral(schemaPath)) {
|
|
110146
|
+
schemaPathIsVerbatim = isVerbatimStringLiteral(schemaPath);
|
|
110147
|
+
schemaPath = decodeQuotedStringLiteral(schemaPath);
|
|
110148
|
+
}
|
|
110149
|
+
if (!schemaPathIsVerbatim) {
|
|
110150
|
+
schemaPath = substituteVariables(schemaPath, variables);
|
|
110037
110151
|
}
|
|
110038
|
-
schemaPath = substituteVariables(schemaPath, variables);
|
|
110039
110152
|
const validationResult = validateAgainstSchemaDetailed(leftValue, schemaPath, basePath);
|
|
110040
110153
|
const passed2 = validationResult.valid;
|
|
110041
110154
|
return {
|
|
@@ -110102,15 +110215,8 @@ function evaluateAssertion(assertion, responses, variables, getValueByPath2, res
|
|
|
110102
110215
|
break;
|
|
110103
110216
|
case "matches":
|
|
110104
110217
|
try {
|
|
110105
|
-
|
|
110106
|
-
|
|
110107
|
-
const lastSlash = pattern.lastIndexOf("/");
|
|
110108
|
-
const flags = pattern.substring(lastSlash + 1);
|
|
110109
|
-
pattern = pattern.substring(1, lastSlash);
|
|
110110
|
-
passed = new RegExp(pattern, flags).test(String(leftValue));
|
|
110111
|
-
} else {
|
|
110112
|
-
passed = new RegExp(pattern).test(String(leftValue));
|
|
110113
|
-
}
|
|
110218
|
+
const pattern = String(rightValue);
|
|
110219
|
+
passed = new RegExp(pattern).test(String(leftValue));
|
|
110114
110220
|
} catch (e) {
|
|
110115
110221
|
return {
|
|
110116
110222
|
passed: false,
|
|
@@ -110146,7 +110252,7 @@ function formatExpression(assertion, variables) {
|
|
|
110146
110252
|
return substituteAssertionDisplayTemplates(`${assertion.leftExpr} ${assertion.operator}`, variables);
|
|
110147
110253
|
}
|
|
110148
110254
|
function substituteAssertionDisplayTemplates(expression, variables) {
|
|
110149
|
-
return variables ?
|
|
110255
|
+
return variables ? mapOutsideVerbatimStrings(expression, (segment) => substituteVariables(segment, variables)) : expression;
|
|
110150
110256
|
}
|
|
110151
110257
|
|
|
110152
110258
|
// src/jsonFileReader.ts
|
|
@@ -110163,8 +110269,8 @@ function parseJsonCommand(line2) {
|
|
|
110163
110269
|
return null;
|
|
110164
110270
|
}
|
|
110165
110271
|
let filePath = match[2].trim();
|
|
110166
|
-
if (
|
|
110167
|
-
filePath = filePath
|
|
110272
|
+
if (isQuotedStringLiteral(filePath)) {
|
|
110273
|
+
filePath = decodeQuotedStringLiteral(filePath);
|
|
110168
110274
|
}
|
|
110169
110275
|
return {
|
|
110170
110276
|
varName: match[1],
|
|
@@ -128016,7 +128122,7 @@ function parseRunArguments(argsStr) {
|
|
|
128016
128122
|
if (!argsStr || !argsStr.trim()) {
|
|
128017
128123
|
return args;
|
|
128018
128124
|
}
|
|
128019
|
-
const parts = argsStr
|
|
128125
|
+
const parts = splitNamedArgumentList(argsStr);
|
|
128020
128126
|
for (const part of parts) {
|
|
128021
128127
|
const trimmed = part.trim();
|
|
128022
128128
|
if (!trimmed) {
|
|
@@ -128025,7 +128131,7 @@ function parseRunArguments(argsStr) {
|
|
|
128025
128131
|
const namedMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.+)$/);
|
|
128026
128132
|
if (namedMatch) {
|
|
128027
128133
|
let value = namedMatch[2].trim();
|
|
128028
|
-
if (
|
|
128134
|
+
if (isQuotedStringLiteral(value)) {
|
|
128029
128135
|
value = decodeQuotedStringLiteral(value);
|
|
128030
128136
|
}
|
|
128031
128137
|
args.push({
|
|
@@ -128034,7 +128140,7 @@ function parseRunArguments(argsStr) {
|
|
|
128034
128140
|
});
|
|
128035
128141
|
} else {
|
|
128036
128142
|
let value = trimmed;
|
|
128037
|
-
if (
|
|
128143
|
+
if (isQuotedStringLiteral(value)) {
|
|
128038
128144
|
value = decodeQuotedStringLiteral(value);
|
|
128039
128145
|
}
|
|
128040
128146
|
args.push({ value });
|
|
@@ -128046,6 +128152,7 @@ function splitNamedArgumentList(argsStr) {
|
|
|
128046
128152
|
const parts = [];
|
|
128047
128153
|
let current = "";
|
|
128048
128154
|
let quoteChar = null;
|
|
128155
|
+
let inVerbatimString = false;
|
|
128049
128156
|
let templateDepth = 0;
|
|
128050
128157
|
let bracketDepth = 0;
|
|
128051
128158
|
for (let i = 0; i < argsStr.length; i++) {
|
|
@@ -128054,13 +128161,20 @@ function splitNamedArgumentList(argsStr) {
|
|
|
128054
128161
|
const prev = i > 0 ? argsStr[i - 1] : "";
|
|
128055
128162
|
if (quoteChar) {
|
|
128056
128163
|
current += char;
|
|
128164
|
+
if (inVerbatimString && char === '"' && next === '"') {
|
|
128165
|
+
current += next;
|
|
128166
|
+
i++;
|
|
128167
|
+
continue;
|
|
128168
|
+
}
|
|
128057
128169
|
if (char === quoteChar && prev !== "\\") {
|
|
128058
128170
|
quoteChar = null;
|
|
128171
|
+
inVerbatimString = false;
|
|
128059
128172
|
}
|
|
128060
128173
|
continue;
|
|
128061
128174
|
}
|
|
128062
128175
|
if (char === '"' || char === "'") {
|
|
128063
128176
|
quoteChar = char;
|
|
128177
|
+
inVerbatimString = char === '"' && prev === "@";
|
|
128064
128178
|
current += char;
|
|
128065
128179
|
continue;
|
|
128066
128180
|
}
|
|
@@ -128520,7 +128634,7 @@ function parseSequenceParameters(line2) {
|
|
|
128520
128634
|
if (!paramsStr) {
|
|
128521
128635
|
return params;
|
|
128522
128636
|
}
|
|
128523
|
-
const paramParts = paramsStr
|
|
128637
|
+
const paramParts = splitNamedArgumentList(paramsStr);
|
|
128524
128638
|
let hasSeenDefault = false;
|
|
128525
128639
|
for (const part of paramParts) {
|
|
128526
128640
|
const trimmed = part.trim();
|
|
@@ -128531,7 +128645,7 @@ function parseSequenceParameters(line2) {
|
|
|
128531
128645
|
if (defaultMatch) {
|
|
128532
128646
|
hasSeenDefault = true;
|
|
128533
128647
|
let defaultValue = defaultMatch[2].trim();
|
|
128534
|
-
if (
|
|
128648
|
+
if (isQuotedStringLiteral(defaultValue)) {
|
|
128535
128649
|
defaultValue = decodeQuotedStringLiteral(defaultValue);
|
|
128536
128650
|
}
|
|
128537
128651
|
params.push({
|
|
@@ -128608,14 +128722,21 @@ function parseDataValues(valuesStr) {
|
|
|
128608
128722
|
const values = [];
|
|
128609
128723
|
let current = "";
|
|
128610
128724
|
let inQuote = null;
|
|
128725
|
+
let inVerbatimString = false;
|
|
128611
128726
|
let i = 0;
|
|
128612
128727
|
while (i < valuesStr.length) {
|
|
128613
128728
|
const char = valuesStr[i];
|
|
128614
128729
|
if (inQuote) {
|
|
128730
|
+
if (inVerbatimString && char === '"' && valuesStr[i + 1] === '"') {
|
|
128731
|
+
current += '""';
|
|
128732
|
+
i += 2;
|
|
128733
|
+
continue;
|
|
128734
|
+
}
|
|
128615
128735
|
if (char === inQuote) {
|
|
128616
|
-
values.push(current);
|
|
128736
|
+
values.push(inVerbatimString ? decodeQuotedStringLiteral(`@"${current}"`) : current);
|
|
128617
128737
|
current = "";
|
|
128618
128738
|
inQuote = null;
|
|
128739
|
+
inVerbatimString = false;
|
|
128619
128740
|
i++;
|
|
128620
128741
|
while (i < valuesStr.length && valuesStr[i] !== ",") {
|
|
128621
128742
|
i++;
|
|
@@ -128626,8 +128747,15 @@ function parseDataValues(valuesStr) {
|
|
|
128626
128747
|
current += char;
|
|
128627
128748
|
}
|
|
128628
128749
|
} else {
|
|
128629
|
-
if (char ===
|
|
128750
|
+
if (char === "@" && valuesStr[i + 1] === '"') {
|
|
128751
|
+
inQuote = '"';
|
|
128752
|
+
inVerbatimString = true;
|
|
128753
|
+
current = "";
|
|
128754
|
+
i += 2;
|
|
128755
|
+
continue;
|
|
128756
|
+
} else if (char === '"' || char === "'") {
|
|
128630
128757
|
inQuote = char;
|
|
128758
|
+
inVerbatimString = false;
|
|
128631
128759
|
current = "";
|
|
128632
128760
|
} else if (char === ",") {
|
|
128633
128761
|
const trimmed = current.trim();
|
|
@@ -128664,7 +128792,7 @@ function parseTypedValue(value) {
|
|
|
128664
128792
|
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
128665
128793
|
return parseFloat(value);
|
|
128666
128794
|
}
|
|
128667
|
-
if (
|
|
128795
|
+
if (isQuotedStringLiteral(value)) {
|
|
128668
128796
|
return decodeQuotedStringLiteral(value);
|
|
128669
128797
|
}
|
|
128670
128798
|
return value;
|
|
@@ -128790,9 +128918,9 @@ function parseVarAssignCommand(line2) {
|
|
|
128790
128918
|
}
|
|
128791
128919
|
function evaluateValueExpression(expr, runtimeVariables) {
|
|
128792
128920
|
const trimmed = expr.trim();
|
|
128793
|
-
if (
|
|
128921
|
+
if (isQuotedStringLiteral(trimmed)) {
|
|
128794
128922
|
const inner = decodeQuotedStringLiteral(trimmed);
|
|
128795
|
-
const substituted = substituteVariables(inner, runtimeVariables);
|
|
128923
|
+
const substituted = isVerbatimStringLiteral(trimmed) ? inner : substituteVariables(inner, runtimeVariables);
|
|
128796
128924
|
return { value: substituted };
|
|
128797
128925
|
}
|
|
128798
128926
|
if (trimmed === "true" || trimmed === "false" || trimmed === "null") {
|
|
@@ -128840,9 +128968,9 @@ function evaluateSqlArgumentExpression(expr, runtimeVariables) {
|
|
|
128840
128968
|
if (!trimmed) {
|
|
128841
128969
|
return { value: "" };
|
|
128842
128970
|
}
|
|
128843
|
-
if (
|
|
128971
|
+
if (isQuotedStringLiteral(trimmed)) {
|
|
128844
128972
|
const inner = decodeQuotedStringLiteral(trimmed);
|
|
128845
|
-
return { value: substituteVariables(inner, runtimeVariables) };
|
|
128973
|
+
return { value: isVerbatimStringLiteral(trimmed) ? inner : substituteVariables(inner, runtimeVariables) };
|
|
128846
128974
|
}
|
|
128847
128975
|
if (trimmed.startsWith("{{") && trimmed.endsWith("}}")) {
|
|
128848
128976
|
return evaluateSqlArgumentExpression(trimmed.slice(2, -2).trim(), runtimeVariables);
|
|
@@ -128952,7 +129080,7 @@ function resolveBareVariables(text, variables) {
|
|
|
128952
129080
|
const parts = splitExpressionParts(text);
|
|
128953
129081
|
const resolvedParts = parts.map((part) => {
|
|
128954
129082
|
const partTrimmed = part.trim();
|
|
128955
|
-
if (
|
|
129083
|
+
if (isQuotedStringLiteral(partTrimmed)) {
|
|
128956
129084
|
return decodeQuotedStringLiteral(partTrimmed);
|
|
128957
129085
|
}
|
|
128958
129086
|
const varMatch = partTrimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
|
|
@@ -128980,6 +129108,7 @@ function splitExpressionParts(expr) {
|
|
|
128980
129108
|
let inString = false;
|
|
128981
129109
|
let stringChar = "";
|
|
128982
129110
|
let escapeNext = false;
|
|
129111
|
+
let inVerbatimString = false;
|
|
128983
129112
|
for (let i = 0; i < expr.length; i++) {
|
|
128984
129113
|
const char = expr[i];
|
|
128985
129114
|
if (escapeNext) {
|
|
@@ -128987,7 +129116,12 @@ function splitExpressionParts(expr) {
|
|
|
128987
129116
|
escapeNext = false;
|
|
128988
129117
|
continue;
|
|
128989
129118
|
}
|
|
128990
|
-
if (inString && char === "
|
|
129119
|
+
if (inString && inVerbatimString && char === '"' && i + 1 < expr.length && expr[i + 1] === '"') {
|
|
129120
|
+
current += char + expr[i + 1];
|
|
129121
|
+
i++;
|
|
129122
|
+
continue;
|
|
129123
|
+
}
|
|
129124
|
+
if (inString && !inVerbatimString && char === "\\") {
|
|
128991
129125
|
current += char;
|
|
128992
129126
|
escapeNext = true;
|
|
128993
129127
|
continue;
|
|
@@ -128995,9 +129129,11 @@ function splitExpressionParts(expr) {
|
|
|
128995
129129
|
if (!inString && (char === '"' || char === "'")) {
|
|
128996
129130
|
inString = true;
|
|
128997
129131
|
stringChar = char;
|
|
129132
|
+
inVerbatimString = char === '"' && i > 0 && expr[i - 1] === "@";
|
|
128998
129133
|
current += char;
|
|
128999
129134
|
} else if (inString && char === stringChar) {
|
|
129000
129135
|
inString = false;
|
|
129136
|
+
inVerbatimString = false;
|
|
129001
129137
|
current += char;
|
|
129002
129138
|
} else if (!inString && char === "+") {
|
|
129003
129139
|
parts.push(current);
|
|
@@ -131444,7 +131580,8 @@ function formatAssertion(assertion, options) {
|
|
|
131444
131580
|
if (assertion.error) {
|
|
131445
131581
|
lines.push(` ${colors.error(`Error: ${assertion.error}`)}`);
|
|
131446
131582
|
} else if (!assertion.passed) {
|
|
131447
|
-
|
|
131583
|
+
const expectedValue = assertion.rightValue !== void 0 ? assertion.rightValue : assertion.rightExpression ?? assertion.operator;
|
|
131584
|
+
lines.push(` ${colors.dim("Expected:")} ${formatValue(expectedValue)}`);
|
|
131448
131585
|
lines.push(` ${colors.dim("Actual:")} ${formatValue(assertion.leftValue)}`);
|
|
131449
131586
|
if (verbose && assertion.leftExpression !== String(assertion.leftValue)) {
|
|
131450
131587
|
lines.push(` ${colors.dim("Expression:")} ${assertion.leftExpression}`);
|
|
@@ -131738,12 +131875,13 @@ function generateTestCase(sequenceName, assertion, redaction) {
|
|
|
131738
131875
|
let xml = ` <testcase name="${escapeXml(testName)}" classname="${escapeXml(className)}">
|
|
131739
131876
|
`;
|
|
131740
131877
|
if (!assertion.passed) {
|
|
131741
|
-
const
|
|
131878
|
+
const expectedValue = assertion.rightValue !== void 0 ? assertion.rightValue : assertion.rightExpression;
|
|
131879
|
+
const message = assertion.error || `Expected ${JSON.stringify(expectedValue)}, got ${JSON.stringify(assertion.leftValue)}`;
|
|
131742
131880
|
xml += ` <failure message="${escapeXml(redactString(message, redaction))}" type="AssertionError">
|
|
131743
131881
|
`;
|
|
131744
131882
|
xml += `<![CDATA[Expression: ${assertion.expression}
|
|
131745
131883
|
`;
|
|
131746
|
-
xml += `Expected: ${JSON.stringify(
|
|
131884
|
+
xml += `Expected: ${JSON.stringify(expectedValue)}
|
|
131747
131885
|
`;
|
|
131748
131886
|
xml += `Actual: ${JSON.stringify(assertion.leftValue)}]]>
|
|
131749
131887
|
`;
|
|
@@ -132115,11 +132253,13 @@ function generateAssertionHtml(step, redaction) {
|
|
|
132115
132253
|
} else {
|
|
132116
132254
|
actualDisplay = String(assertion.leftValue);
|
|
132117
132255
|
}
|
|
132256
|
+
const expectedValue = assertion.rightValue !== void 0 ? assertion.rightValue : assertion.rightExpression;
|
|
132257
|
+
const expectedDisplay = typeof expectedValue === "object" ? JSON.stringify(expectedValue, null, 2) : String(expectedValue);
|
|
132118
132258
|
const pathInfo = assertion.jsonPath ? `<div class="assertion-path"><strong>Path:</strong> <code>${escapeHtml(assertion.jsonPath)}</code></div>` : "";
|
|
132119
132259
|
detailsHtml = `
|
|
132120
132260
|
<div class="assertion-details">
|
|
132121
132261
|
${pathInfo}
|
|
132122
|
-
<div><strong>Expected:</strong> <code>${escapeHtml(
|
|
132262
|
+
<div><strong>Expected:</strong> <code>${escapeHtml(expectedDisplay)}</code></div>
|
|
132123
132263
|
<div><strong>Actual:</strong> <code>${escapeHtml(actualDisplay)}</code></div>
|
|
132124
132264
|
${assertion.error ? `<div class="error"><strong>Error:</strong> ${escapeHtml(redactString(assertion.error, redaction))}</div>` : ""}
|
|
132125
132265
|
</div>`;
|
|
@@ -133770,6 +133910,294 @@ function splitImportResolutionErrors(errors) {
|
|
|
133770
133910
|
return { blockingErrors, warningErrors };
|
|
133771
133911
|
}
|
|
133772
133912
|
|
|
133913
|
+
// src/nornenvLanguageModel.ts
|
|
133914
|
+
var SECTION_HEADER_REGEX = /^\s*\[(env|template):([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s+extends\s+([^\]]+))?\]\s*$/i;
|
|
133915
|
+
function parseParentList(clause) {
|
|
133916
|
+
if (!clause) {
|
|
133917
|
+
return [];
|
|
133918
|
+
}
|
|
133919
|
+
return clause.split(",").map((parent) => parent.trim()).filter((parent) => /^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(parent));
|
|
133920
|
+
}
|
|
133921
|
+
function parseNornenvDocumentModel(text) {
|
|
133922
|
+
const sections = [];
|
|
133923
|
+
const declarations = [];
|
|
133924
|
+
const lines = text.split("\n");
|
|
133925
|
+
let currentSection;
|
|
133926
|
+
for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
|
|
133927
|
+
const line2 = lines[lineNumber];
|
|
133928
|
+
const sectionMatch = line2.trim().match(SECTION_HEADER_REGEX);
|
|
133929
|
+
if (sectionMatch) {
|
|
133930
|
+
currentSection = {
|
|
133931
|
+
kind: sectionMatch[1].toLowerCase(),
|
|
133932
|
+
name: sectionMatch[2],
|
|
133933
|
+
parents: parseParentList(sectionMatch[3]),
|
|
133934
|
+
lineNumber
|
|
133935
|
+
};
|
|
133936
|
+
sections.push(currentSection);
|
|
133937
|
+
continue;
|
|
133938
|
+
}
|
|
133939
|
+
const connectionMatch = line2.match(/^(\s*)(secret\s+)?connectionString\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$/i);
|
|
133940
|
+
if (connectionMatch) {
|
|
133941
|
+
const displayName = connectionMatch[3];
|
|
133942
|
+
const name = `${displayName}_connectionString`;
|
|
133943
|
+
const nameStart = line2.indexOf(displayName);
|
|
133944
|
+
const value = connectionMatch[4];
|
|
133945
|
+
const valueStart = line2.length - value.length;
|
|
133946
|
+
declarations.push({
|
|
133947
|
+
name,
|
|
133948
|
+
displayName,
|
|
133949
|
+
value,
|
|
133950
|
+
secret: Boolean(connectionMatch[2]),
|
|
133951
|
+
sectionKind: currentSection?.kind,
|
|
133952
|
+
sectionName: currentSection?.name,
|
|
133953
|
+
lineNumber,
|
|
133954
|
+
nameStart,
|
|
133955
|
+
nameEnd: nameStart + displayName.length,
|
|
133956
|
+
valueStart,
|
|
133957
|
+
valueEnd: line2.length
|
|
133958
|
+
});
|
|
133959
|
+
continue;
|
|
133960
|
+
}
|
|
133961
|
+
const variableMatch = line2.match(/^(\s*)(secret|var)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$/i);
|
|
133962
|
+
if (variableMatch) {
|
|
133963
|
+
const name = variableMatch[3];
|
|
133964
|
+
const nameStart = line2.indexOf(name);
|
|
133965
|
+
const value = variableMatch[4];
|
|
133966
|
+
const valueStart = line2.length - value.length;
|
|
133967
|
+
declarations.push({
|
|
133968
|
+
name,
|
|
133969
|
+
displayName: name,
|
|
133970
|
+
value,
|
|
133971
|
+
secret: variableMatch[2].toLowerCase() === "secret",
|
|
133972
|
+
sectionKind: currentSection?.kind,
|
|
133973
|
+
sectionName: currentSection?.name,
|
|
133974
|
+
lineNumber,
|
|
133975
|
+
nameStart,
|
|
133976
|
+
nameEnd: nameStart + name.length,
|
|
133977
|
+
valueStart,
|
|
133978
|
+
valueEnd: line2.length
|
|
133979
|
+
});
|
|
133980
|
+
}
|
|
133981
|
+
}
|
|
133982
|
+
return { sections, declarations };
|
|
133983
|
+
}
|
|
133984
|
+
|
|
133985
|
+
// src/nornenvRegionRefactor.ts
|
|
133986
|
+
var MATRIX_ENV_NAME_REGEX = /^([a-zA-Z][a-zA-Z0-9-]*)_([a-zA-Z][a-zA-Z0-9-]*)$/;
|
|
133987
|
+
var MIN_STAGES = 2;
|
|
133988
|
+
var MIN_REGIONS = 2;
|
|
133989
|
+
var MIN_CELLS = 3;
|
|
133990
|
+
function isConnectionStringVar(name) {
|
|
133991
|
+
return name.endsWith("_connectionString");
|
|
133992
|
+
}
|
|
133993
|
+
function inferDeclarationKind(name) {
|
|
133994
|
+
return isConnectionStringVar(name) ? "connectionString" : "var";
|
|
133995
|
+
}
|
|
133996
|
+
function inferDisplayName(name) {
|
|
133997
|
+
return isConnectionStringVar(name) ? name.slice(0, -"_connectionString".length) : name;
|
|
133998
|
+
}
|
|
133999
|
+
function declarationKind(declaration) {
|
|
134000
|
+
return declaration.name.endsWith("_connectionString") && declaration.displayName !== declaration.name ? "connectionString" : "var";
|
|
134001
|
+
}
|
|
134002
|
+
function buildDeclarationMap(text) {
|
|
134003
|
+
const model = parseNornenvDocumentModel(text);
|
|
134004
|
+
const byEnv = /* @__PURE__ */ new Map();
|
|
134005
|
+
for (const declaration of model.declarations) {
|
|
134006
|
+
if (declaration.sectionKind !== "env" || !declaration.sectionName) {
|
|
134007
|
+
continue;
|
|
134008
|
+
}
|
|
134009
|
+
if (!byEnv.has(declaration.sectionName)) {
|
|
134010
|
+
byEnv.set(declaration.sectionName, /* @__PURE__ */ new Map());
|
|
134011
|
+
}
|
|
134012
|
+
byEnv.get(declaration.sectionName).set(declaration.name, declaration);
|
|
134013
|
+
}
|
|
134014
|
+
return byEnv;
|
|
134015
|
+
}
|
|
134016
|
+
function getCellValue(cell, name, declarationsByEnv, config2) {
|
|
134017
|
+
const value = cell.env.variables[name];
|
|
134018
|
+
if (value === void 0) {
|
|
134019
|
+
return void 0;
|
|
134020
|
+
}
|
|
134021
|
+
const declaration = declarationsByEnv.get(cell.envName)?.get(name);
|
|
134022
|
+
return {
|
|
134023
|
+
value,
|
|
134024
|
+
secret: declaration?.secret ?? config2.secretNames.has(name),
|
|
134025
|
+
kind: declaration ? declarationKind(declaration) : inferDeclarationKind(name),
|
|
134026
|
+
displayName: declaration?.displayName ?? inferDisplayName(name)
|
|
134027
|
+
};
|
|
134028
|
+
}
|
|
134029
|
+
function classifyByAxis(name, cells, axisKeys, getAxisKey, declarationsByEnv, config2) {
|
|
134030
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
134031
|
+
let presentCells = 0;
|
|
134032
|
+
for (const key of axisKeys) {
|
|
134033
|
+
const axisCells = cells.filter((cell) => getAxisKey(cell) === key);
|
|
134034
|
+
const presentValues = axisCells.map((cell) => getCellValue(cell, name, declarationsByEnv, config2)).filter((value) => value !== void 0);
|
|
134035
|
+
if (presentValues.length === 0) {
|
|
134036
|
+
continue;
|
|
134037
|
+
}
|
|
134038
|
+
presentCells += presentValues.length;
|
|
134039
|
+
if (presentValues.length !== axisCells.length) {
|
|
134040
|
+
return void 0;
|
|
134041
|
+
}
|
|
134042
|
+
const first = presentValues[0];
|
|
134043
|
+
if (presentValues.some((value) => value.value !== first.value)) {
|
|
134044
|
+
return void 0;
|
|
134045
|
+
}
|
|
134046
|
+
byKey.set(key, {
|
|
134047
|
+
value: first.value,
|
|
134048
|
+
secret: presentValues.some((value) => value.secret),
|
|
134049
|
+
kind: first.kind,
|
|
134050
|
+
displayName: first.displayName
|
|
134051
|
+
});
|
|
134052
|
+
}
|
|
134053
|
+
return byKey.size > 0 && presentCells >= 2 ? byKey : void 0;
|
|
134054
|
+
}
|
|
134055
|
+
function detectRegionPattern(config2, text) {
|
|
134056
|
+
const cells = [];
|
|
134057
|
+
for (const env3 of config2.environments) {
|
|
134058
|
+
const match = env3.name.match(MATRIX_ENV_NAME_REGEX);
|
|
134059
|
+
if (!match) {
|
|
134060
|
+
continue;
|
|
134061
|
+
}
|
|
134062
|
+
if (env3.parents.length > 0) {
|
|
134063
|
+
return void 0;
|
|
134064
|
+
}
|
|
134065
|
+
cells.push({ stage: match[1], region: match[2], envName: env3.name, env: env3 });
|
|
134066
|
+
}
|
|
134067
|
+
const stages = Array.from(new Set(cells.map((c) => c.stage)));
|
|
134068
|
+
const regions = Array.from(new Set(cells.map((c) => c.region)));
|
|
134069
|
+
if (stages.length < MIN_STAGES || regions.length < MIN_REGIONS || cells.length < MIN_CELLS) {
|
|
134070
|
+
return void 0;
|
|
134071
|
+
}
|
|
134072
|
+
if ((/* @__PURE__ */ new Set([...stages, ...regions])).size !== stages.length + regions.length) {
|
|
134073
|
+
return void 0;
|
|
134074
|
+
}
|
|
134075
|
+
const existingTemplates = new Set(config2.templates.map((template) => template.name));
|
|
134076
|
+
if ([...stages, ...regions].some((name) => existingTemplates.has(name))) {
|
|
134077
|
+
return void 0;
|
|
134078
|
+
}
|
|
134079
|
+
const allVarNames = /* @__PURE__ */ new Set();
|
|
134080
|
+
for (const cell of cells) {
|
|
134081
|
+
for (const name of Object.keys(cell.env.variables)) {
|
|
134082
|
+
allVarNames.add(name);
|
|
134083
|
+
}
|
|
134084
|
+
}
|
|
134085
|
+
let liftedToStage = 0;
|
|
134086
|
+
let liftedToRegion = 0;
|
|
134087
|
+
let leafSpecific = 0;
|
|
134088
|
+
let skippedConnectionStrings = 0;
|
|
134089
|
+
const assignments = [];
|
|
134090
|
+
const declarationsByEnv = buildDeclarationMap(text);
|
|
134091
|
+
for (const name of allVarNames) {
|
|
134092
|
+
if (isConnectionStringVar(name)) {
|
|
134093
|
+
skippedConnectionStrings++;
|
|
134094
|
+
}
|
|
134095
|
+
const stageValues = isConnectionStringVar(name) ? void 0 : classifyByAxis(name, cells, stages, (cell) => cell.stage, declarationsByEnv, config2);
|
|
134096
|
+
const regionValues = isConnectionStringVar(name) ? void 0 : classifyByAxis(name, cells, regions, (cell) => cell.region, declarationsByEnv, config2);
|
|
134097
|
+
if (stageValues) {
|
|
134098
|
+
assignments.push({ name, axis: "stage", byKey: stageValues });
|
|
134099
|
+
liftedToStage++;
|
|
134100
|
+
} else if (regionValues) {
|
|
134101
|
+
assignments.push({ name, axis: "region", byKey: regionValues });
|
|
134102
|
+
liftedToRegion++;
|
|
134103
|
+
} else {
|
|
134104
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
134105
|
+
for (const cell of cells) {
|
|
134106
|
+
const value = getCellValue(cell, name, declarationsByEnv, config2);
|
|
134107
|
+
if (value !== void 0) {
|
|
134108
|
+
byKey.set(cell.envName, value);
|
|
134109
|
+
}
|
|
134110
|
+
}
|
|
134111
|
+
if (byKey.size > 0) {
|
|
134112
|
+
assignments.push({ name, axis: "leaf", byKey });
|
|
134113
|
+
leafSpecific++;
|
|
134114
|
+
}
|
|
134115
|
+
}
|
|
134116
|
+
}
|
|
134117
|
+
const model = parseNornenvDocumentModel(text);
|
|
134118
|
+
const matrixNames = new Set(cells.map((c) => c.envName));
|
|
134119
|
+
const matrixSectionLines = model.sections.filter((s) => s.kind === "env" && matrixNames.has(s.name)).map((s) => s.lineNumber).sort((a, b) => a - b);
|
|
134120
|
+
if (matrixSectionLines.length === 0) {
|
|
134121
|
+
return void 0;
|
|
134122
|
+
}
|
|
134123
|
+
const firstLine = matrixSectionLines[0];
|
|
134124
|
+
const lastMatrixHeaderLine = matrixSectionLines[matrixSectionLines.length - 1];
|
|
134125
|
+
const sectionsInReplacementRange = model.sections.filter(
|
|
134126
|
+
(section) => section.lineNumber >= firstLine && section.lineNumber <= lastMatrixHeaderLine
|
|
134127
|
+
);
|
|
134128
|
+
if (sectionsInReplacementRange.some((section) => section.kind !== "env" || !matrixNames.has(section.name))) {
|
|
134129
|
+
return void 0;
|
|
134130
|
+
}
|
|
134131
|
+
const lineCount = text.split("\n").length;
|
|
134132
|
+
const sectionsAfter = model.sections.filter((s) => s.lineNumber > lastMatrixHeaderLine).map((s) => s.lineNumber).sort((a, b) => a - b);
|
|
134133
|
+
const lastLine = sectionsAfter.length > 0 ? sectionsAfter[0] - 1 : lineCount - 1;
|
|
134134
|
+
return {
|
|
134135
|
+
stages,
|
|
134136
|
+
regions,
|
|
134137
|
+
cells,
|
|
134138
|
+
assignments,
|
|
134139
|
+
replaceRange: { startLine: firstLine, endLine: lastLine },
|
|
134140
|
+
summary: { liftedToStage, liftedToRegion, leafSpecific, skippedConnectionStrings }
|
|
134141
|
+
};
|
|
134142
|
+
}
|
|
134143
|
+
function generateRegionRefactor(pattern) {
|
|
134144
|
+
const blocks = [];
|
|
134145
|
+
const fmtVar = (assignment, value) => {
|
|
134146
|
+
if (value.kind === "connectionString") {
|
|
134147
|
+
const secretPrefix = value.secret ? "secret " : "";
|
|
134148
|
+
return ` ${secretPrefix}connectionString ${value.displayName} = ${value.value}`;
|
|
134149
|
+
}
|
|
134150
|
+
const keyword = value.secret ? "secret" : "var";
|
|
134151
|
+
return ` ${keyword} ${assignment.name} = ${value.value}`;
|
|
134152
|
+
};
|
|
134153
|
+
for (const stage of pattern.stages) {
|
|
134154
|
+
const stageVars = pattern.assignments.filter((a) => a.axis === "stage" && a.byKey.has(stage));
|
|
134155
|
+
if (stageVars.length === 0) {
|
|
134156
|
+
continue;
|
|
134157
|
+
}
|
|
134158
|
+
const lines = [`[template:${stage}]`];
|
|
134159
|
+
for (const v of stageVars) {
|
|
134160
|
+
lines.push(fmtVar(v, v.byKey.get(stage)));
|
|
134161
|
+
}
|
|
134162
|
+
blocks.push(lines.join("\n"));
|
|
134163
|
+
}
|
|
134164
|
+
for (const region of pattern.regions) {
|
|
134165
|
+
const regionVars = pattern.assignments.filter((a) => a.axis === "region" && a.byKey.has(region));
|
|
134166
|
+
if (regionVars.length === 0) {
|
|
134167
|
+
continue;
|
|
134168
|
+
}
|
|
134169
|
+
const lines = [`[template:${region}]`];
|
|
134170
|
+
for (const v of regionVars) {
|
|
134171
|
+
lines.push(fmtVar(v, v.byKey.get(region)));
|
|
134172
|
+
}
|
|
134173
|
+
blocks.push(lines.join("\n"));
|
|
134174
|
+
}
|
|
134175
|
+
for (const cell of pattern.cells) {
|
|
134176
|
+
const leafVars = pattern.assignments.filter((a) => a.axis === "leaf" && a.byKey.has(cell.envName));
|
|
134177
|
+
const header = `[env:${cell.envName} extends ${cell.stage}, ${cell.region}]`;
|
|
134178
|
+
if (leafVars.length === 0) {
|
|
134179
|
+
blocks.push(header);
|
|
134180
|
+
continue;
|
|
134181
|
+
}
|
|
134182
|
+
const lines = [header];
|
|
134183
|
+
for (const v of leafVars) {
|
|
134184
|
+
lines.push(fmtVar(v, v.byKey.get(cell.envName)));
|
|
134185
|
+
}
|
|
134186
|
+
blocks.push(lines.join("\n"));
|
|
134187
|
+
}
|
|
134188
|
+
return blocks.join("\n\n");
|
|
134189
|
+
}
|
|
134190
|
+
function applyRegionRefactorToText(text, pattern) {
|
|
134191
|
+
const lines = text.split("\n");
|
|
134192
|
+
const startLine = pattern.replaceRange.startLine;
|
|
134193
|
+
const endLine = Math.min(pattern.replaceRange.endLine, lines.length - 1);
|
|
134194
|
+
const replacementLines = generateRegionRefactor(pattern).split("\n");
|
|
134195
|
+
lines.splice(startLine, endLine - startLine + 1, ...replacementLines);
|
|
134196
|
+
const result = lines.join("\n");
|
|
134197
|
+
return text.endsWith("\n") && !result.endsWith("\n") ? `${result}
|
|
134198
|
+
` : result;
|
|
134199
|
+
}
|
|
134200
|
+
|
|
133773
134201
|
// src/cli.ts
|
|
133774
134202
|
function handleImportResolutionErrors(errors, colors) {
|
|
133775
134203
|
const { blockingErrors, warningErrors } = splitImportResolutionErrors(errors);
|
|
@@ -133884,7 +134312,9 @@ function parseArgs(args) {
|
|
|
133884
134312
|
noRedact: false,
|
|
133885
134313
|
tagFilters: [],
|
|
133886
134314
|
tagsFilter: [],
|
|
133887
|
-
insecure: false
|
|
134315
|
+
insecure: false,
|
|
134316
|
+
refactorRegionPattern: false,
|
|
134317
|
+
writeRefactor: false
|
|
133888
134318
|
};
|
|
133889
134319
|
for (let i = 0; i < args.length; i++) {
|
|
133890
134320
|
const arg = args[i];
|
|
@@ -133902,6 +134332,10 @@ function parseArgs(args) {
|
|
|
133902
134332
|
options.timeout = parseInt(args[++i], 10) * 1e3;
|
|
133903
134333
|
} else if (arg === "--insecure") {
|
|
133904
134334
|
options.insecure = true;
|
|
134335
|
+
} else if (arg === "--refactor-region-pattern" || arg === "--refactor-nornenv-region-pattern") {
|
|
134336
|
+
options.refactorRegionPattern = true;
|
|
134337
|
+
} else if (arg === "--write") {
|
|
134338
|
+
options.writeRefactor = true;
|
|
133905
134339
|
} else if (arg === "--no-fail") {
|
|
133906
134340
|
options.failOnError = false;
|
|
133907
134341
|
} else if (arg === "--no-redact") {
|
|
@@ -133957,6 +134391,9 @@ Options:
|
|
|
133957
134391
|
-o, --output-dir <dir> Output directory for reports (auto-generates timestamped files)
|
|
133958
134392
|
--tag <filter> Filter sequences by tag (AND logic, can be repeated)
|
|
133959
134393
|
--tags <filters> Filter sequences by tags (OR logic, comma-separated)
|
|
134394
|
+
--refactor-region-pattern
|
|
134395
|
+
Refactor a flat .nornenv STAGE_REGION matrix to templates
|
|
134396
|
+
--write Apply --refactor-region-pattern instead of printing result
|
|
133960
134397
|
-h, --help Show this help message
|
|
133961
134398
|
|
|
133962
134399
|
Report Generation:
|
|
@@ -134000,11 +134437,62 @@ Examples:
|
|
|
134000
134437
|
norn api-tests.norn --html report.html # Generate HTML report (explicit)
|
|
134001
134438
|
norn api-tests.norn --insecure # Allow self-signed/local TLS certs
|
|
134002
134439
|
norn api-tests.norn --no-redact # Show all data (no redaction)
|
|
134440
|
+
norn .nornenv --refactor-region-pattern # Print refactored .nornenv
|
|
134441
|
+
norn .nornenv --refactor-region-pattern --write
|
|
134003
134442
|
norn secrets keygen --name team-main # Generate shared key and cache locally
|
|
134004
134443
|
norn secrets import-key --kid team-main # Save shared key from your vault
|
|
134005
134444
|
norn secrets audit . # Fail if plaintext secrets are committed
|
|
134006
134445
|
`);
|
|
134007
134446
|
}
|
|
134447
|
+
function formatRegionPatternSummary(pattern) {
|
|
134448
|
+
const { liftedToStage, liftedToRegion, leafSpecific, skippedConnectionStrings } = pattern.summary;
|
|
134449
|
+
const lines = [
|
|
134450
|
+
`Detected ${pattern.cells.length} envs across ${pattern.stages.length} stages x ${pattern.regions.length} regions.`,
|
|
134451
|
+
`Lifted ${liftedToStage} vars to stage templates (${pattern.stages.join(", ")}).`,
|
|
134452
|
+
`Lifted ${liftedToRegion} vars to region templates (${pattern.regions.join(", ")}).`,
|
|
134453
|
+
`Kept ${leafSpecific} vars leaf-specific.`
|
|
134454
|
+
];
|
|
134455
|
+
if (skippedConnectionStrings > 0) {
|
|
134456
|
+
lines.push(`Kept ${skippedConnectionStrings} connection-string var${skippedConnectionStrings === 1 ? "" : "s"} in leaf envs.`);
|
|
134457
|
+
}
|
|
134458
|
+
return lines;
|
|
134459
|
+
}
|
|
134460
|
+
function runNornenvRegionRefactor(filePath, options) {
|
|
134461
|
+
const content = fs19.readFileSync(filePath, "utf-8");
|
|
134462
|
+
const config2 = parseEnvFile(content, filePath);
|
|
134463
|
+
const pattern = detectRegionPattern(config2, content);
|
|
134464
|
+
if (!pattern) {
|
|
134465
|
+
if (options.output === "json") {
|
|
134466
|
+
console.log(JSON.stringify({ success: false, changed: false, error: "No region pattern detected" }, null, 2));
|
|
134467
|
+
} else {
|
|
134468
|
+
console.error("No region pattern detected in this .nornenv file.");
|
|
134469
|
+
}
|
|
134470
|
+
process.exit(1);
|
|
134471
|
+
}
|
|
134472
|
+
const refactored = applyRegionRefactorToText(content, pattern);
|
|
134473
|
+
if (options.writeRefactor) {
|
|
134474
|
+
fs19.writeFileSync(filePath, refactored, "utf-8");
|
|
134475
|
+
}
|
|
134476
|
+
if (options.output === "json") {
|
|
134477
|
+
console.log(JSON.stringify({
|
|
134478
|
+
success: true,
|
|
134479
|
+
changed: refactored !== content,
|
|
134480
|
+
file: filePath,
|
|
134481
|
+
summary: pattern.summary,
|
|
134482
|
+
stages: pattern.stages,
|
|
134483
|
+
regions: pattern.regions,
|
|
134484
|
+
output: options.writeRefactor ? void 0 : refactored
|
|
134485
|
+
}, null, 2));
|
|
134486
|
+
} else if (options.writeRefactor) {
|
|
134487
|
+
for (const line2 of formatRegionPatternSummary(pattern)) {
|
|
134488
|
+
console.log(line2);
|
|
134489
|
+
}
|
|
134490
|
+
console.log(`Refactored ${filePath}`);
|
|
134491
|
+
} else {
|
|
134492
|
+
console.log(refactored);
|
|
134493
|
+
}
|
|
134494
|
+
process.exit(0);
|
|
134495
|
+
}
|
|
134008
134496
|
async function runSingleRequest(fileContent, variables, cookieJar, apiDefinitions, filePath, envContext) {
|
|
134009
134497
|
const lines = fileContent.split("\n");
|
|
134010
134498
|
const requestLines = [];
|
|
@@ -134169,6 +134657,17 @@ async function main() {
|
|
|
134169
134657
|
process.exit(1);
|
|
134170
134658
|
}
|
|
134171
134659
|
const isDirectory = fs19.statSync(inputPath).isDirectory();
|
|
134660
|
+
if (options.writeRefactor && !options.refactorRegionPattern) {
|
|
134661
|
+
console.error("Error: --write can only be used with --refactor-region-pattern");
|
|
134662
|
+
process.exit(1);
|
|
134663
|
+
}
|
|
134664
|
+
if (options.refactorRegionPattern) {
|
|
134665
|
+
if (isDirectory) {
|
|
134666
|
+
console.error("Error: --refactor-region-pattern requires a specific .nornenv file, not a directory");
|
|
134667
|
+
process.exit(1);
|
|
134668
|
+
}
|
|
134669
|
+
runNornenvRegionRefactor(inputPath, options);
|
|
134670
|
+
}
|
|
134172
134671
|
let filesToRun;
|
|
134173
134672
|
if (isDirectory) {
|
|
134174
134673
|
filesToRun = discoverNornFiles(inputPath);
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "norn-cli",
|
|
3
3
|
"displayName": "Norn — API Tests in Your Repo",
|
|
4
4
|
"description": "Version-controlled API tests your team can keep. Author and debug HTTP sequences in VS Code, then run the same files in CI.",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.5.0",
|
|
6
6
|
"publisher": "Norn-PeterKrustanov",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Peter Krastanov"
|
|
@@ -103,6 +103,11 @@
|
|
|
103
103
|
"title": "Peek Inherited Norn Environment Variables",
|
|
104
104
|
"category": "Norn"
|
|
105
105
|
},
|
|
106
|
+
{
|
|
107
|
+
"command": "norn.nornenv.refactorRegionPattern",
|
|
108
|
+
"title": "Refactor Region Pattern To Templates",
|
|
109
|
+
"category": "Norn"
|
|
110
|
+
},
|
|
106
111
|
{
|
|
107
112
|
"command": "norn.showCoverage",
|
|
108
113
|
"title": "Show API Coverage",
|
|
@@ -410,6 +415,14 @@
|
|
|
410
415
|
"type": "boolean",
|
|
411
416
|
"default": true,
|
|
412
417
|
"description": "Verify SSL/TLS certificates for HTTPS requests and Swagger/OpenAPI fetches. Disable only for local development with self-signed certificates."
|
|
418
|
+
},
|
|
419
|
+
"norn.testExplorer.exclude": {
|
|
420
|
+
"type": "array",
|
|
421
|
+
"default": [],
|
|
422
|
+
"items": {
|
|
423
|
+
"type": "string"
|
|
424
|
+
},
|
|
425
|
+
"description": "Workspace-relative glob patterns for .norn files or folders to hide from the Norn Test Explorer. Useful for negative fixtures, live-only tests, or documentation demos."
|
|
413
426
|
}
|
|
414
427
|
}
|
|
415
428
|
},
|
|
@@ -440,7 +453,7 @@
|
|
|
440
453
|
"watch:esbuild": "node esbuild.js --watch",
|
|
441
454
|
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
|
442
455
|
"package": "npm run check-types && npm run lint && node esbuild.js --production",
|
|
443
|
-
"compile-tests": "tsc -p . --outDir out",
|
|
456
|
+
"compile-tests": "node -e \"require('fs').rmSync('out/test',{recursive:true,force:true})\" && tsc -p . --outDir out",
|
|
444
457
|
"watch-tests": "tsc -p . -w --outDir out",
|
|
445
458
|
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
|
446
459
|
"check-types": "tsc --noEmit",
|
|
@@ -448,7 +461,8 @@
|
|
|
448
461
|
"validate:skills": "node ./scripts/validate-skills.mjs",
|
|
449
462
|
"test": "vscode-test",
|
|
450
463
|
"test:regression": "node ./dist/cli.js ./tests/Regression/ -e prelive",
|
|
451
|
-
"
|
|
464
|
+
"test:prerelease": "npm test && npm run test:regression",
|
|
465
|
+
"publish:npm": "node -e \"const fs=require('fs');const p=require('./package.json');p.name='norn-cli';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n')\" && npm publish; exit_code=$?; node -e \"const fs=require('fs');const p=require('./package.json');p.name='norn';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n')\"; exit $exit_code",
|
|
452
466
|
"publish:vsce": "node -e \"const p=require('./package.json');p.name='norn';require('fs').writeFileSync('package.json',JSON.stringify(p,null,2))\" && npx vsce publish",
|
|
453
467
|
"publish:all": "npm run publish:npm && npm run publish:vsce"
|
|
454
468
|
},
|
|
@@ -480,4 +494,4 @@
|
|
|
480
494
|
"bin": {
|
|
481
495
|
"norn": "./dist/cli.js"
|
|
482
496
|
}
|
|
483
|
-
}
|
|
497
|
+
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm run *)",
|
|
5
|
-
"WebSearch",
|
|
6
|
-
"Bash(git checkout *)",
|
|
7
|
-
"Bash(node ./dist/cli.js tests/Regression/nornenv-templates/extends-resolution.norn -s PrintResolvedValues -e prod_uk)",
|
|
8
|
-
"Bash(node ./dist/cli.js tests/Regression/nornenv-templates/extends-resolution.norn -s PrintResolvedValues -e prod_us)",
|
|
9
|
-
"Bash(node ./dist/cli.js tests/Regression/nornenv-templates/extends-resolution.norn -s PrintResolvedValues -e diamond)",
|
|
10
|
-
"Bash(node ./dist/cli.js tests/Regression/nornenv-templates/extends-resolution.norn -s PrintResolvedValues -e base)",
|
|
11
|
-
"Bash(mv .nornenv .nornenv.bak)",
|
|
12
|
-
"Bash(cp cycle.nornenv .nornenv)",
|
|
13
|
-
"Bash(timeout 10 node ../../../dist/cli.js extends-resolution.norn -s PrintResolvedValues -e c)",
|
|
14
|
-
"Bash(time node ../../../dist/cli.js extends-resolution.norn -s PrintResolvedValues -e c)",
|
|
15
|
-
"Bash(time node /Users/petercrest/Worktable/Projects/vsApi/dist/cli.js extends-resolution.norn -s PrintResolvedValues -e c)"
|
|
16
|
-
]
|
|
17
|
-
}
|
|
18
|
-
}
|