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.
@@ -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
- ## [Unreleased]
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
- "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.
11
+ "Extension" means the Norn extension for Visual Studio Code.
12
+
13
+ "CLI" means the Norn command-line interface.
12
14
 
13
- "License Key" means a unique key-code that enables a single Authorized User to use the Software at a time. Only Licensor and/or its representatives are permitted to produce License Keys for the Software.
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
- "Commercial Use" means any use of the Software in connection with a business, including but not limited to: (a) use by an employee or contractor during paid work hours, (b) use to develop, test, or maintain software or services that generate revenue, (c) use within a for-profit organisation, or (d) use in CI/CD pipelines for commercial projects.
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
- "Personal Use" means use by an individual for personal projects, learning, education, or open-source projects that do not generate revenue.
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 Norn software is licensed to provide functionality to test APIs in Visual Studio Code as an Extension. The Norn CLI can be used locally on the user's computer for Personal Use without a license. A license is required to use the Software for Commercial Use, including use of the Norn CLI in CI/CD build pipelines.
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
- Businesses and organisations may evaluate the Software for Commercial Use without a license for a period of thirty (30) days from first use ("Evaluation Period"). The Evaluation Period is:
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
- After the Evaluation Period, continued Commercial Use requires a valid license. Abuse of the Evaluation Period, including but not limited to repeated evaluations by the same organisation, rotating users to extend evaluation, or using evaluation for production purposes, constitutes a breach of this Agreement.
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
- COMMERCIAL USE
41
+ LICENSED USE (PIPELINE USE OF THE CLI)
50
42
 
51
- A license is required for Commercial Use of the Software after the Evaluation Period. Commercial Use includes but is not limited to:
52
- - Use within a for-profit company or organisation
53
- - Use by employees or contractors during paid work
54
- - Use in CI/CD pipelines for commercial projects
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 commercial licensing options.
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 your use as Personal Use when it constitutes Commercial Use.
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 is a REST client for VS Code that keeps ad hoc requests, reusable API flows, and test automation in the same files. Write a request once, turn it into a sequence, debug it in the editor, and run the same `.norn` files from the CLI in CI.
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 (variables[value] !== void 0) {
127587
+ if (shouldResolveBareRequestValue(value, variables)) {
127580
127588
  return stringifyRequestValue(variables[value]);
127581
127589
  }
127582
127590
  const pathMatch2 = value.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(\.\w[\w-]*(?:\.\w[\w-]*|\[\d+\])*|\[\d+\](?:\.\w[\w-]*|\[\d+\])*)$/);
