norn-cli 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/norn-social-campaign/SKILL.md +70 -0
- package/CHANGELOG.md +6 -0
- package/demos/nornenv-region-refactor/README.md +64 -0
- package/dist/cli.js +360 -1
- package/out/apiResponseIntellisenseCache.js +394 -0
- package/out/assertionRunner.js +567 -0
- package/out/cacheDir.js +136 -0
- package/out/chatParticipant.js +763 -0
- package/out/cli/colors.js +127 -0
- package/out/cli/formatters/assertion.js +102 -0
- package/out/cli/formatters/index.js +23 -0
- package/out/cli/formatters/response.js +106 -0
- package/out/cli/formatters/summary.js +246 -0
- package/out/cli/redaction.js +237 -0
- package/out/cli/reporters/html.js +689 -0
- package/out/cli/reporters/index.js +22 -0
- package/out/cli/reporters/junit.js +226 -0
- package/out/codeLensProvider.js +351 -0
- package/out/compareContentProvider.js +85 -0
- package/out/completionProvider.js +3739 -0
- package/out/contractAssertionSummary.js +225 -0
- package/out/contractDecorationProvider.js +243 -0
- package/out/coverageCalculator.js +879 -0
- package/out/coveragePanel.js +597 -0
- package/out/debug/breakpointResolver.js +84 -0
- package/out/debug/breakpoints.js +52 -0
- package/out/debug/nornDebugAdapter.js +166 -0
- package/out/debug/nornDebugSession.js +613 -0
- package/out/debug/sequenceLocationIndex.js +77 -0
- package/out/debug/types.js +3 -0
- package/out/deepClone.js +21 -0
- package/out/diagnosticProvider.js +2554 -0
- package/out/environmentParser.js +736 -0
- package/out/environmentProvider.js +544 -0
- package/out/environmentTemplates.js +146 -0
- package/out/errors/formatError.js +113 -0
- package/out/errors/nornError.js +29 -0
- package/out/formUrlEncoded.js +89 -0
- package/out/httpClient.js +348 -0
- package/out/httpRuntimeOptions.js +16 -0
- package/out/importErrors.js +31 -0
- package/out/inlayHintResolver.js +70 -0
- package/out/jsonFileReader.js +323 -0
- package/out/mcpClient.js +193 -0
- package/out/mcpConfig.js +184 -0
- package/out/mcpToolIntellisenseCache.js +96 -0
- package/out/mcpToolSchema.js +50 -0
- package/out/nornConfig.js +132 -0
- package/out/nornHoverProvider.js +124 -0
- package/out/nornInlayHintsProvider.js +191 -0
- package/out/nornPrompt.js +755 -0
- package/out/nornSqlParser.js +286 -0
- package/out/nornapiHoverProvider.js +135 -0
- package/out/nornapiInlayHintsProvider.js +94 -0
- package/out/nornapiParser.js +324 -0
- package/out/nornenvCodeActionProvider.js +101 -0
- package/out/nornenvDecorationProvider.js +239 -0
- package/out/nornenvFoldingProvider.js +63 -0
- package/out/nornenvHoverProvider.js +114 -0
- package/out/nornenvInlayHintsProvider.js +99 -0
- package/out/nornenvLanguageModel.js +187 -0
- package/out/nornenvRegionRefactor.js +267 -0
- package/out/nornsqlHoverProvider.js +95 -0
- package/out/nornsqlInlayHintsProvider.js +114 -0
- package/out/parser.js +839 -0
- package/out/pathAccess.js +28 -0
- package/out/postmanImportPanel.js +732 -0
- package/out/postmanImportPlanner.js +1155 -0
- package/out/postmanImportSidebarView.js +532 -0
- package/out/quotedString.js +35 -0
- package/out/requestPreparation.js +179 -0
- package/out/requestValidation.js +146 -0
- package/out/responsePanel.js +7754 -0
- package/out/schemaGenerator.js +562 -0
- package/out/scriptRunner.js +419 -0
- package/out/secrets/cliSecrets.js +415 -0
- package/out/secrets/crypto.js +105 -0
- package/out/secrets/envFileSecrets.js +177 -0
- package/out/secrets/keyStore.js +259 -0
- package/out/sequenceDeclaration.js +15 -0
- package/out/sequenceRunner.js +3590 -0
- package/out/sqlAdapterRunner.js +122 -0
- package/out/sqlBuiltInAdapters.js +604 -0
- package/out/sqlConfig.js +184 -0
- package/out/starterCatalog.js +554 -0
- package/out/stringUtils.js +25 -0
- package/out/swaggerBodyIntellisenseCache.js +114 -0
- package/out/swaggerParser.js +464 -0
- package/out/testProvider.js +767 -0
- package/out/theoryCaseLoader.js +113 -0
- package/out/validationCache.js +211 -0
- package/package.json +6 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: norn-social-campaign
|
|
3
|
+
description: Continue Peter's Norn LinkedIn social-media campaign — draft/review/schedule posts, reply to engagement, evaluate Week-N kill-criteria, log community signals. Use whenever the user asks about Norn LinkedIn posts, replies, engagement metrics, the GTM experiment, what to post next, or any social/content work for Norn.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Norn Social Media Campaign — Cross-Session Continuity
|
|
7
|
+
|
|
8
|
+
You are resuming an ongoing GTM experiment Peter has been running for **Norn** (a commercial VS Code extension + CLI he builds solo while employed full-time, UK-based). Most strategic decisions are **locked** — your job is to execute within them, not re-litigate. The canonical state lives in repo `Docs/`; read those before doing real work.
|
|
9
|
+
|
|
10
|
+
## Read these first (canonical, in this repo)
|
|
11
|
+
|
|
12
|
+
- `Docs/gtm_plan.md` — **the master plan**. Locked positioning, 13-week kill-criteria with anchored dates, action steps, and an append-only **Progress Log** (scroll to the bottom to see exactly where things stand).
|
|
13
|
+
- `Docs/mot_reaction_series.md` — **the LinkedIn posts** (the filename is misnamed — see Anti-patterns; rename pending).
|
|
14
|
+
- `Docs/linkedin_series.md` — LinkedIn delivery rules (tags, first-comment text, image policy, posting cadence).
|
|
15
|
+
- `Docs/market_signals.md` — append-only community pain-signal intel log. Append; don't re-derive.
|
|
16
|
+
- `Docs/postman_rot_essay.md` (long-form v3) and `Docs/mot_series.md` (v1 micro-posts) — backup bank, not active.
|
|
17
|
+
|
|
18
|
+
Memory files `norn-gtm-decision.md` and `norn-messaging-locked.md` auto-load and carry the durable decisions.
|
|
19
|
+
|
|
20
|
+
## What is LOCKED — do not reopen
|
|
21
|
+
|
|
22
|
+
- **Positioning spine (verbatim, never paraphrase):**
|
|
23
|
+
> 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.
|
|
24
|
+
Category = repo-native API regression testing. NOT "REST client", NOT vaguer "workflow automation."
|
|
25
|
+
- **GTM motion:** public commercial launch (PLG + upmarket founder-led semi-sales). 13-week time-boxed experiment. Kill-criteria + anchored dates in `gtm_plan.md`. "CV-asset" is an accepted non-failure outcome.
|
|
26
|
+
- **Sell to:** CTO / founding eng / tech lead at small companies (~≤20–30 people). **QA = evangelist, not buyer.** Eng Manager parked until 50+ people.
|
|
27
|
+
- **Pricing model:** VS Code Extension is **free forever (incl. commercial).** CLI is **free for local use.** **Paid only for CLI Pipeline Use (CI/CD).** No evaluation period. Encoded in `LICENSE` (vsApi + Norn, kept in sync).
|
|
28
|
+
- **Channel: LinkedIn only.** MoT dropped 2026-05-18. One uniform series posted to LinkedIn.
|
|
29
|
+
- **Voice:** dry, specific, recognition-humor, no slang/meme/"touch grass" register, clarity > cleverness, no clever coda after the closing question. ~150–220 words per post.
|
|
30
|
+
- **Canonical ending** (append verbatim, URL-free, to every post):
|
|
31
|
+
> Disclosure so nobody feels tricked: I build Norn — my attempt at the in-the-repo fix. That's the entire pitch; the post stands without it. Still genuinely curious about the question above, though.
|
|
32
|
+
- **Tags (fixed, every post):** `#API #Testing #DeveloperTools`. Add `#MCP` only on the MCP post. 4 max.
|
|
33
|
+
- **First comment** (within ~1 min of posting):
|
|
34
|
+
> The Norn bit I mentioned, if you want to poke at it: https://nornapi.com
|
|
35
|
+
- **Posting cadence:** Tue–Thu ~8:30am UK time, one post/day, ≥4–5 days apart.
|
|
36
|
+
- **Visual rule:** real source screenshot (with identifying bits cropped) if a genuine artifact exists; text-only otherwise. **Never** Norn logo. **Never** AI imagery.
|
|
37
|
+
- **Self-presentation:** "QA / automation engineer who builds Norn." **Never "Founder."** The reason is strategic (credibility + disclosure-not-pitch posture), not legal — Peter's employment contract is permissive, employer has long known.
|
|
38
|
+
|
|
39
|
+
## Anti-patterns — do NOT do
|
|
40
|
+
|
|
41
|
+
- **Don't reopen the positioning spine.** It was rewritten many times before locking; uniformity *is* the strategy.
|
|
42
|
+
- **Don't suggest pitching Peter's employer.** Internal-champion path was closed 2026-05-18 — declined "prefer not to sour the relationship" (structural/relationship objection, not product, not IP). Their "no" is excluded from any market-signal read.
|
|
43
|
+
- **Don't raise the employer IP-alarm.** Contract permissive; employer long aware; Peter refused a restrictive replacement years ago specifically to protect this. Settled.
|
|
44
|
+
- **Don't suggest MoT / Reddit / forums as a posting channel.** Dropped. LinkedIn only.
|
|
45
|
+
- **Don't pitch Norn in post bodies or in comment replies.** The canonical ending + first comment do all the selling. Replies = host of a conversation, not salesperson.
|
|
46
|
+
- **Don't suggest a separate LinkedIn-native content stream.** One uniform series. Per-channel variants caused the consistency problem the whole session was about.
|
|
47
|
+
- **Don't add clever/meta codas after the closing question.** Two prior attempts ("wishful Slack message", "I want to collect these") had to be fixed because they were too clever for the reader. Plain question close, full stop.
|
|
48
|
+
- **Don't suggest building product features speculatively.** Build bets in `market_signals.md` are *candidates* — build only after Week-7+ signals justify. "Don't build on spec."
|
|
49
|
+
- **Don't write more posts on spec before the Week-4 verdict.** If the format doesn't travel, posts 7+ are wasted on a wrong format.
|
|
50
|
+
- **The file `mot_reaction_series.md` is misnamed** (no MoT anymore). Rename to `linkedin_posts.md` is a pending cleanup; cross-refs in `gtm_plan.md` and `linkedin_series.md` will need updating in the same pass.
|
|
51
|
+
|
|
52
|
+
## How to handle common requests
|
|
53
|
+
|
|
54
|
+
- **"Give me the next post" / "what should I post":** check the Progress Log in `gtm_plan.md` to see which posts are scheduled vs sent. Posts live in `mot_reaction_series.md`. Format LinkedIn-ready: strip the internal `## N. Title` label; strip markdown asterisks (LinkedIn shows them literally); append the canonical ending; append the tag line; provide the first-comment text separately. Image: real cropped screenshot if a genuine source exists, otherwise text-only.
|
|
55
|
+
- **"Reply to this comment":** stay in voice; *extend* the discussion, never thank generically; no Norn pitch in the reply (the bio carries it); reply **fast** — early engagement is the single biggest reach lever, far above any other tactic.
|
|
56
|
+
- **"How's the post doing?" / metrics:** calibrate via ratios more than raw numbers. Comment-to-reaction ratio above ~8% = high engagement. A **Send** (LinkedIn DM forward) is the highest-value action — it's the peer-to-peer endorsement the plan was built for. Saves > likes in intent. n=1 is not a trend.
|
|
57
|
+
- **"Week-N review":** open the kill-criteria table in `gtm_plan.md` (dates anchored to 2026-05-21 launch). Evaluate signals honestly against the rule. **The pre-committed exit only works if the review actually happens** — sunk cost will narrate; resist it.
|
|
58
|
+
- **New community pain signal (Reddit/Lobsters/HN):** append verbatim quote + URL + date to `Docs/market_signals.md`, triage into LinkedIn angle + build-bet columns. Don't let mining displace posting.
|
|
59
|
+
|
|
60
|
+
## Working style with Peter
|
|
61
|
+
|
|
62
|
+
- Convert relative dates to absolute when logging (UK timezone).
|
|
63
|
+
- Peter is a non-writer; he trusts drafts. When asked for content, give **clean, copy-pasteable** text. No padding, no "here you go" framing.
|
|
64
|
+
- Peter values **brevity, decisiveness, and honest pushback** over compliance. If something he's about to do is wrong, say so directly with reasoning. Don't be a yes-man — he's caught me on it before.
|
|
65
|
+
- **Consistency is his #1 value.** Keep the Docs as the single source of truth. Any time copy gets revised, sync everywhere it appears.
|
|
66
|
+
- When in doubt about state, **read the bottom of `gtm_plan.md`'s Progress Log** — that's where the truth lives.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
This skill is the index. The `Docs/` files are the canon. When in doubt, read them.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the "Norn" extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [2.4.0] - 2026-05-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Per-folder and persisted active environment** — the active env is now tracked per `.nornenv` file (keyed by absolute path) and persisted to the workspace memento. In monorepos each folder remembers its own selection, and selections survive VS Code restart. The status bar shows the env for the current editor's nearest `.nornenv`. Stale entries (deleted `.nornenv` files) are pruned on load.
|
|
9
|
+
- **Region-pattern refactor** — when a `.nornenv` contains a flat `[env:STAGE_REGION]` matrix (e.g. `dev_us`, `dev_uk`, `prod_us`, `prod_uk`), the top of the file shows a "Refactor N envs into S+R templates" CodeLens. Clicking it classifies each variable by axis (stage / region / leaf-specific) and lifts shared values into `[template:STAGE]` + `[template:REGION]` blocks, leaving only true leaf-specific overrides in the env sections. Missing values and `connectionString` declarations stay leaf-specific, per-declaration `secret` keywords are preserved, and the same generator is available in the CLI via `--refactor-region-pattern` / `--write`. The VS Code refactor is one `WorkspaceEdit` so a single Cmd+Z reverses it.
|
|
10
|
+
|
|
5
11
|
## [2.3.0] - 2026-05-20
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Region-pattern refactor showcase
|
|
2
|
+
|
|
3
|
+
A flat `[env:STAGE_REGION]` matrix that the Norn extension can refactor into the templates + extends shape in one click.
|
|
4
|
+
|
|
5
|
+
## How to use
|
|
6
|
+
|
|
7
|
+
1. Open [.nornenv](./.nornenv) in VS Code.
|
|
8
|
+
2. A `$(sparkle) Refactor 4 envs into 2+2 templates` CodeLens appears at the top of the file.
|
|
9
|
+
3. Click it. A confirmation dialog summarises what will change:
|
|
10
|
+
- 2 vars (`baseUrl`, `apiKey`) lifted to stage templates (`dev`, `prod`)
|
|
11
|
+
- 2 vars (`dbHost`, `bucket`) lifted to region templates (`us`, `uk`)
|
|
12
|
+
- 1 var (`failoverHost`) kept as leaf-specific on `prod_uk`
|
|
13
|
+
4. Confirm. The flat 4-env matrix is replaced with templates + extending envs:
|
|
14
|
+
|
|
15
|
+
```nornenv
|
|
16
|
+
[template:dev]
|
|
17
|
+
var baseUrl = https://dev.example.com
|
|
18
|
+
var apiKey = dev-key-123
|
|
19
|
+
|
|
20
|
+
[template:prod]
|
|
21
|
+
var baseUrl = https://api.example.com
|
|
22
|
+
secret apiKey = prod-key-789
|
|
23
|
+
|
|
24
|
+
[template:us]
|
|
25
|
+
var dbHost = db.us.example.com
|
|
26
|
+
var bucket = data-us
|
|
27
|
+
|
|
28
|
+
[template:uk]
|
|
29
|
+
var dbHost = db.uk.example.com
|
|
30
|
+
var bucket = data-uk
|
|
31
|
+
|
|
32
|
+
[env:dev_us extends dev, us]
|
|
33
|
+
|
|
34
|
+
[env:dev_uk extends dev, uk]
|
|
35
|
+
|
|
36
|
+
[env:prod_us extends prod, us]
|
|
37
|
+
|
|
38
|
+
[env:prod_uk extends prod, uk]
|
|
39
|
+
var failoverHost = api-failover.uk.example.com
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
5. The `secret` keyword on `apiKey` is preserved when the var is lifted to a template.
|
|
43
|
+
|
|
44
|
+
## CLI
|
|
45
|
+
|
|
46
|
+
The same generator is available from the local CLI:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node ./dist/cli.js demos/nornenv-region-refactor/.nornenv --refactor-region-pattern
|
|
50
|
+
node ./dist/cli.js demos/nornenv-region-refactor/.nornenv --refactor-region-pattern --write
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## When the refactor fires
|
|
54
|
+
|
|
55
|
+
The CodeLens appears only when **all** of these are true:
|
|
56
|
+
- At least 2 stages × 2 regions
|
|
57
|
+
- At least 3 populated cells
|
|
58
|
+
- All matrix envs are flat (no existing `extends` clause)
|
|
59
|
+
|
|
60
|
+
It does not touch envs that already use `extends`, envs with names that don't match the `STAGE_REGION` shape, or `connectionString` declarations (those are left in their leaves — refactor manually).
|
|
61
|
+
|
|
62
|
+
## Cmd+Z
|
|
63
|
+
|
|
64
|
+
Everything goes through a single `WorkspaceEdit`, so a single Cmd+Z undoes the whole refactor.
|
package/dist/cli.js
CHANGED
|
@@ -133770,6 +133770,294 @@ function splitImportResolutionErrors(errors) {
|
|
|
133770
133770
|
return { blockingErrors, warningErrors };
|
|
133771
133771
|
}
|
|
133772
133772
|
|
|
133773
|
+
// src/nornenvLanguageModel.ts
|
|
133774
|
+
var SECTION_HEADER_REGEX = /^\s*\[(env|template):([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s+extends\s+([^\]]+))?\]\s*$/i;
|
|
133775
|
+
function parseParentList(clause) {
|
|
133776
|
+
if (!clause) {
|
|
133777
|
+
return [];
|
|
133778
|
+
}
|
|
133779
|
+
return clause.split(",").map((parent) => parent.trim()).filter((parent) => /^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(parent));
|
|
133780
|
+
}
|
|
133781
|
+
function parseNornenvDocumentModel(text) {
|
|
133782
|
+
const sections = [];
|
|
133783
|
+
const declarations = [];
|
|
133784
|
+
const lines = text.split("\n");
|
|
133785
|
+
let currentSection;
|
|
133786
|
+
for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
|
|
133787
|
+
const line2 = lines[lineNumber];
|
|
133788
|
+
const sectionMatch = line2.trim().match(SECTION_HEADER_REGEX);
|
|
133789
|
+
if (sectionMatch) {
|
|
133790
|
+
currentSection = {
|
|
133791
|
+
kind: sectionMatch[1].toLowerCase(),
|
|
133792
|
+
name: sectionMatch[2],
|
|
133793
|
+
parents: parseParentList(sectionMatch[3]),
|
|
133794
|
+
lineNumber
|
|
133795
|
+
};
|
|
133796
|
+
sections.push(currentSection);
|
|
133797
|
+
continue;
|
|
133798
|
+
}
|
|
133799
|
+
const connectionMatch = line2.match(/^(\s*)(secret\s+)?connectionString\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$/i);
|
|
133800
|
+
if (connectionMatch) {
|
|
133801
|
+
const displayName = connectionMatch[3];
|
|
133802
|
+
const name = `${displayName}_connectionString`;
|
|
133803
|
+
const nameStart = line2.indexOf(displayName);
|
|
133804
|
+
const value = connectionMatch[4];
|
|
133805
|
+
const valueStart = line2.length - value.length;
|
|
133806
|
+
declarations.push({
|
|
133807
|
+
name,
|
|
133808
|
+
displayName,
|
|
133809
|
+
value,
|
|
133810
|
+
secret: Boolean(connectionMatch[2]),
|
|
133811
|
+
sectionKind: currentSection?.kind,
|
|
133812
|
+
sectionName: currentSection?.name,
|
|
133813
|
+
lineNumber,
|
|
133814
|
+
nameStart,
|
|
133815
|
+
nameEnd: nameStart + displayName.length,
|
|
133816
|
+
valueStart,
|
|
133817
|
+
valueEnd: line2.length
|
|
133818
|
+
});
|
|
133819
|
+
continue;
|
|
133820
|
+
}
|
|
133821
|
+
const variableMatch = line2.match(/^(\s*)(secret|var)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$/i);
|
|
133822
|
+
if (variableMatch) {
|
|
133823
|
+
const name = variableMatch[3];
|
|
133824
|
+
const nameStart = line2.indexOf(name);
|
|
133825
|
+
const value = variableMatch[4];
|
|
133826
|
+
const valueStart = line2.length - value.length;
|
|
133827
|
+
declarations.push({
|
|
133828
|
+
name,
|
|
133829
|
+
displayName: name,
|
|
133830
|
+
value,
|
|
133831
|
+
secret: variableMatch[2].toLowerCase() === "secret",
|
|
133832
|
+
sectionKind: currentSection?.kind,
|
|
133833
|
+
sectionName: currentSection?.name,
|
|
133834
|
+
lineNumber,
|
|
133835
|
+
nameStart,
|
|
133836
|
+
nameEnd: nameStart + name.length,
|
|
133837
|
+
valueStart,
|
|
133838
|
+
valueEnd: line2.length
|
|
133839
|
+
});
|
|
133840
|
+
}
|
|
133841
|
+
}
|
|
133842
|
+
return { sections, declarations };
|
|
133843
|
+
}
|
|
133844
|
+
|
|
133845
|
+
// src/nornenvRegionRefactor.ts
|
|
133846
|
+
var MATRIX_ENV_NAME_REGEX = /^([a-zA-Z][a-zA-Z0-9-]*)_([a-zA-Z][a-zA-Z0-9-]*)$/;
|
|
133847
|
+
var MIN_STAGES = 2;
|
|
133848
|
+
var MIN_REGIONS = 2;
|
|
133849
|
+
var MIN_CELLS = 3;
|
|
133850
|
+
function isConnectionStringVar(name) {
|
|
133851
|
+
return name.endsWith("_connectionString");
|
|
133852
|
+
}
|
|
133853
|
+
function inferDeclarationKind(name) {
|
|
133854
|
+
return isConnectionStringVar(name) ? "connectionString" : "var";
|
|
133855
|
+
}
|
|
133856
|
+
function inferDisplayName(name) {
|
|
133857
|
+
return isConnectionStringVar(name) ? name.slice(0, -"_connectionString".length) : name;
|
|
133858
|
+
}
|
|
133859
|
+
function declarationKind(declaration) {
|
|
133860
|
+
return declaration.name.endsWith("_connectionString") && declaration.displayName !== declaration.name ? "connectionString" : "var";
|
|
133861
|
+
}
|
|
133862
|
+
function buildDeclarationMap(text) {
|
|
133863
|
+
const model = parseNornenvDocumentModel(text);
|
|
133864
|
+
const byEnv = /* @__PURE__ */ new Map();
|
|
133865
|
+
for (const declaration of model.declarations) {
|
|
133866
|
+
if (declaration.sectionKind !== "env" || !declaration.sectionName) {
|
|
133867
|
+
continue;
|
|
133868
|
+
}
|
|
133869
|
+
if (!byEnv.has(declaration.sectionName)) {
|
|
133870
|
+
byEnv.set(declaration.sectionName, /* @__PURE__ */ new Map());
|
|
133871
|
+
}
|
|
133872
|
+
byEnv.get(declaration.sectionName).set(declaration.name, declaration);
|
|
133873
|
+
}
|
|
133874
|
+
return byEnv;
|
|
133875
|
+
}
|
|
133876
|
+
function getCellValue(cell, name, declarationsByEnv, config2) {
|
|
133877
|
+
const value = cell.env.variables[name];
|
|
133878
|
+
if (value === void 0) {
|
|
133879
|
+
return void 0;
|
|
133880
|
+
}
|
|
133881
|
+
const declaration = declarationsByEnv.get(cell.envName)?.get(name);
|
|
133882
|
+
return {
|
|
133883
|
+
value,
|
|
133884
|
+
secret: declaration?.secret ?? config2.secretNames.has(name),
|
|
133885
|
+
kind: declaration ? declarationKind(declaration) : inferDeclarationKind(name),
|
|
133886
|
+
displayName: declaration?.displayName ?? inferDisplayName(name)
|
|
133887
|
+
};
|
|
133888
|
+
}
|
|
133889
|
+
function classifyByAxis(name, cells, axisKeys, getAxisKey, declarationsByEnv, config2) {
|
|
133890
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
133891
|
+
let presentCells = 0;
|
|
133892
|
+
for (const key of axisKeys) {
|
|
133893
|
+
const axisCells = cells.filter((cell) => getAxisKey(cell) === key);
|
|
133894
|
+
const presentValues = axisCells.map((cell) => getCellValue(cell, name, declarationsByEnv, config2)).filter((value) => value !== void 0);
|
|
133895
|
+
if (presentValues.length === 0) {
|
|
133896
|
+
continue;
|
|
133897
|
+
}
|
|
133898
|
+
presentCells += presentValues.length;
|
|
133899
|
+
if (presentValues.length !== axisCells.length) {
|
|
133900
|
+
return void 0;
|
|
133901
|
+
}
|
|
133902
|
+
const first = presentValues[0];
|
|
133903
|
+
if (presentValues.some((value) => value.value !== first.value)) {
|
|
133904
|
+
return void 0;
|
|
133905
|
+
}
|
|
133906
|
+
byKey.set(key, {
|
|
133907
|
+
value: first.value,
|
|
133908
|
+
secret: presentValues.some((value) => value.secret),
|
|
133909
|
+
kind: first.kind,
|
|
133910
|
+
displayName: first.displayName
|
|
133911
|
+
});
|
|
133912
|
+
}
|
|
133913
|
+
return byKey.size > 0 && presentCells >= 2 ? byKey : void 0;
|
|
133914
|
+
}
|
|
133915
|
+
function detectRegionPattern(config2, text) {
|
|
133916
|
+
const cells = [];
|
|
133917
|
+
for (const env3 of config2.environments) {
|
|
133918
|
+
const match = env3.name.match(MATRIX_ENV_NAME_REGEX);
|
|
133919
|
+
if (!match) {
|
|
133920
|
+
continue;
|
|
133921
|
+
}
|
|
133922
|
+
if (env3.parents.length > 0) {
|
|
133923
|
+
return void 0;
|
|
133924
|
+
}
|
|
133925
|
+
cells.push({ stage: match[1], region: match[2], envName: env3.name, env: env3 });
|
|
133926
|
+
}
|
|
133927
|
+
const stages = Array.from(new Set(cells.map((c) => c.stage)));
|
|
133928
|
+
const regions = Array.from(new Set(cells.map((c) => c.region)));
|
|
133929
|
+
if (stages.length < MIN_STAGES || regions.length < MIN_REGIONS || cells.length < MIN_CELLS) {
|
|
133930
|
+
return void 0;
|
|
133931
|
+
}
|
|
133932
|
+
if ((/* @__PURE__ */ new Set([...stages, ...regions])).size !== stages.length + regions.length) {
|
|
133933
|
+
return void 0;
|
|
133934
|
+
}
|
|
133935
|
+
const existingTemplates = new Set(config2.templates.map((template) => template.name));
|
|
133936
|
+
if ([...stages, ...regions].some((name) => existingTemplates.has(name))) {
|
|
133937
|
+
return void 0;
|
|
133938
|
+
}
|
|
133939
|
+
const allVarNames = /* @__PURE__ */ new Set();
|
|
133940
|
+
for (const cell of cells) {
|
|
133941
|
+
for (const name of Object.keys(cell.env.variables)) {
|
|
133942
|
+
allVarNames.add(name);
|
|
133943
|
+
}
|
|
133944
|
+
}
|
|
133945
|
+
let liftedToStage = 0;
|
|
133946
|
+
let liftedToRegion = 0;
|
|
133947
|
+
let leafSpecific = 0;
|
|
133948
|
+
let skippedConnectionStrings = 0;
|
|
133949
|
+
const assignments = [];
|
|
133950
|
+
const declarationsByEnv = buildDeclarationMap(text);
|
|
133951
|
+
for (const name of allVarNames) {
|
|
133952
|
+
if (isConnectionStringVar(name)) {
|
|
133953
|
+
skippedConnectionStrings++;
|
|
133954
|
+
}
|
|
133955
|
+
const stageValues = isConnectionStringVar(name) ? void 0 : classifyByAxis(name, cells, stages, (cell) => cell.stage, declarationsByEnv, config2);
|
|
133956
|
+
const regionValues = isConnectionStringVar(name) ? void 0 : classifyByAxis(name, cells, regions, (cell) => cell.region, declarationsByEnv, config2);
|
|
133957
|
+
if (stageValues) {
|
|
133958
|
+
assignments.push({ name, axis: "stage", byKey: stageValues });
|
|
133959
|
+
liftedToStage++;
|
|
133960
|
+
} else if (regionValues) {
|
|
133961
|
+
assignments.push({ name, axis: "region", byKey: regionValues });
|
|
133962
|
+
liftedToRegion++;
|
|
133963
|
+
} else {
|
|
133964
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
133965
|
+
for (const cell of cells) {
|
|
133966
|
+
const value = getCellValue(cell, name, declarationsByEnv, config2);
|
|
133967
|
+
if (value !== void 0) {
|
|
133968
|
+
byKey.set(cell.envName, value);
|
|
133969
|
+
}
|
|
133970
|
+
}
|
|
133971
|
+
if (byKey.size > 0) {
|
|
133972
|
+
assignments.push({ name, axis: "leaf", byKey });
|
|
133973
|
+
leafSpecific++;
|
|
133974
|
+
}
|
|
133975
|
+
}
|
|
133976
|
+
}
|
|
133977
|
+
const model = parseNornenvDocumentModel(text);
|
|
133978
|
+
const matrixNames = new Set(cells.map((c) => c.envName));
|
|
133979
|
+
const matrixSectionLines = model.sections.filter((s) => s.kind === "env" && matrixNames.has(s.name)).map((s) => s.lineNumber).sort((a, b) => a - b);
|
|
133980
|
+
if (matrixSectionLines.length === 0) {
|
|
133981
|
+
return void 0;
|
|
133982
|
+
}
|
|
133983
|
+
const firstLine = matrixSectionLines[0];
|
|
133984
|
+
const lastMatrixHeaderLine = matrixSectionLines[matrixSectionLines.length - 1];
|
|
133985
|
+
const sectionsInReplacementRange = model.sections.filter(
|
|
133986
|
+
(section) => section.lineNumber >= firstLine && section.lineNumber <= lastMatrixHeaderLine
|
|
133987
|
+
);
|
|
133988
|
+
if (sectionsInReplacementRange.some((section) => section.kind !== "env" || !matrixNames.has(section.name))) {
|
|
133989
|
+
return void 0;
|
|
133990
|
+
}
|
|
133991
|
+
const lineCount = text.split("\n").length;
|
|
133992
|
+
const sectionsAfter = model.sections.filter((s) => s.lineNumber > lastMatrixHeaderLine).map((s) => s.lineNumber).sort((a, b) => a - b);
|
|
133993
|
+
const lastLine = sectionsAfter.length > 0 ? sectionsAfter[0] - 1 : lineCount - 1;
|
|
133994
|
+
return {
|
|
133995
|
+
stages,
|
|
133996
|
+
regions,
|
|
133997
|
+
cells,
|
|
133998
|
+
assignments,
|
|
133999
|
+
replaceRange: { startLine: firstLine, endLine: lastLine },
|
|
134000
|
+
summary: { liftedToStage, liftedToRegion, leafSpecific, skippedConnectionStrings }
|
|
134001
|
+
};
|
|
134002
|
+
}
|
|
134003
|
+
function generateRegionRefactor(pattern) {
|
|
134004
|
+
const blocks = [];
|
|
134005
|
+
const fmtVar = (assignment, value) => {
|
|
134006
|
+
if (value.kind === "connectionString") {
|
|
134007
|
+
const secretPrefix = value.secret ? "secret " : "";
|
|
134008
|
+
return ` ${secretPrefix}connectionString ${value.displayName} = ${value.value}`;
|
|
134009
|
+
}
|
|
134010
|
+
const keyword = value.secret ? "secret" : "var";
|
|
134011
|
+
return ` ${keyword} ${assignment.name} = ${value.value}`;
|
|
134012
|
+
};
|
|
134013
|
+
for (const stage of pattern.stages) {
|
|
134014
|
+
const stageVars = pattern.assignments.filter((a) => a.axis === "stage" && a.byKey.has(stage));
|
|
134015
|
+
if (stageVars.length === 0) {
|
|
134016
|
+
continue;
|
|
134017
|
+
}
|
|
134018
|
+
const lines = [`[template:${stage}]`];
|
|
134019
|
+
for (const v of stageVars) {
|
|
134020
|
+
lines.push(fmtVar(v, v.byKey.get(stage)));
|
|
134021
|
+
}
|
|
134022
|
+
blocks.push(lines.join("\n"));
|
|
134023
|
+
}
|
|
134024
|
+
for (const region of pattern.regions) {
|
|
134025
|
+
const regionVars = pattern.assignments.filter((a) => a.axis === "region" && a.byKey.has(region));
|
|
134026
|
+
if (regionVars.length === 0) {
|
|
134027
|
+
continue;
|
|
134028
|
+
}
|
|
134029
|
+
const lines = [`[template:${region}]`];
|
|
134030
|
+
for (const v of regionVars) {
|
|
134031
|
+
lines.push(fmtVar(v, v.byKey.get(region)));
|
|
134032
|
+
}
|
|
134033
|
+
blocks.push(lines.join("\n"));
|
|
134034
|
+
}
|
|
134035
|
+
for (const cell of pattern.cells) {
|
|
134036
|
+
const leafVars = pattern.assignments.filter((a) => a.axis === "leaf" && a.byKey.has(cell.envName));
|
|
134037
|
+
const header = `[env:${cell.envName} extends ${cell.stage}, ${cell.region}]`;
|
|
134038
|
+
if (leafVars.length === 0) {
|
|
134039
|
+
blocks.push(header);
|
|
134040
|
+
continue;
|
|
134041
|
+
}
|
|
134042
|
+
const lines = [header];
|
|
134043
|
+
for (const v of leafVars) {
|
|
134044
|
+
lines.push(fmtVar(v, v.byKey.get(cell.envName)));
|
|
134045
|
+
}
|
|
134046
|
+
blocks.push(lines.join("\n"));
|
|
134047
|
+
}
|
|
134048
|
+
return blocks.join("\n\n");
|
|
134049
|
+
}
|
|
134050
|
+
function applyRegionRefactorToText(text, pattern) {
|
|
134051
|
+
const lines = text.split("\n");
|
|
134052
|
+
const startLine = pattern.replaceRange.startLine;
|
|
134053
|
+
const endLine = Math.min(pattern.replaceRange.endLine, lines.length - 1);
|
|
134054
|
+
const replacementLines = generateRegionRefactor(pattern).split("\n");
|
|
134055
|
+
lines.splice(startLine, endLine - startLine + 1, ...replacementLines);
|
|
134056
|
+
const result = lines.join("\n");
|
|
134057
|
+
return text.endsWith("\n") && !result.endsWith("\n") ? `${result}
|
|
134058
|
+
` : result;
|
|
134059
|
+
}
|
|
134060
|
+
|
|
133773
134061
|
// src/cli.ts
|
|
133774
134062
|
function handleImportResolutionErrors(errors, colors) {
|
|
133775
134063
|
const { blockingErrors, warningErrors } = splitImportResolutionErrors(errors);
|
|
@@ -133884,7 +134172,9 @@ function parseArgs(args) {
|
|
|
133884
134172
|
noRedact: false,
|
|
133885
134173
|
tagFilters: [],
|
|
133886
134174
|
tagsFilter: [],
|
|
133887
|
-
insecure: false
|
|
134175
|
+
insecure: false,
|
|
134176
|
+
refactorRegionPattern: false,
|
|
134177
|
+
writeRefactor: false
|
|
133888
134178
|
};
|
|
133889
134179
|
for (let i = 0; i < args.length; i++) {
|
|
133890
134180
|
const arg = args[i];
|
|
@@ -133902,6 +134192,10 @@ function parseArgs(args) {
|
|
|
133902
134192
|
options.timeout = parseInt(args[++i], 10) * 1e3;
|
|
133903
134193
|
} else if (arg === "--insecure") {
|
|
133904
134194
|
options.insecure = true;
|
|
134195
|
+
} else if (arg === "--refactor-region-pattern" || arg === "--refactor-nornenv-region-pattern") {
|
|
134196
|
+
options.refactorRegionPattern = true;
|
|
134197
|
+
} else if (arg === "--write") {
|
|
134198
|
+
options.writeRefactor = true;
|
|
133905
134199
|
} else if (arg === "--no-fail") {
|
|
133906
134200
|
options.failOnError = false;
|
|
133907
134201
|
} else if (arg === "--no-redact") {
|
|
@@ -133957,6 +134251,9 @@ Options:
|
|
|
133957
134251
|
-o, --output-dir <dir> Output directory for reports (auto-generates timestamped files)
|
|
133958
134252
|
--tag <filter> Filter sequences by tag (AND logic, can be repeated)
|
|
133959
134253
|
--tags <filters> Filter sequences by tags (OR logic, comma-separated)
|
|
134254
|
+
--refactor-region-pattern
|
|
134255
|
+
Refactor a flat .nornenv STAGE_REGION matrix to templates
|
|
134256
|
+
--write Apply --refactor-region-pattern instead of printing result
|
|
133960
134257
|
-h, --help Show this help message
|
|
133961
134258
|
|
|
133962
134259
|
Report Generation:
|
|
@@ -134000,11 +134297,62 @@ Examples:
|
|
|
134000
134297
|
norn api-tests.norn --html report.html # Generate HTML report (explicit)
|
|
134001
134298
|
norn api-tests.norn --insecure # Allow self-signed/local TLS certs
|
|
134002
134299
|
norn api-tests.norn --no-redact # Show all data (no redaction)
|
|
134300
|
+
norn .nornenv --refactor-region-pattern # Print refactored .nornenv
|
|
134301
|
+
norn .nornenv --refactor-region-pattern --write
|
|
134003
134302
|
norn secrets keygen --name team-main # Generate shared key and cache locally
|
|
134004
134303
|
norn secrets import-key --kid team-main # Save shared key from your vault
|
|
134005
134304
|
norn secrets audit . # Fail if plaintext secrets are committed
|
|
134006
134305
|
`);
|
|
134007
134306
|
}
|
|
134307
|
+
function formatRegionPatternSummary(pattern) {
|
|
134308
|
+
const { liftedToStage, liftedToRegion, leafSpecific, skippedConnectionStrings } = pattern.summary;
|
|
134309
|
+
const lines = [
|
|
134310
|
+
`Detected ${pattern.cells.length} envs across ${pattern.stages.length} stages x ${pattern.regions.length} regions.`,
|
|
134311
|
+
`Lifted ${liftedToStage} vars to stage templates (${pattern.stages.join(", ")}).`,
|
|
134312
|
+
`Lifted ${liftedToRegion} vars to region templates (${pattern.regions.join(", ")}).`,
|
|
134313
|
+
`Kept ${leafSpecific} vars leaf-specific.`
|
|
134314
|
+
];
|
|
134315
|
+
if (skippedConnectionStrings > 0) {
|
|
134316
|
+
lines.push(`Kept ${skippedConnectionStrings} connection-string var${skippedConnectionStrings === 1 ? "" : "s"} in leaf envs.`);
|
|
134317
|
+
}
|
|
134318
|
+
return lines;
|
|
134319
|
+
}
|
|
134320
|
+
function runNornenvRegionRefactor(filePath, options) {
|
|
134321
|
+
const content = fs19.readFileSync(filePath, "utf-8");
|
|
134322
|
+
const config2 = parseEnvFile(content, filePath);
|
|
134323
|
+
const pattern = detectRegionPattern(config2, content);
|
|
134324
|
+
if (!pattern) {
|
|
134325
|
+
if (options.output === "json") {
|
|
134326
|
+
console.log(JSON.stringify({ success: false, changed: false, error: "No region pattern detected" }, null, 2));
|
|
134327
|
+
} else {
|
|
134328
|
+
console.error("No region pattern detected in this .nornenv file.");
|
|
134329
|
+
}
|
|
134330
|
+
process.exit(1);
|
|
134331
|
+
}
|
|
134332
|
+
const refactored = applyRegionRefactorToText(content, pattern);
|
|
134333
|
+
if (options.writeRefactor) {
|
|
134334
|
+
fs19.writeFileSync(filePath, refactored, "utf-8");
|
|
134335
|
+
}
|
|
134336
|
+
if (options.output === "json") {
|
|
134337
|
+
console.log(JSON.stringify({
|
|
134338
|
+
success: true,
|
|
134339
|
+
changed: refactored !== content,
|
|
134340
|
+
file: filePath,
|
|
134341
|
+
summary: pattern.summary,
|
|
134342
|
+
stages: pattern.stages,
|
|
134343
|
+
regions: pattern.regions,
|
|
134344
|
+
output: options.writeRefactor ? void 0 : refactored
|
|
134345
|
+
}, null, 2));
|
|
134346
|
+
} else if (options.writeRefactor) {
|
|
134347
|
+
for (const line2 of formatRegionPatternSummary(pattern)) {
|
|
134348
|
+
console.log(line2);
|
|
134349
|
+
}
|
|
134350
|
+
console.log(`Refactored ${filePath}`);
|
|
134351
|
+
} else {
|
|
134352
|
+
console.log(refactored);
|
|
134353
|
+
}
|
|
134354
|
+
process.exit(0);
|
|
134355
|
+
}
|
|
134008
134356
|
async function runSingleRequest(fileContent, variables, cookieJar, apiDefinitions, filePath, envContext) {
|
|
134009
134357
|
const lines = fileContent.split("\n");
|
|
134010
134358
|
const requestLines = [];
|
|
@@ -134169,6 +134517,17 @@ async function main() {
|
|
|
134169
134517
|
process.exit(1);
|
|
134170
134518
|
}
|
|
134171
134519
|
const isDirectory = fs19.statSync(inputPath).isDirectory();
|
|
134520
|
+
if (options.writeRefactor && !options.refactorRegionPattern) {
|
|
134521
|
+
console.error("Error: --write can only be used with --refactor-region-pattern");
|
|
134522
|
+
process.exit(1);
|
|
134523
|
+
}
|
|
134524
|
+
if (options.refactorRegionPattern) {
|
|
134525
|
+
if (isDirectory) {
|
|
134526
|
+
console.error("Error: --refactor-region-pattern requires a specific .nornenv file, not a directory");
|
|
134527
|
+
process.exit(1);
|
|
134528
|
+
}
|
|
134529
|
+
runNornenvRegionRefactor(inputPath, options);
|
|
134530
|
+
}
|
|
134172
134531
|
let filesToRun;
|
|
134173
134532
|
if (isDirectory) {
|
|
134174
134533
|
filesToRun = discoverNornFiles(inputPath);
|