norn-cli 2.2.2 → 2.3.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/.claude/settings.local.json +18 -0
- package/CHANGELOG.md +16 -1
- package/LICENSE +20 -29
- package/README.md +32 -1
- package/demos/nornenv-showcase/README.md +62 -0
- package/demos/nornenv-showcase/norn.config.json +16 -0
- package/demos/nornenv-showcase/showcase.norn +70 -0
- package/demos/nornenv-showcase/showcase.nornapi +26 -0
- package/demos/nornenv-showcase/showcase.nornsql +20 -0
- package/dist/cli.js +204 -53
- package/package.json +33 -11
- package/.kanbn/index.md +0 -31
- package/.kanbn/tasks/book-first-mentor-session.md +0 -13
- package/.kanbn/tasks/decide-what-success-in-a-pilot-looks-like.md +0 -9
- package/.kanbn/tasks/do-5-customer-conversations.md +0 -9
- package/.kanbn/tasks/finalise-the-one-line-pitch.md +0 -11
- package/.kanbn/tasks/interview-script.md +0 -49
- package/.kanbn/tasks/make-a-list-of-10-people-to-speak-to.md +0 -11
- package/.kanbn/tasks/prepare-your-customer-interview-questions.md +0 -11
- package/.kanbn/tasks/recruit-2/342/200/2233-pilot-users.md +0 -9
- package/.kanbn/tasks/refine-your-pitch.md +0 -9
- package/.kanbn/tasks/use-the-shiplight-website-as-a-template-to-improve-norn-website.md +0 -9
- package/.kanbn/tasks/write-down-repeated-wording.md +0 -9
- package/.kanbn/tasks/write-the-one-pager.md +0 -27
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the "Norn" extension will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [2.3.0] - 2026-05-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Inlay hints + hover across every Norn file type**:
|
|
9
|
+
- `.norn`: every `{{name}}` / `{{$env.name}}` token resolves under the active env, with three-scope precedence — sequence-local `var` declarations ← file-level `var` declarations ← active env's effective vars. Runtime values (`$1.body.x`, `run X()`, request captures) are intentionally not rendered inline; hover narrates them with source line.
|
|
10
|
+
- `.nornapi`: every `{{...}}` token in headers groups and endpoint definitions resolves under the active env.
|
|
11
|
+
- `.nornsql`: the `connection NAME` line shows the resolved `NAME_connectionString` from `.nornenv` (masked if a `secret connectionString`). Hover narrates the source. Any `{{...}}` in SQL bodies is handled for parity.
|
|
12
|
+
- Resolution and reference parsing live in `src/inlayHintResolver.ts` so every provider behaves identically.
|
|
13
|
+
- **`.nornenv` Templates And Extends (Extension + CLI)**:
|
|
14
|
+
- Added `[template:NAME]` sections and multi-parent template `extends` on `[env:...]` / `[template:...]` headers.
|
|
15
|
+
- Resolved env values as `common <- template1 <- template2 <- self`; templates compose into envs but are not selectable in the status bar or CLI `-e`.
|
|
16
|
+
- Added `.nornenv` Activate/Deactivate and Peek inherited CodeLens actions, folding ranges, smart header decorations, hover resolution chains, inlay hints for `{{...}}`, and inherited-variable completions.
|
|
17
|
+
- Added diagnostics for unknown extends parents, invalid env parents, extends cycles, out-of-scope cross-section references, and typo-like override names.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Endpoint path arguments now keep bare `.nornenv` names as literal path values unless the reference is explicit (`{{name}}` / `{{$env.name}}`), while declared file/sequence variables still resolve in endpoint calls.
|
|
6
21
|
|
|
7
22
|
## [2.2.0] - 2026-05-09
|
|
8
23
|
|
package/LICENSE
CHANGED
|
@@ -8,13 +8,17 @@ If you do not wish to be bound by the terms of this Agreement, you may not downl
|
|
|
8
8
|
|
|
9
9
|
• DEFINITIONS
|
|
10
10
|
|
|
11
|
-
"
|
|
11
|
+
"Extension" means the Norn extension for Visual Studio Code.
|
|
12
|
+
|
|
13
|
+
"CLI" means the Norn command-line interface.
|
|
12
14
|
|
|
13
|
-
"
|
|
15
|
+
"Pipeline Use" means execution of the CLI within an automated build, test, integration, delivery or deployment pipeline, including but not limited to continuous integration and continuous delivery (CI/CD) systems, whether hosted or self-hosted, and whether or not triggered by a human.
|
|
14
16
|
|
|
15
|
-
"
|
|
17
|
+
"Local Use" means execution of the CLI directly by an individual on a workstation or development machine, outside of any automated pipeline.
|
|
18
|
+
|
|
19
|
+
"Authorized User" means any employee, independent contractor or other temporary worker authorized by Licensee to use the Software while performing duties within the scope of their employment or assignment.
|
|
16
20
|
|
|
17
|
-
"
|
|
21
|
+
"License Key" means a unique key-code that authorizes Pipeline Use of the CLI. Only Licensor and/or its representatives are permitted to produce License Keys for the Software.
|
|
18
22
|
|
|
19
23
|
• OWNERSHIP
|
|
20
24
|
|
|
@@ -26,35 +30,22 @@ Upon your acceptance of this Agreement, Licensor grants you a limited, non-trans
|
|
|
26
30
|
|
|
27
31
|
• INTENDED USERS OF THE SOFTWARE
|
|
28
32
|
|
|
29
|
-
The
|
|
30
|
-
|
|
31
|
-
• FREE USE (PERSONAL USE)
|
|
32
|
-
|
|
33
|
-
You may install and use the Software free of charge for Personal Use, including:
|
|
34
|
-
- Personal projects and experimentation
|
|
35
|
-
- Learning and educational purposes
|
|
36
|
-
- Open-source projects that do not generate revenue
|
|
37
|
-
- Academic research
|
|
38
|
-
|
|
39
|
-
• COMMERCIAL EVALUATION
|
|
33
|
+
The Extension is free to install and use for everyone, including individuals, businesses and organisations, for any purpose including commercial purposes, and never requires a License Key. The CLI is free for Local Use. A License Key is required only for Pipeline Use of the CLI. No License Key is required to use the Extension or to use the CLI for Local Use.
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
- Limited to one (1) evaluation period per organisation
|
|
43
|
-
- Intended for genuine evaluation purposes only
|
|
44
|
-
- Not to be used for production workloads or client projects
|
|
45
|
-
- Not extendable or renewable without written permission from the Licensor
|
|
35
|
+
• FREE USE
|
|
46
36
|
|
|
47
|
-
|
|
37
|
+
You may install and use the following free of charge, without a License Key:
|
|
38
|
+
- The Extension, for any use, including commercial use within a for-profit company or organisation
|
|
39
|
+
- The CLI for Local Use, including personal projects and experimentation, learning and education, academic research, and open-source projects
|
|
48
40
|
|
|
49
|
-
•
|
|
41
|
+
• LICENSED USE (PIPELINE USE OF THE CLI)
|
|
50
42
|
|
|
51
|
-
A
|
|
52
|
-
-
|
|
53
|
-
- Use
|
|
54
|
-
- Use in CI/CD
|
|
55
|
-
- Use to develop, test, or maintain revenue-generating software or services
|
|
43
|
+
A valid License Key is required for any Pipeline Use of the CLI. For the avoidance of doubt:
|
|
44
|
+
- The Extension never requires a License Key, including when used commercially within a for-profit company or organisation
|
|
45
|
+
- Local Use of the CLI never requires a License Key
|
|
46
|
+
- Only Pipeline Use of the CLI (for example, running the CLI in a CI/CD build, test, or deployment pipeline) requires a License Key
|
|
56
47
|
|
|
57
|
-
Please contact the Licensor for
|
|
48
|
+
Please contact the Licensor for licensing options.
|
|
58
49
|
|
|
59
50
|
• RESTRICTIONS ON USE OF THE SOFTWARE
|
|
60
51
|
|
|
@@ -64,7 +55,7 @@ b) Decompile, disassemble or reverse engineer the Software or attempt to do any
|
|
|
64
55
|
c) Reproduce, copy, distribute, resell or otherwise use the whole or any part of the Software's code for any commercial purpose.
|
|
65
56
|
d) Disable, modify or hide notifications sent by the Software.
|
|
66
57
|
e) Distribute, resell, or share License Keys.
|
|
67
|
-
f) Misrepresent
|
|
58
|
+
f) Misrepresent Pipeline Use of the CLI as Local Use, or otherwise circumvent the License Key requirement for Pipeline Use.
|
|
68
59
|
g) Use older versions of the Software to circumvent licensing requirements.
|
|
69
60
|
|
|
70
61
|
• LIMITED WARRANTY AND DISCLAIMER OF WARRANTY
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Norn
|
|
2
2
|
|
|
3
|
-
Norn
|
|
3
|
+
Norn turns API requests into version-controlled tests your whole team can keep — authored and debugged in VS Code, run on every PR in CI. Write a request once, turn it into a tested sequence, debug it with breakpoints in the editor, and run the exact same `.norn` files from the CLI in your pipeline.
|
|
4
4
|
|
|
5
5
|
### Simple API Requests
|
|
6
6
|
|
|
@@ -72,6 +72,37 @@ npm install -g norn-cli
|
|
|
72
72
|
norn ./tests/smoke.norn -e dev
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
## `.nornenv` Templates And Extends
|
|
76
|
+
|
|
77
|
+
Use `[template:name]` sections for reusable environment building blocks, then compose selectable `[env:name]` sections with `extends`. Only templates can be extended. Templates are not selectable from the status bar or CLI; only `[env:...]` names can be used with `-e`.
|
|
78
|
+
|
|
79
|
+
```nornenv
|
|
80
|
+
var timeout = 30000
|
|
81
|
+
|
|
82
|
+
[template:prod]
|
|
83
|
+
var baseUrl = https://api.example.com
|
|
84
|
+
secret apiKey = prod-key-789
|
|
85
|
+
|
|
86
|
+
[template:uk]
|
|
87
|
+
var dbHost = db.uk.example.com
|
|
88
|
+
var bucket = data-uk
|
|
89
|
+
|
|
90
|
+
[env:prod_uk extends prod, uk]
|
|
91
|
+
var failoverHost = api-failover.uk.example.com
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Resolution order is `common <- template1 <- template2 <- self`, so later templates win on collisions and the env section itself wins over everything. The VS Code editor also shows Activate CodeLens actions, inherited-variable peeks, hover resolution chains, and inlay hints for `{{name}}` / `{{$env.name}}` references.
|
|
95
|
+
|
|
96
|
+
## Inlay Hints Across Every File Type
|
|
97
|
+
|
|
98
|
+
Every `{{...}}` reference in any Norn file shows its resolved value as a gray inline hint under the active env, with the same masking-for-secrets rules as `.nornenv`. The three scopes available in `.norn` resolve in precedence order:
|
|
99
|
+
|
|
100
|
+
1. **Sequence-local** `var` declarations inside the current `test sequence ... end sequence` block
|
|
101
|
+
2. **File-level** `var` declarations at the top of the file (above any sequence)
|
|
102
|
+
3. **Active env** effective vars (`common` ← ancestor templates ← self)
|
|
103
|
+
|
|
104
|
+
`{{$env.name}}` always reads from scope 3, skipping local/file vars. Runtime values (`$1.body.id`, request captures, `run X()` returns) render no hint inline; hover narrates them with source line. `.nornapi` and `.nornsql` use env scope only; `.nornsql` additionally shows the resolved connection string after `connection NAME`.
|
|
105
|
+
|
|
75
106
|
## Deterministic MCP Tools
|
|
76
107
|
|
|
77
108
|
Norn can call MCP tools from sequences without leaving the `.norn` runtime. MCP sessions are deterministic and shared across the full sequence run, so nested sequences reuse the same connection for the same resolved server alias.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# `.nornenv` Feature Showcase
|
|
2
|
+
|
|
3
|
+
A single `.nornenv` that touches every feature we've shipped — templates, multi-parent `extends`, diamond inheritance, secrets, connection-string templates, imports, plus every diagnostic. Open [.nornenv](./.nornenv) in VS Code and the editor experience does most of the talking.
|
|
4
|
+
|
|
5
|
+
## What to try, in order
|
|
6
|
+
|
|
7
|
+
1. **Open the file.** Non-active sections auto-collapse to one line; the smart header chips show `extends X, Y · N vars · K inherited · M overrides` plus any warning pills.
|
|
8
|
+
2. **Click `▶ Activate` on `[env:prod_eu]`.** It's the most feature-dense env. Watch the file change:
|
|
9
|
+
- The `ACTIVE` pill appears on its header
|
|
10
|
+
- A brand-gradient marker shows up in the gutter on its line
|
|
11
|
+
- A teal tick appears on the right-side overview ruler
|
|
12
|
+
- Every `{{...}}` reference in the file gets an inlay hint with the now-resolved value
|
|
13
|
+
- Secrets render as `(secret)` rather than the raw value
|
|
14
|
+
3. **Hover anything** — a variable name on the left side of a `=`, or a `{{ref}}`. The hover shows the resolution chain: where it's defined, whether it was inherited, and the current resolved value.
|
|
15
|
+
4. **Try `▶ Peek inherited`** on a leaf env like `[env:prod_eu]` — a quick-pick lists all 8+ inherited variables with their source templates and resolved values.
|
|
16
|
+
5. **Type `var ` inside `[env:prod_eu]`** — IntelliSense suggests every inherited variable name, so you can pick one to override without retyping.
|
|
17
|
+
6. **Start a new section header `[`** — completion offers both `[env:` and `[template:`. After typing `[env:foo `, completion offers `extends`, then suggests every available template as a parent.
|
|
18
|
+
7. **Scroll to the bottom** — the diagnostics-only block deliberately triggers every linter check:
|
|
19
|
+
- `[env:bad_unknown_parent extends prdo, us]` — `prdo` squiggles as `unknown-extends-parent` with a quick-fix suggesting `prod`
|
|
20
|
+
- `var apiKy = oops-typo` — squiggles as `unknown-override-target` with a suggestion
|
|
21
|
+
- `var dangling = {{kmsKey}}` — squiggles as `unresolved-cross-section-ref`
|
|
22
|
+
- `[env:bad_env_parent extends prod_us]` — `prod_us` squiggles as `extends-env-parent`
|
|
23
|
+
- `[template:loop_a]` / `[template:loop_b]` — both squiggle as `extends-cycle`
|
|
24
|
+
|
|
25
|
+
## What to try at the CLI
|
|
26
|
+
|
|
27
|
+
After `npm run compile`, run [showcase.norn](./showcase.norn) which prints every resolved value.
|
|
28
|
+
|
|
29
|
+
> **Heads-up about `{{…}}` inside `.nornenv` values:** the IDE's inlay hints show the **fully recursively resolved** form (e.g. `apiBase → "https://api.prod.example.com/v2"` under `prod_eu`). The `.norn` runtime substitution is single-level — when you print `{{apiBase}}` from a `.norn` file, you'll see the literal `https://api.{{stageHost}}.example.com/{{apiVersion}}` because `apiBase`'s *value* still contains template tokens. That's standard Norn behavior — switch to the IDE for the deep view, or use leaf vars (stageHost, region, dbHost, …) for clean CLI prints.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Most feature-dense env (self-override + diamond touchpoints).
|
|
33
|
+
node ./dist/cli.js demos/nornenv-showcase/showcase.norn -e prod_eu
|
|
34
|
+
|
|
35
|
+
# Right-most-wins demo — prod_strict (rightmost prod-chain template) wins on collisions.
|
|
36
|
+
node ./dist/cli.js demos/nornenv-showcase/showcase.norn -e prod_eu_strict
|
|
37
|
+
|
|
38
|
+
# Diamond inheritance — shared ancestor merged once.
|
|
39
|
+
node ./dist/cli.js demos/nornenv-showcase/showcase.norn -e diamond
|
|
40
|
+
|
|
41
|
+
# Templates are not selectable — this should error and list only env names.
|
|
42
|
+
node ./dist/cli.js demos/nornenv-showcase/showcase.norn -e prod
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What each section demonstrates
|
|
46
|
+
|
|
47
|
+
| Section | Feature |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `import "./shared/.nornenv"` | Multi-file composition; chained imports merge into common |
|
|
50
|
+
| top-of-file `var ...` | Common variables (shared by every env) |
|
|
51
|
+
| `var apiBase = https://api.{{stageHost}}…` | Common values may reference any name — resolves under the active env |
|
|
52
|
+
| `[template:dev/staging/prod]` | Stage building blocks (not selectable) |
|
|
53
|
+
| `[template:prod_strict extends prod]` | Template extending another template (chained) |
|
|
54
|
+
| `[template:us/eu]` | Region building blocks |
|
|
55
|
+
| `[template:db_main]` with `connectionString` + `secret connectionString` | Connection-string templates, plain and secret |
|
|
56
|
+
| `[env:dev_us extends dev, us, db_main]` | Composing 3 templates into one selectable env |
|
|
57
|
+
| `[env:prod_eu] ... var logLevel = error` | Self-override wins over every parent |
|
|
58
|
+
| `[env:prod_eu_strict extends prod_strict, eu, db_main]` | Right-most parent wins on collisions |
|
|
59
|
+
| `[env:diamond extends dev, prod_strict]` | Diamond: shared ancestor merged once |
|
|
60
|
+
| `[env:bad_unknown_parent ...]` | Unknown parent + typo override + dangling ref diagnostics |
|
|
61
|
+
| `[env:bad_env_parent extends prod_us]` | Env-as-parent diagnostic |
|
|
62
|
+
| `[template:loop_a/loop_b]` | Cycle diagnostic |
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"sql": {
|
|
4
|
+
"connections": {
|
|
5
|
+
"main": {
|
|
6
|
+
"adapter": "showcase-fake-adapter",
|
|
7
|
+
"profile": "main"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"adapters": {
|
|
11
|
+
"showcase-fake-adapter": {
|
|
12
|
+
"command": ["node", "-e", "process.stderr.write('showcase: SQL adapter not wired; this file is a visual demo only.\\n'); process.exit(1);"]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Showcase .norn — exercises the inlay hints + hover from src/nornInlayHintsProvider
|
|
2
|
+
# and src/nornHoverProvider across all three scopes:
|
|
3
|
+
# 1. Sequence-local `var` declarations inside a `test sequence ... end sequence` block
|
|
4
|
+
# 2. File-level `var` declarations (above any sequence)
|
|
5
|
+
# 3. Active .nornenv environment (common + ancestor templates + self)
|
|
6
|
+
#
|
|
7
|
+
# Open this file with [env:prod_eu] active to watch every {{...}} reference
|
|
8
|
+
# resolve inline; hover any reference for the source + value resolution chain.
|
|
9
|
+
#
|
|
10
|
+
# CLI verification:
|
|
11
|
+
# node ./dist/cli.js demos/nornenv-showcase/showcase.norn -s ShowcaseResolvedValues -e prod_eu
|
|
12
|
+
# node ./dist/cli.js demos/nornenv-showcase/showcase.norn -s ShowcaseResolvedValues -e prod_eu_strict
|
|
13
|
+
# node ./dist/cli.js demos/nornenv-showcase/showcase.norn -s ShowcaseResolvedValues -e diamond
|
|
14
|
+
|
|
15
|
+
# ─── File-level vars (scope 2) ──────────────────────────────────────────────
|
|
16
|
+
# These reference env vars defined in showcase.nornenv ({{region}}, {{apiVersion}}).
|
|
17
|
+
# Inlay hints should resolve them recursively under the active env.
|
|
18
|
+
var siteTag = "showcase-{{region}}"
|
|
19
|
+
var docsHomepage = "{{apiBase}}/help"
|
|
20
|
+
|
|
21
|
+
# ─── Sequence 1: print every resolved env-level value ───────────────────────
|
|
22
|
+
test sequence ShowcaseResolvedValues
|
|
23
|
+
print "apiBase" | "{{apiBase}}"
|
|
24
|
+
print "dataBucket" | "{{dataBucket}}"
|
|
25
|
+
print "docsUrl" | "{{docsUrl}}"
|
|
26
|
+
print "stageHost" | "{{stageHost}}"
|
|
27
|
+
print "region" | "{{region}}"
|
|
28
|
+
print "dbHost" | "{{dbHost}}"
|
|
29
|
+
print "logLevel" | "{{logLevel}}"
|
|
30
|
+
print "retries" | "{{retries}}"
|
|
31
|
+
print "orgName" | "{{orgName}}"
|
|
32
|
+
print "main_connectionString" | "{{main_connectionString}}"
|
|
33
|
+
print "reports_connectionString" | "{{reports_connectionString}}"
|
|
34
|
+
print "apiKey" | "{{apiKey}}"
|
|
35
|
+
end sequence
|
|
36
|
+
|
|
37
|
+
# ─── Sequence 2: demonstrate all three scopes in one place ──────────────────
|
|
38
|
+
test sequence ShowcaseScopeLayers
|
|
39
|
+
# File-level var (scope 2) referenced from inside a sequence
|
|
40
|
+
print "siteTag" | "{{siteTag}}"
|
|
41
|
+
print "docsHomepage" | "{{docsHomepage}}"
|
|
42
|
+
|
|
43
|
+
# Sequence-local var (scope 1) — static value
|
|
44
|
+
var greeting = "Hello from {{stageHost}}"
|
|
45
|
+
print "greeting" | "{{greeting}}"
|
|
46
|
+
|
|
47
|
+
# Sequence-local var (scope 1) — composes env + region
|
|
48
|
+
var fullUserUrl = "{{apiBase}}/{{region}}/users"
|
|
49
|
+
print "fullUserUrl" | "{{fullUserUrl}}"
|
|
50
|
+
|
|
51
|
+
# Sequence-local var (scope 1) — overrides a file-level name within this sequence
|
|
52
|
+
var siteTag = "scoped-override-{{stageHost}}"
|
|
53
|
+
print "siteTag (local override)" | "{{siteTag}}"
|
|
54
|
+
|
|
55
|
+
# Explicit env-only reference — bypasses local/file scope, reads directly from env
|
|
56
|
+
print "env-only region" | "{{$env.region}}"
|
|
57
|
+
end sequence
|
|
58
|
+
|
|
59
|
+
# ─── Sequence 3: runtime vars are intentionally NOT shown by inlay hints ────
|
|
60
|
+
test sequence ShowcaseRuntimeRefs
|
|
61
|
+
# Sequence-local but RUNTIME (response capture) — no inlay hint expected on {{traceId}}.
|
|
62
|
+
# Hover on the reference will narrate "runtime expression" with source line.
|
|
63
|
+
GET https://httpbin.org/uuid
|
|
64
|
+
assert $1.status == 200
|
|
65
|
+
var traceId = $1.body.uuid
|
|
66
|
+
print "traceId" | "{{traceId}}"
|
|
67
|
+
|
|
68
|
+
# Response refs like {{$1.body.uuid}} are also runtime-only — no inlay hint.
|
|
69
|
+
print "uuid" | "{{$1.body.uuid}}"
|
|
70
|
+
end sequence
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Showcase .nornapi — every {{...}} on this file resolves under the active env via
|
|
2
|
+
# the inlay hints from src/nornapiInlayHintsProvider.ts. Hover any reference for
|
|
3
|
+
# the resolution chain.
|
|
4
|
+
#
|
|
5
|
+
# Try: activate [env:prod_eu] from showcase.nornenv and watch the URLs/headers
|
|
6
|
+
# below resolve to prod_eu's effective values.
|
|
7
|
+
|
|
8
|
+
headers Standard
|
|
9
|
+
Content-Type: application/json
|
|
10
|
+
Accept: application/json
|
|
11
|
+
end headers
|
|
12
|
+
|
|
13
|
+
headers AuthHeaders
|
|
14
|
+
Authorization: Bearer {{apiKey}}
|
|
15
|
+
X-Region: {{region}}
|
|
16
|
+
X-Stage: {{stageHost}}
|
|
17
|
+
X-Api-Version: {{apiVersion}}
|
|
18
|
+
end headers
|
|
19
|
+
|
|
20
|
+
endpoints
|
|
21
|
+
HealthCheck: GET {{apiBase}}/health
|
|
22
|
+
ListCustomers: GET {{apiBase}}/customers?region={{region}}
|
|
23
|
+
GetCustomer: GET {{apiBase}}/customers/{customerId}
|
|
24
|
+
CreateOrder: POST {{apiBase}}/orders
|
|
25
|
+
Failover: GET {{failoverHost}}/status
|
|
26
|
+
end endpoints
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Showcase .nornsql — the `connection main` line below is resolved by
|
|
2
|
+
# src/nornsqlInlayHintsProvider.ts to `main_connectionString` from the
|
|
3
|
+
# nearest .nornenv. Hover the identifier `main` to see where it's defined
|
|
4
|
+
# in [template:db_main] and what it resolves to under the active env.
|
|
5
|
+
#
|
|
6
|
+
# Try: activate [env:prod_eu] and watch the inlay after `main` show the
|
|
7
|
+
# resolved Server=tcp:db.eu.example.com,1433;... connection string.
|
|
8
|
+
|
|
9
|
+
connection main
|
|
10
|
+
|
|
11
|
+
query ListBuyers(region)
|
|
12
|
+
select Id, Company, Email
|
|
13
|
+
from Buyers
|
|
14
|
+
where Region = :region
|
|
15
|
+
end query
|
|
16
|
+
|
|
17
|
+
command RecordAuditEntry(actor, action)
|
|
18
|
+
insert into Audit (Actor, Action, At)
|
|
19
|
+
values (:actor, :action, current_timestamp)
|
|
20
|
+
end command
|
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 (
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
|
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
|
|
132715
|
-
if (
|
|
132716
|
-
|
|
132717
|
-
|
|
132718
|
-
|
|
132719
|
-
|
|
132720
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
133228
|
+
const envMatch = trimmed.match(envRegex);
|
|
133078
133229
|
if (envMatch) {
|
|
133079
133230
|
currentEnv = envMatch[1];
|
|
133080
133231
|
continue;
|
|
@@ -133653,7 +133804,7 @@ function resolveEnvironmentForPath(targetPath, selectedEnv) {
|
|
|
133653
133804
|
console.error(`Fix .nornenv import errors before running tests.`);
|
|
133654
133805
|
process.exit(1);
|
|
133655
133806
|
}
|
|
133656
|
-
|
|
133807
|
+
let variables = { ...envConfig.common };
|
|
133657
133808
|
const secretNames = new Set(envConfig.secretNames);
|
|
133658
133809
|
const secretValues = new Map(envConfig.secretValues);
|
|
133659
133810
|
const availableEnvironments = envConfig.environments.map((e) => e.name);
|
|
@@ -133672,9 +133823,9 @@ function resolveEnvironmentForPath(targetPath, selectedEnv) {
|
|
|
133672
133823
|
};
|
|
133673
133824
|
}
|
|
133674
133825
|
targetEnvVariables = targetEnv.variables;
|
|
133675
|
-
|
|
133676
|
-
for (const [name, value] of Object.entries(
|
|
133677
|
-
if (secretNames.has(name)) {
|
|
133826
|
+
variables = resolveEffectiveEnvVariables(selectedEnv, envConfig);
|
|
133827
|
+
for (const [name, value] of Object.entries(variables)) {
|
|
133828
|
+
if (secretNames.has(name) && !secretValues.has(name)) {
|
|
133678
133829
|
secretValues.set(name, value);
|
|
133679
133830
|
}
|
|
133680
133831
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "norn-cli",
|
|
3
|
-
"displayName": "Norn
|
|
4
|
-
"description": "
|
|
5
|
-
"version": "2.
|
|
3
|
+
"displayName": "Norn — API Tests in Your Repo",
|
|
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.3.0",
|
|
6
6
|
"publisher": "Norn-PeterKrustanov",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Peter Krastanov"
|
|
@@ -22,21 +22,22 @@
|
|
|
22
22
|
"theme": "dark"
|
|
23
23
|
},
|
|
24
24
|
"keywords": [
|
|
25
|
-
"
|
|
26
|
-
"
|
|
25
|
+
"api testing",
|
|
26
|
+
"integration testing",
|
|
27
|
+
"ci",
|
|
28
|
+
"regression testing",
|
|
29
|
+
"api automation",
|
|
30
|
+
"openapi",
|
|
27
31
|
"http",
|
|
28
|
-
"
|
|
29
|
-
"client",
|
|
30
|
-
"rest client",
|
|
31
|
-
"api testing"
|
|
32
|
+
"test automation"
|
|
32
33
|
],
|
|
33
34
|
"engines": {
|
|
34
35
|
"vscode": "^1.108.1"
|
|
35
36
|
},
|
|
36
37
|
"categories": [
|
|
38
|
+
"Testing",
|
|
37
39
|
"Programming Languages",
|
|
38
|
-
"Snippets"
|
|
39
|
-
"Other"
|
|
40
|
+
"Snippets"
|
|
40
41
|
],
|
|
41
42
|
"activationEvents": [
|
|
42
43
|
"onLanguage:norn",
|
|
@@ -87,6 +88,21 @@
|
|
|
87
88
|
"title": "Select Environment",
|
|
88
89
|
"category": "Norn"
|
|
89
90
|
},
|
|
91
|
+
{
|
|
92
|
+
"command": "norn.nornenv.activate",
|
|
93
|
+
"title": "Activate Norn Environment",
|
|
94
|
+
"category": "Norn"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"command": "norn.nornenv.deactivate",
|
|
98
|
+
"title": "Deactivate Norn Environment",
|
|
99
|
+
"category": "Norn"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"command": "norn.nornenv.peekInherited",
|
|
103
|
+
"title": "Peek Inherited Norn Environment Variables",
|
|
104
|
+
"category": "Norn"
|
|
105
|
+
},
|
|
90
106
|
{
|
|
91
107
|
"command": "norn.showCoverage",
|
|
92
108
|
"title": "Show API Coverage",
|
|
@@ -344,6 +360,12 @@
|
|
|
344
360
|
"key": "ctr+alt+r",
|
|
345
361
|
"mac": "ctr+alt+r",
|
|
346
362
|
"when": "editorLangId == norn"
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"command": "norn.nornenv.enter",
|
|
366
|
+
"key": "enter",
|
|
367
|
+
"mac": "enter",
|
|
368
|
+
"when": "editorTextFocus && editorLangId == nornenv && !suggestWidgetVisible"
|
|
347
369
|
}
|
|
348
370
|
],
|
|
349
371
|
"chatParticipants": [
|
package/.kanbn/index.md
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
startedColumns:
|
|
3
|
-
- 'In Progress'
|
|
4
|
-
completedColumns:
|
|
5
|
-
- Done
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Norn
|
|
9
|
-
|
|
10
|
-
## Backlog
|
|
11
|
-
|
|
12
|
-
- [do-5-customer-conversations](tasks/do-5-customer-conversations.md)
|
|
13
|
-
- [write-down-repeated-wording](tasks/write-down-repeated-wording.md)
|
|
14
|
-
- [refine-your-pitch](tasks/refine-your-pitch.md)
|
|
15
|
-
- [recruit-2–3-pilot-users](tasks/recruit-2–3-pilot-users.md)
|
|
16
|
-
- [decide-what-success-in-a-pilot-looks-like](tasks/decide-what-success-in-a-pilot-looks-like.md)
|
|
17
|
-
- [interview-script](tasks/interview-script.md)
|
|
18
|
-
|
|
19
|
-
## Todo
|
|
20
|
-
|
|
21
|
-
- [finalise-the-one-line-pitch](tasks/finalise-the-one-line-pitch.md)
|
|
22
|
-
- [make-a-list-of-10-people-to-speak-to](tasks/make-a-list-of-10-people-to-speak-to.md)
|
|
23
|
-
- [prepare-your-customer-interview-questions](tasks/prepare-your-customer-interview-questions.md)
|
|
24
|
-
- [use-the-shiplight-website-as-a-template-to-improve-norn-website](tasks/use-the-shiplight-website-as-a-template-to-improve-norn-website.md)
|
|
25
|
-
|
|
26
|
-
## In Progress
|
|
27
|
-
|
|
28
|
-
## Done
|
|
29
|
-
|
|
30
|
-
- [book-first-mentor-session](tasks/book-first-mentor-session.md)
|
|
31
|
-
- [write-the-one-pager](tasks/write-the-one-pager.md)
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
created: 2026-04-09T22:55:34.350Z
|
|
3
|
-
updated: 2026-04-12T12:19:50.345Z
|
|
4
|
-
assigned: ""
|
|
5
|
-
progress: 0
|
|
6
|
-
tags:
|
|
7
|
-
- 'Week One'
|
|
8
|
-
due: 2026-04-12T00:00:00.000Z
|
|
9
|
-
started: 2026-04-12T09:08:33.269Z
|
|
10
|
-
completed: 2026-04-12T12:19:50.345Z
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
# Book first mentor session
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
created: 2026-04-09T23:25:42.568Z
|
|
3
|
-
updated: 2026-04-09T23:25:42.565Z
|
|
4
|
-
assigned: ""
|
|
5
|
-
progress: 0
|
|
6
|
-
tags: []
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Interview Script
|
|
10
|
-
|
|
11
|
-
This is the important bit. You are not trying to impress them. You are trying to learn where the pain is.
|
|
12
|
-
Opening
|
|
13
|
-
“Thanks for taking the time. I’m working on a VS Code-native API testing and automation tool called Norn. I’m speaking to people to understand how teams currently handle API testing, bug reproduction, and automation. I’m not here to hard-sell you anything — I mainly want to learn how you do things today and where the pain is.”
|
|
14
|
-
Warm-up
|
|
15
|
-
“Can you tell me a bit about your role and how involved you are with API testing or debugging?”
|
|
16
|
-
Current workflow
|
|
17
|
-
“How do you currently test APIs day to day?”
|
|
18
|
-
“What tools do you use for exploratory API work?”
|
|
19
|
-
“What tools do you use for repeatable automated API tests?”
|
|
20
|
-
“Are those the same tools, or different?”
|
|
21
|
-
“Who usually owns API regression coverage in your team?”
|
|
22
|
-
“When there’s a bug, how do developers usually reproduce it?”
|
|
23
|
-
Pain discovery
|
|
24
|
-
“What’s the most annoying part of your current setup?”
|
|
25
|
-
“Where does the process break down?”
|
|
26
|
-
“Do developers and QA use the same tools or mostly different ones?”
|
|
27
|
-
“Have you ever had useful API tests or requests trapped in one place where other people don’t really use them?”
|
|
28
|
-
“What becomes hard to maintain as the project grows?”
|
|
29
|
-
“If you could magically remove one frustration from your API workflow, what would it be?”
|
|
30
|
-
Team and buying context
|
|
31
|
-
“If a new tool genuinely improved this, who would care most?”
|
|
32
|
-
“Who would likely use it first?”
|
|
33
|
-
“Who would need to approve it?”
|
|
34
|
-
“Would this be more of a team decision, an engineering decision, or something individuals would adopt first?”
|
|
35
|
-
Reaction to the concept
|
|
36
|
-
After you’ve listened first, say this:
|
|
37
|
-
“I’m building something that lets developers and QA write readable requests, reusable definitions, environments, assertions, and multi-step sequences directly in VS Code, with the aim of reducing the gap between exploratory work and automation. Based on what you’ve said, what parts of that sound useful and what parts don’t?”
|
|
38
|
-
Then ask:
|
|
39
|
-
“What would make you want to try something like that?”
|
|
40
|
-
“What would make you ignore it?”
|
|
41
|
-
“What would you compare it against immediately?”
|
|
42
|
-
“What would it need to do well before you’d take it seriously?”
|
|
43
|
-
Pilot questions
|
|
44
|
-
“If I gave you access to try this on a real workflow, what would be the best use case to test first?”
|
|
45
|
-
“What would success look like for you in a pilot?”
|
|
46
|
-
“What would need to happen for you to keep using it after the trial?”
|
|
47
|
-
“If it worked well, would you see this as something for just you, or for your wider team?”
|
|
48
|
-
Close
|
|
49
|
-
“This has been really useful. Would you be open to a follow-up once I’ve tightened the product and pilot setup?”
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
created: 2026-04-09T22:56:55.687Z
|
|
3
|
-
updated: 2026-04-09T23:14:25.954Z
|
|
4
|
-
assigned: ""
|
|
5
|
-
progress: 1
|
|
6
|
-
tags:
|
|
7
|
-
- 'Week One'
|
|
8
|
-
due: 2026-04-12T00:00:00.000Z
|
|
9
|
-
completed: 2026-04-10T00:00:00.000Z
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Write the one pager
|
|
13
|
-
|
|
14
|
-
Before your first mentor call, write one page with only these headings:
|
|
15
|
-
What Norn is
|
|
16
|
-
Who it is for
|
|
17
|
-
What problem it solves
|
|
18
|
-
What people use today instead
|
|
19
|
-
Why those alternatives are painful
|
|
20
|
-
What is already built
|
|
21
|
-
What proof you have so far
|
|
22
|
-
What you need help deciding next
|
|
23
|
-
|
|
24
|
-
## Comments
|
|
25
|
-
|
|
26
|
-
- date: 2026-04-09T23:11:55.306Z
|
|
27
|
-
https://docs.google.com/document/d/10L0HINdu6bvcKK5FhMBthPHUQYFKo7ix8OBFo-CcodU/edit?usp=sharing
|