127583
- if (pathMatch2 && variables[pathMatch2[1]] !== void 0) {
127591
+ if (pathMatch2 && shouldResolveBareRequestValue(pathMatch2[1], variables)) {
127584
127592
  const nestedValue = getNestedPathValue(variables[pathMatch2[1]], pathMatch2[2].replace(/^\./, ""));
127585
127593
  if (nestedValue !== void 0) {
127586
127594
  return stringifyRequestValue(nestedValue);
@@ -127832,7 +127840,7 @@ function resolveEnvironmentTemplateValue(variableName, variables, secretNames =
127832
127840
  kind: "scope",
127833
127841
  variableName: name,
127834
127842
  reference: referenceName,
127835
- message: `.nornenv template reference '${referenceName}' is environment-specific. Values inside [env:...] sections can only reference common variables.`
127843
+ message: `.nornenv template reference '${referenceName}' is not in scope. Values inside [env:...] sections can reference common variables and inherited ancestor variables only.`
127836
127844
  });
127837
127845
  }
127838
127846
  if (resolved.secret) {
@@ -132675,24 +132683,58 @@ function listCachedSecretKeyIds(targetPath) {
132675
132683
  // src/environmentParser.ts
132676
132684
  var ENV_FILENAME = ".nornenv";
132677
132685
  var importRegex = /^import\s+["']?(.+?)["']?\s*$/;
132678
- var envRegex = /^\[env:([a-zA-Z_][a-zA-Z0-9_-]*)\]$/;
132686
+ var sectionHeaderRegex = /^\[(env|template):([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s+extends\s+([^\]]+?))?\]\s*$/;
132687
+ var identifierRegex = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
132679
132688
  var varRegex = /^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
132680
132689
  var connectionStringRegex = /^connectionString\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
132681
132690
  var secretRegex = /^secret\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
132682
132691
  var secretConnectionStringRegex = /^secret\s+connectionString\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
132692
+ function parseExtendsClause(clause) {
132693
+ if (!clause) {
132694
+ return [];
132695
+ }
132696
+ const seen = /* @__PURE__ */ new Set();
132697
+ const names = [];
132698
+ for (const raw of clause.split(",")) {
132699
+ const name = raw.trim();
132700
+ if (!name || !identifierRegex.test(name) || seen.has(name)) {
132701
+ continue;
132702
+ }
132703
+ seen.add(name);
132704
+ names.push(name);
132705
+ }
132706
+ return names;
132707
+ }
132683
132708
  function parseEnvFile(content, sourceFilePath) {
132684
132709
  const lines = content.split("\n");
132685
132710
  const config2 = {
132686
132711
  common: {},
132687
132712
  environments: [],
132713
+ templates: [],
132688
132714
  secretNames: /* @__PURE__ */ new Set(),
132689
132715
  secretValues: /* @__PURE__ */ new Map(),
132690
132716
  imports: [],
132691
132717
  misplacedImports: [],
132692
132718
  secretDeclarations: []
132693
132719
  };
132694
- let currentEnv = null;
132720
+ let currentSection = null;
132721
+ let currentKind = null;
132695
132722
  let seenContent = false;
132723
+ const assignVariable = (varName, varValue) => {
132724
+ if (currentSection) {
132725
+ currentSection.variables[varName] = varValue;
132726
+ } else {
132727
+ config2.common[varName] = varValue;
132728
+ }
132729
+ };
132730
+ const buildSecretDeclaration = (varName, varValue, lineNumber) => ({
132731
+ name: varName,
132732
+ value: varValue,
132733
+ envName: currentKind === "env" ? currentSection?.name : void 0,
132734
+ templateName: currentKind === "template" ? currentSection?.name : void 0,
132735
+ lineNumber,
132736
+ filePath: sourceFilePath
132737
+ });
132696
132738
  for (let i = 0; i < lines.length; i++) {
132697
132739
  const trimmed = lines[i].trim();
132698
132740
  if (!trimmed || trimmed.startsWith("#")) {
@@ -132711,13 +132753,21 @@ function parseEnvFile(content, sourceFilePath) {
132711
132753
  continue;
132712
132754
  }
132713
132755
  seenContent = true;
132714
- const envMatch = trimmed.match(envRegex);
132715
- if (envMatch) {
132716
- currentEnv = {
132717
- name: envMatch[1],
132718
- variables: {}
132719
- };
132720
- config2.environments.push(currentEnv);
132756
+ const sectionMatch = trimmed.match(sectionHeaderRegex);
132757
+ if (sectionMatch) {
132758
+ const kind = sectionMatch[1];
132759
+ const name = sectionMatch[2];
132760
+ const parents = parseExtendsClause(sectionMatch[3]);
132761
+ if (kind === "env") {
132762
+ const env3 = { name, variables: {}, parents };
132763
+ config2.environments.push(env3);
132764
+ currentSection = env3;
132765
+ } else {
132766
+ const template = { name, variables: {}, parents };
132767
+ config2.templates.push(template);
132768
+ currentSection = template;
132769
+ }
132770
+ currentKind = kind;
132721
132771
  continue;
132722
132772
  }
132723
132773
  const secretConnectionStringMatch = trimmed.match(secretConnectionStringRegex);
@@ -132725,40 +132775,20 @@ function parseEnvFile(content, sourceFilePath) {
132725
132775
  const profileName = secretConnectionStringMatch[1];
132726
132776
  const varName = `${profileName}_connectionString`;
132727
132777
  const varValue = secretConnectionStringMatch[2].trim();
132728
- config2.secretDeclarations.push({
132729
- name: varName,
132730
- value: varValue,
132731
- envName: currentEnv?.name,
132732
- lineNumber: i,
132733
- filePath: sourceFilePath
132734
- });
132778
+ config2.secretDeclarations.push(buildSecretDeclaration(varName, varValue, i));
132735
132779
  config2.secretNames.add(varName);
132736
132780
  config2.secretValues.set(varName, varValue);
132737
- if (currentEnv) {
132738
- currentEnv.variables[varName] = varValue;
132739
- } else {
132740
- config2.common[varName] = varValue;
132741
- }
132781
+ assignVariable(varName, varValue);
132742
132782
  continue;
132743
132783
  }
132744
132784
  const secretMatch = trimmed.match(secretRegex);
132745
132785
  if (secretMatch) {
132746
132786
  const varName = secretMatch[1];
132747
132787
  const varValue = secretMatch[2].trim();
132748
- config2.secretDeclarations.push({
132749
- name: varName,
132750
- value: varValue,
132751
- envName: currentEnv?.name,
132752
- lineNumber: i,
132753
- filePath: sourceFilePath
132754
- });
132788
+ config2.secretDeclarations.push(buildSecretDeclaration(varName, varValue, i));
132755
132789
  config2.secretNames.add(varName);
132756
132790
  config2.secretValues.set(varName, varValue);
132757
- if (currentEnv) {
132758
- currentEnv.variables[varName] = varValue;
132759
- } else {
132760
- config2.common[varName] = varValue;
132761
- }
132791
+ assignVariable(varName, varValue);
132762
132792
  continue;
132763
132793
  }
132764
132794
  const connectionStringMatch = trimmed.match(connectionStringRegex);
@@ -132766,22 +132796,14 @@ function parseEnvFile(content, sourceFilePath) {
132766
132796
  const profileName = connectionStringMatch[1];
132767
132797
  const varName = `${profileName}_connectionString`;
132768
132798
  const varValue = connectionStringMatch[2].trim();
132769
- if (currentEnv) {
132770
- currentEnv.variables[varName] = varValue;
132771
- } else {
132772
- config2.common[varName] = varValue;
132773
- }
132799
+ assignVariable(varName, varValue);
132774
132800
  continue;
132775
132801
  }
132776
132802
  const varMatch = trimmed.match(varRegex);
132777
132803
  if (varMatch) {
132778
132804
  const varName = varMatch[1];
132779
132805
  const varValue = varMatch[2].trim();
132780
- if (currentEnv) {
132781
- currentEnv.variables[varName] = varValue;
132782
- } else {
132783
- config2.common[varName] = varValue;
132784
- }
132806
+ assignVariable(varName, varValue);
132785
132807
  }
132786
132808
  }
132787
132809
  return config2;
@@ -132910,6 +132932,11 @@ function registerVariableOrigins(config2, filePath, origins) {
132910
132932
  origins.set(`env:${env3.name}:${varName}`, { filePath, line: -1, varName });
132911
132933
  }
132912
132934
  }
132935
+ for (const template of config2.templates) {
132936
+ for (const varName of Object.keys(template.variables)) {
132937
+ origins.set(`template:${template.name}:${varName}`, { filePath, line: -1, varName });
132938
+ }
132939
+ }
132913
132940
  }
132914
132941
  function mergeConfigs(target, source, targetFilePath, sourceFilePath, variableOrigins, errors) {
132915
132942
  for (const [varName, varValue] of Object.entries(source.common)) {
@@ -132932,8 +132959,14 @@ function mergeConfigs(target, source, targetFilePath, sourceFilePath, variableOr
132932
132959
  for (const sourceEnv of source.environments) {
132933
132960
  let targetEnv = target.environments.find((e) => e.name === sourceEnv.name);
132934
132961
  if (!targetEnv) {
132935
- targetEnv = { name: sourceEnv.name, variables: {} };
132962
+ targetEnv = { name: sourceEnv.name, variables: {}, parents: [...sourceEnv.parents] };
132936
132963
  target.environments.push(targetEnv);
132964
+ } else {
132965
+ for (const parent of sourceEnv.parents) {
132966
+ if (!targetEnv.parents.includes(parent)) {
132967
+ targetEnv.parents.push(parent);
132968
+ }
132969
+ }
132937
132970
  }
132938
132971
  for (const [varName, varValue] of Object.entries(sourceEnv.variables)) {
132939
132972
  const key = `env:${sourceEnv.name}:${varName}`;
@@ -132952,6 +132985,35 @@ function mergeConfigs(target, source, targetFilePath, sourceFilePath, variableOr
132952
132985
  }
132953
132986
  }
132954
132987
  }
132988
+ for (const sourceTemplate of source.templates) {
132989
+ let targetTemplate = target.templates.find((t) => t.name === sourceTemplate.name);
132990
+ if (!targetTemplate) {
132991
+ targetTemplate = { name: sourceTemplate.name, variables: {}, parents: [...sourceTemplate.parents] };
132992
+ target.templates.push(targetTemplate);
132993
+ } else {
132994
+ for (const parent of sourceTemplate.parents) {
132995
+ if (!targetTemplate.parents.includes(parent)) {
132996
+ targetTemplate.parents.push(parent);
132997
+ }
132998
+ }
132999
+ }
133000
+ for (const [varName, varValue] of Object.entries(sourceTemplate.variables)) {
133001
+ const key = `template:${sourceTemplate.name}:${varName}`;
133002
+ const existing = variableOrigins.get(key);
133003
+ if (existing) {
133004
+ const sourceLabel = toDisplayPath(sourceFilePath, targetFilePath);
133005
+ const existingLabel = toDisplayPath(existing.filePath, targetFilePath);
133006
+ errors.push({
133007
+ message: `Duplicate variable '${varName}' in [template:${sourceTemplate.name}] section. Found in '${existingLabel}' and '${sourceLabel}'.`,
133008
+ filePath: sourceFilePath,
133009
+ line: -1
133010
+ });
133011
+ } else {
133012
+ targetTemplate.variables[varName] = varValue;
133013
+ variableOrigins.set(key, { filePath: sourceFilePath, line: -1, varName });
133014
+ }
133015
+ }
133016
+ }
132955
133017
  for (const name of source.secretNames) {
132956
133018
  target.secretNames.add(name);
132957
133019
  }
@@ -132983,6 +133045,90 @@ function loadAndResolveEnvFile(filePath) {
132983
133045
  result.secretErrors.push(...resolveEncryptedSecretValues(result.config, filePath));
132984
133046
  return result;
132985
133047
  }
133048
+ function findExtendsNode(name, config2) {
133049
+ const env3 = config2.environments.find((e) => e.name === name);
133050
+ if (env3) {
133051
+ return { node: env3, kind: "env" };
133052
+ }
133053
+ const template = config2.templates.find((t) => t.name === name);
133054
+ if (template) {
133055
+ return { node: template, kind: "template" };
133056
+ }
133057
+ return void 0;
133058
+ }
133059
+ function findExtendsTemplate(name, config2) {
133060
+ const template = config2.templates.find((t) => t.name === name);
133061
+ return template ? { node: template, kind: "template" } : void 0;
133062
+ }
133063
+ function resolveEffectiveEnvVariables(envName, config2) {
133064
+ return Object.fromEntries(
133065
+ Array.from(resolveEffectiveEnvVariableDetails(envName, config2).entries()).map(([name, detail]) => [name, detail.value])
133066
+ );
133067
+ }
133068
+ function resolveInheritedVariableDetails(nodeName, config2) {
133069
+ const inherited = /* @__PURE__ */ new Map();
133070
+ const visited = /* @__PURE__ */ new Set();
133071
+ const stack = /* @__PURE__ */ new Set();
133072
+ const self2 = findExtendsNode(nodeName, config2);
133073
+ if (!self2) {
133074
+ return inherited;
133075
+ }
133076
+ const walk = (name) => {
133077
+ if (visited.has(name) || stack.has(name)) {
133078
+ return;
133079
+ }
133080
+ const found = findExtendsTemplate(name, config2);
133081
+ if (!found) {
133082
+ return;
133083
+ }
133084
+ stack.add(name);
133085
+ for (const parent of found.node.parents) {
133086
+ walk(parent);
133087
+ }
133088
+ for (const [varName, value] of Object.entries(found.node.variables)) {
133089
+ inherited.set(varName, {
133090
+ name: varName,
133091
+ value,
133092
+ sourceKind: found.kind,
133093
+ sourceName: found.node.name,
133094
+ inherited: true
133095
+ });
133096
+ }
133097
+ visited.add(name);
133098
+ stack.delete(name);
133099
+ };
133100
+ for (const parent of self2.node.parents) {
133101
+ walk(parent);
133102
+ }
133103
+ return inherited;
133104
+ }
133105
+ function resolveEffectiveEnvVariableDetails(envName, config2) {
133106
+ const details = /* @__PURE__ */ new Map();
133107
+ for (const [name, value] of Object.entries(config2.common)) {
133108
+ details.set(name, {
133109
+ name,
133110
+ value,
133111
+ sourceKind: "common",
133112
+ inherited: true
133113
+ });
133114
+ }
133115
+ for (const [name, detail] of resolveInheritedVariableDetails(envName, config2)) {
133116
+ details.set(name, detail);
133117
+ }
133118
+ const env3 = config2.environments.find((e) => e.name === envName);
133119
+ if (env3) {
133120
+ for (const [name, value] of Object.entries(env3.variables)) {
133121
+ details.set(name, {
133122
+ name,
133123
+ value,
133124
+ sourceKind: "env",
133125
+ sourceName: env3.name,
133126
+ inherited: false
133127
+ });
133128
+ }
133129
+ }
133130
+ return details;
133131
+ }
132986
133132
  function resolveEncryptedSecretValues(config2, entryFilePath) {
132987
133133
  const errors = [];
132988
133134
  for (const declaration of config2.secretDeclarations) {
@@ -133035,6 +133181,11 @@ function resolveEncryptedSecretValues(config2, entryFilePath) {
133035
133181
  if (env3) {
133036
133182
  env3.variables[declaration.name] = plaintext;
133037
133183
  }
133184
+ } else if (declaration.templateName) {
133185
+ const template = config2.templates.find((t) => t.name === declaration.templateName);
133186
+ if (template) {
133187
+ template.variables[declaration.name] = plaintext;
133188
+ }
133038
133189
  } else {
133039
133190
  config2.common[declaration.name] = plaintext;
133040
133191
  }
@@ -133052,7 +133203,7 @@ var import_process = require("process");
133052
133203
  // src/secrets/envFileSecrets.ts
133053
133204
  var fs16 = __toESM(require("fs"));
133054
133205
  var path16 = __toESM(require("path"));
133055
- var envRegex2 = /^\s*\[env:([a-zA-Z_][a-zA-Z0-9_-]*)\]\s*$/;
133206
+ var envRegex = /^\s*\[env:([a-zA-Z_][a-zA-Z0-9_-]*)\]\s*$/;
133056
133207
  var secretConnectionStringRegex2 = /^(\s*secret\s+connectionString\s+)([a-zA-Z_][a-zA-Z0-9_]*)(\s*=\s*)(.+)$/;
133057
133208
  var secretRegex2 = /^(\s*secret\s+)([a-zA-Z_][a-zA-Z0-9_]*)(\s*=\s*)(.+)$/;
133058
133209
  function splitContentLines(content) {
@@ -133074,7 +133225,7 @@ function extractSecretLines(content, filePath) {
133074
133225
  if (!trimmed || trimmed.startsWith("#")) {
133075
133226
  continue;
133076
133227
  }
133077
- const envMatch = trimmed.match(envRegex2);
133228
+ const envMatch = trimmed.match(envRegex);
133078
133229
  if (envMatch) {
133079
133230
  currentEnv = envMatch[1];
133080
133231
  continue;
@@ -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
- const variables = { ...envConfig.common };
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
- Object.assign(variables, targetEnvVariables);
133676
- for (const [name, value] of Object.entries(targetEnvVariables)) {
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 - REST Client",
4
- "description": "A powerful REST client for making HTTP requests with sequences, variables, scripts, and cookie support",
5
- "version": "2.2.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
- "rest",
26
- "api",
25
+ "api testing",
26
+ "integration testing",
27
+ "ci",
28
+ "regression testing",
29
+ "api automation",
30
+ "openapi",
27
31
  "http",
28
- "request",
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,9 +0,0 @@
1
- ---
2
- created: 2026-04-09T23:04:43.339Z
3
- updated: 2026-04-09T23:04:43.337Z
4
- assigned: ""
5
- progress: 0
6
- tags: []
7
- ---
8
-
9
- # Decide what success in a pilot looks like
@@ -1,9 +0,0 @@
1
- ---
2
- created: 2026-04-09T23:03:31.570Z
3
- updated: 2026-04-09T23:03:31.569Z
4
- assigned: ""
5
- progress: 0
6
- tags: []
7
- ---
8
-
9
- # Do 5 customer conversations
@@ -1,11 +0,0 @@
1
- ---
2
- created: 2026-04-09T22:59:46.277Z
3
- updated: 2026-04-09T23:03:00.808Z
4
- assigned: ""
5
- progress: 0
6
- tags:
7
- - 'Week One'
8
- due: 2026-04-12T00:00:00.000Z
9
- ---
10
-
11
- # Finalise the one-line pitch
@@ -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,11 +0,0 @@
1
- ---
2
- created: 2026-04-09T23:00:11.428Z
3
- updated: 2026-04-09T23:02:58.760Z
4
- assigned: ""
5
- progress: 0
6
- tags:
7
- - 'Week One'
8
- due: 2026-04-12T00:00:00.000Z
9
- ---
10
-
11
- # Make a list of 10 people to speak to
@@ -1,11 +0,0 @@
1
- ---
2
- created: 2026-04-09T23:00:42.150Z
3
- updated: 2026-04-09T23:02:55.974Z
4
- assigned: ""
5
- progress: 0
6
- tags:
7
- - 'Week One'
8
- due: 2026-04-12T00:00:00.000Z
9
- ---
10
-
11
- # Prepare your customer interview questions
@@ -1,9 +0,0 @@
1
- ---
2
- created: 2026-04-09T23:04:26.660Z
3
- updated: 2026-04-09T23:04:26.657Z
4
- assigned: ""
5
- progress: 0
6
- tags: []
7
- ---
8
-
9
- # Recruit 2–3 pilot users
@@ -1,9 +0,0 @@
1
- ---
2
- created: 2026-04-09T23:04:09.931Z
3
- updated: 2026-04-09T23:04:09.930Z
4
- assigned: ""
5
- progress: 0
6
- tags: []
7
- ---
8
-
9
- # Refine your pitch
@@ -1,9 +0,0 @@
1
- ---
2
- created: 2026-04-30T21:13:26.675Z
3
- updated: 2026-04-30T21:13:26.669Z
4
- assigned: ""
5
- progress: 0
6
- tags: []
7
- ---
8
-
9
- # Use the shiplight website as a template to improve norn website
@@ -1,9 +0,0 @@
1
- ---
2
- created: 2026-04-09T23:03:51.949Z
3
- updated: 2026-04-09T23:03:51.947Z
4
- assigned: ""
5
- progress: 0
6
- tags: []
7
- ---
8
-
9
- # Write down repeated wording
@@ -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