norn-cli 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +24 -1
- package/dist/cli.js +217 -77
- package/package.json +13 -4
- package/.claude/settings.local.json +0 -18
- package/.claude/skills/norn-social-campaign/SKILL.md +0 -70
- package/out/apiResponseIntellisenseCache.js +0 -394
- package/out/assertionRunner.js +0 -567
- package/out/cacheDir.js +0 -136
- package/out/chatParticipant.js +0 -763
- package/out/cli/colors.js +0 -127
- package/out/cli/formatters/assertion.js +0 -102
- package/out/cli/formatters/index.js +0 -23
- package/out/cli/formatters/response.js +0 -106
- package/out/cli/formatters/summary.js +0 -246
- package/out/cli/redaction.js +0 -237
- package/out/cli/reporters/html.js +0 -689
- package/out/cli/reporters/index.js +0 -22
- package/out/cli/reporters/junit.js +0 -226
- package/out/codeLensProvider.js +0 -351
- package/out/compareContentProvider.js +0 -85
- package/out/completionProvider.js +0 -3739
- package/out/contractAssertionSummary.js +0 -225
- package/out/contractDecorationProvider.js +0 -243
- package/out/coverageCalculator.js +0 -879
- package/out/coveragePanel.js +0 -597
- package/out/debug/breakpointResolver.js +0 -84
- package/out/debug/breakpoints.js +0 -52
- package/out/debug/nornDebugAdapter.js +0 -166
- package/out/debug/nornDebugSession.js +0 -613
- package/out/debug/sequenceLocationIndex.js +0 -77
- package/out/debug/types.js +0 -3
- package/out/deepClone.js +0 -21
- package/out/diagnosticProvider.js +0 -2554
- package/out/environmentParser.js +0 -736
- package/out/environmentProvider.js +0 -544
- package/out/environmentTemplates.js +0 -146
- package/out/errors/formatError.js +0 -113
- package/out/errors/nornError.js +0 -29
- package/out/formUrlEncoded.js +0 -89
- package/out/httpClient.js +0 -348
- package/out/httpRuntimeOptions.js +0 -16
- package/out/importErrors.js +0 -31
- package/out/inlayHintResolver.js +0 -70
- package/out/jsonFileReader.js +0 -323
- package/out/mcpClient.js +0 -193
- package/out/mcpConfig.js +0 -184
- package/out/mcpToolIntellisenseCache.js +0 -96
- package/out/mcpToolSchema.js +0 -50
- package/out/nornConfig.js +0 -132
- package/out/nornHoverProvider.js +0 -124
- package/out/nornInlayHintsProvider.js +0 -191
- package/out/nornPrompt.js +0 -755
- package/out/nornSqlParser.js +0 -286
- package/out/nornapiHoverProvider.js +0 -135
- package/out/nornapiInlayHintsProvider.js +0 -94
- package/out/nornapiParser.js +0 -324
- package/out/nornenvCodeActionProvider.js +0 -101
- package/out/nornenvDecorationProvider.js +0 -239
- package/out/nornenvFoldingProvider.js +0 -63
- package/out/nornenvHoverProvider.js +0 -114
- package/out/nornenvInlayHintsProvider.js +0 -99
- package/out/nornenvLanguageModel.js +0 -187
- package/out/nornenvRegionRefactor.js +0 -267
- package/out/nornsqlHoverProvider.js +0 -95
- package/out/nornsqlInlayHintsProvider.js +0 -114
- package/out/parser.js +0 -839
- package/out/pathAccess.js +0 -28
- package/out/postmanImportPanel.js +0 -732
- package/out/postmanImportPlanner.js +0 -1155
- package/out/postmanImportSidebarView.js +0 -532
- package/out/quotedString.js +0 -35
- package/out/requestPreparation.js +0 -179
- package/out/requestValidation.js +0 -146
- package/out/responsePanel.js +0 -7754
- package/out/schemaGenerator.js +0 -562
- package/out/scriptRunner.js +0 -419
- package/out/secrets/cliSecrets.js +0 -415
- package/out/secrets/crypto.js +0 -105
- package/out/secrets/envFileSecrets.js +0 -177
- package/out/secrets/keyStore.js +0 -259
- package/out/sequenceDeclaration.js +0 -15
- package/out/sequenceRunner.js +0 -3590
- package/out/sqlAdapterRunner.js +0 -122
- package/out/sqlBuiltInAdapters.js +0 -604
- package/out/sqlConfig.js +0 -184
- package/out/starterCatalog.js +0 -554
- package/out/stringUtils.js +0 -25
- package/out/swaggerBodyIntellisenseCache.js +0 -114
- package/out/swaggerParser.js +0 -464
- package/out/testProvider.js +0 -767
- package/out/theoryCaseLoader.js +0 -113
- package/out/validationCache.js +0 -211
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "norn-cli",
|
|
3
3
|
"displayName": "Norn — API Tests in Your Repo",
|
|
4
4
|
"description": "Version-controlled API tests your team can keep. Author and debug HTTP sequences in VS Code, then run the same files in CI.",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.5.0",
|
|
6
6
|
"publisher": "Norn-PeterKrustanov",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Peter Krastanov"
|
|
@@ -415,6 +415,14 @@
|
|
|
415
415
|
"type": "boolean",
|
|
416
416
|
"default": true,
|
|
417
417
|
"description": "Verify SSL/TLS certificates for HTTPS requests and Swagger/OpenAPI fetches. Disable only for local development with self-signed certificates."
|
|
418
|
+
},
|
|
419
|
+
"norn.testExplorer.exclude": {
|
|
420
|
+
"type": "array",
|
|
421
|
+
"default": [],
|
|
422
|
+
"items": {
|
|
423
|
+
"type": "string"
|
|
424
|
+
},
|
|
425
|
+
"description": "Workspace-relative glob patterns for .norn files or folders to hide from the Norn Test Explorer. Useful for negative fixtures, live-only tests, or documentation demos."
|
|
418
426
|
}
|
|
419
427
|
}
|
|
420
428
|
},
|
|
@@ -445,7 +453,7 @@
|
|
|
445
453
|
"watch:esbuild": "node esbuild.js --watch",
|
|
446
454
|
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
|
447
455
|
"package": "npm run check-types && npm run lint && node esbuild.js --production",
|
|
448
|
-
"compile-tests": "tsc -p . --outDir out",
|
|
456
|
+
"compile-tests": "node -e \"require('fs').rmSync('out/test',{recursive:true,force:true})\" && tsc -p . --outDir out",
|
|
449
457
|
"watch-tests": "tsc -p . -w --outDir out",
|
|
450
458
|
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
|
451
459
|
"check-types": "tsc --noEmit",
|
|
@@ -453,7 +461,8 @@
|
|
|
453
461
|
"validate:skills": "node ./scripts/validate-skills.mjs",
|
|
454
462
|
"test": "vscode-test",
|
|
455
463
|
"test:regression": "node ./dist/cli.js ./tests/Regression/ -e prelive",
|
|
456
|
-
"
|
|
464
|
+
"test:prerelease": "npm test && npm run test:regression",
|
|
465
|
+
"publish:npm": "node -e \"const fs=require('fs');const p=require('./package.json');p.name='norn-cli';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n')\" && npm publish; exit_code=$?; node -e \"const fs=require('fs');const p=require('./package.json');p.name='norn';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n')\"; exit $exit_code",
|
|
457
466
|
"publish:vsce": "node -e \"const p=require('./package.json');p.name='norn';require('fs').writeFileSync('package.json',JSON.stringify(p,null,2))\" && npx vsce publish",
|
|
458
467
|
"publish:all": "npm run publish:npm && npm run publish:vsce"
|
|
459
468
|
},
|
|
@@ -485,4 +494,4 @@
|
|
|
485
494
|
"bin": {
|
|
486
495
|
"norn": "./dist/cli.js"
|
|
487
496
|
}
|
|
488
|
-
}
|
|
497
|
+
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm run *)",
|
|
5
|
-
"WebSearch",
|
|
6
|
-
"Bash(git checkout *)",
|
|
7
|
-
"Bash(node ./dist/cli.js tests/Regression/nornenv-templates/extends-resolution.norn -s PrintResolvedValues -e prod_uk)",
|
|
8
|
-
"Bash(node ./dist/cli.js tests/Regression/nornenv-templates/extends-resolution.norn -s PrintResolvedValues -e prod_us)",
|
|
9
|
-
"Bash(node ./dist/cli.js tests/Regression/nornenv-templates/extends-resolution.norn -s PrintResolvedValues -e diamond)",
|
|
10
|
-
"Bash(node ./dist/cli.js tests/Regression/nornenv-templates/extends-resolution.norn -s PrintResolvedValues -e base)",
|
|
11
|
-
"Bash(mv .nornenv .nornenv.bak)",
|
|
12
|
-
"Bash(cp cycle.nornenv .nornenv)",
|
|
13
|
-
"Bash(timeout 10 node ../../../dist/cli.js extends-resolution.norn -s PrintResolvedValues -e c)",
|
|
14
|
-
"Bash(time node ../../../dist/cli.js extends-resolution.norn -s PrintResolvedValues -e c)",
|
|
15
|
-
"Bash(time node /Users/petercrest/Worktable/Projects/vsApi/dist/cli.js extends-resolution.norn -s PrintResolvedValues -e c)"
|
|
16
|
-
]
|
|
17
|
-
}
|
|
18
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
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.
|
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.parseApiResponseRequestLine = parseApiResponseRequestLine;
|
|
37
|
-
exports.parseApiResponseRequestText = parseApiResponseRequestText;
|
|
38
|
-
exports.saveApiResponseShapeForRequest = saveApiResponseShapeForRequest;
|
|
39
|
-
exports.getCachedApiResponseShapeForRequest = getCachedApiResponseShapeForRequest;
|
|
40
|
-
exports.getApiResponseShapeNodeAtPath = getApiResponseShapeNodeAtPath;
|
|
41
|
-
exports.getApiResponseShapeProperties = getApiResponseShapeProperties;
|
|
42
|
-
exports.getApiResponseShapeTypeSummary = getApiResponseShapeTypeSummary;
|
|
43
|
-
exports.parseApiResponseBodyPathForCompletion = parseApiResponseBodyPathForCompletion;
|
|
44
|
-
const crypto = __importStar(require("crypto"));
|
|
45
|
-
const path = __importStar(require("path"));
|
|
46
|
-
const cacheDir_1 = require("./cacheDir");
|
|
47
|
-
const stringUtils_1 = require("./stringUtils");
|
|
48
|
-
const CACHE_VERSION = 1;
|
|
49
|
-
const CACHE_FILE = 'api-response-intellisense.json';
|
|
50
|
-
const MAX_DEPTH = 6;
|
|
51
|
-
const MAX_OBJECT_PROPERTIES = 100;
|
|
52
|
-
const MAX_ARRAY_ITEMS = 20;
|
|
53
|
-
function removeRetryOptions(value) {
|
|
54
|
-
return value
|
|
55
|
-
.replace(/\bretry\s+\d+\b/ig, '')
|
|
56
|
-
.replace(/\bbackoff\s+\d+(?:\.\d+)?\s*(?:s|ms|seconds?|milliseconds?)?\b/ig, '')
|
|
57
|
-
.trim();
|
|
58
|
-
}
|
|
59
|
-
function normalizeQuotedTarget(target) {
|
|
60
|
-
const trimmed = target.trim();
|
|
61
|
-
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
62
|
-
(trimmed.startsWith('\'') && trimmed.endsWith('\''))) {
|
|
63
|
-
return trimmed.slice(1, -1).trim();
|
|
64
|
-
}
|
|
65
|
-
return trimmed;
|
|
66
|
-
}
|
|
67
|
-
function normalizeTargetForIdentity(target) {
|
|
68
|
-
return normalizeQuotedTarget(removeRetryOptions(target)).replace(/\s+/g, ' ').trim();
|
|
69
|
-
}
|
|
70
|
-
function hashTarget(target) {
|
|
71
|
-
return crypto.createHash('sha256').update(target).digest('hex').slice(0, 16);
|
|
72
|
-
}
|
|
73
|
-
function redactSensitiveTargetParts(target) {
|
|
74
|
-
let redacted = target.replace(/(https?:\/\/)([^/\s:@]+):([^/\s@]+)@/ig, '$1***@');
|
|
75
|
-
redacted = redacted.replace(/([?&][^=&\s]*(?:token|secret|password|passwd|pwd|key|auth|signature|sig)[^=&\s]*=)[^&\s]+/ig, '$1***');
|
|
76
|
-
redacted = redacted.replace(/\b(?:bearer|basic)\s+[a-z0-9._~+/=-]+/ig, match => `${match.split(/\s+/)[0]} ***`);
|
|
77
|
-
return redacted;
|
|
78
|
-
}
|
|
79
|
-
function normalizeEnvironment(environment) {
|
|
80
|
-
const trimmed = environment?.trim();
|
|
81
|
-
return trimmed ? trimmed : undefined;
|
|
82
|
-
}
|
|
83
|
-
function normalizeIdentity(identity) {
|
|
84
|
-
const rootPath = (0, cacheDir_1.findProjectRoot)(identity.sourceFile);
|
|
85
|
-
const absoluteSourceFile = path.resolve(identity.sourceFile);
|
|
86
|
-
const relativeSourceFile = path.relative(rootPath, absoluteSourceFile).replace(/\\/g, '/');
|
|
87
|
-
const target = normalizeTargetForIdentity(identity.target);
|
|
88
|
-
return {
|
|
89
|
-
rootPath,
|
|
90
|
-
sourceFile: relativeSourceFile || path.basename(absoluteSourceFile),
|
|
91
|
-
sourceLine: identity.sourceLine,
|
|
92
|
-
method: identity.method.toUpperCase(),
|
|
93
|
-
target,
|
|
94
|
-
targetHash: hashTarget(target),
|
|
95
|
-
environment: normalizeEnvironment(identity.environment)
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
function buildCacheKey(identity) {
|
|
99
|
-
return [
|
|
100
|
-
identity.sourceFile,
|
|
101
|
-
identity.sourceLine === undefined ? '' : String(identity.sourceLine),
|
|
102
|
-
identity.method,
|
|
103
|
-
identity.targetHash,
|
|
104
|
-
identity.environment ?? ''
|
|
105
|
-
].map(encodeURIComponent).join('|');
|
|
106
|
-
}
|
|
107
|
-
function getCachePath(rootPath) {
|
|
108
|
-
return (0, cacheDir_1.getNornCacheFilePath)(rootPath, CACHE_FILE);
|
|
109
|
-
}
|
|
110
|
-
function loadCache(rootPath) {
|
|
111
|
-
return (0, cacheDir_1.loadVersionedJsonCache)({
|
|
112
|
-
cachePath: getCachePath(rootPath),
|
|
113
|
-
version: CACHE_VERSION,
|
|
114
|
-
createDefault: () => ({ version: CACHE_VERSION, entries: {} }),
|
|
115
|
-
isValid: cache => typeof cache.entries === 'object' && cache.entries !== null
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
function saveCache(rootPath, cache) {
|
|
119
|
-
return (0, cacheDir_1.saveVersionedJsonCache)(getCachePath(rootPath), cache, () => !!(0, cacheDir_1.ensureNornCacheDir)(rootPath));
|
|
120
|
-
}
|
|
121
|
-
function parseApiResponseRequestLine(line) {
|
|
122
|
-
const trimmed = (0, stringUtils_1.stripInlineComment)(line).trim();
|
|
123
|
-
const varMatch = trimmed.match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
|
|
124
|
-
if (varMatch) {
|
|
125
|
-
return {
|
|
126
|
-
variableName: varMatch[1],
|
|
127
|
-
method: varMatch[2].toUpperCase(),
|
|
128
|
-
target: normalizeTargetForIdentity(varMatch[3])
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
const requestMatch = trimmed.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
|
|
132
|
-
if (!requestMatch) {
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
return {
|
|
136
|
-
method: requestMatch[1].toUpperCase(),
|
|
137
|
-
target: normalizeTargetForIdentity(requestMatch[2])
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
function parseApiResponseRequestText(requestText) {
|
|
141
|
-
const lines = requestText.split('\n');
|
|
142
|
-
for (let lineOffset = 0; lineOffset < lines.length; lineOffset++) {
|
|
143
|
-
const request = parseApiResponseRequestLine(lines[lineOffset]);
|
|
144
|
-
if (request) {
|
|
145
|
-
return { lineOffset, request };
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
function getCacheableJsonRoot(responseBody) {
|
|
151
|
-
let candidate = responseBody;
|
|
152
|
-
if (typeof candidate === 'string') {
|
|
153
|
-
try {
|
|
154
|
-
candidate = JSON.parse(candidate);
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
return undefined;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (candidate !== null && typeof candidate === 'object') {
|
|
161
|
-
return candidate;
|
|
162
|
-
}
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
function getPrimitiveShape(value) {
|
|
166
|
-
if (value === null) {
|
|
167
|
-
return { type: 'null' };
|
|
168
|
-
}
|
|
169
|
-
switch (typeof value) {
|
|
170
|
-
case 'string':
|
|
171
|
-
return { type: 'string' };
|
|
172
|
-
case 'number':
|
|
173
|
-
return { type: 'number' };
|
|
174
|
-
case 'boolean':
|
|
175
|
-
return { type: 'boolean' };
|
|
176
|
-
default:
|
|
177
|
-
return { type: 'unknown' };
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
function buildShape(value, depth = 0) {
|
|
181
|
-
if (depth >= MAX_DEPTH) {
|
|
182
|
-
if (Array.isArray(value)) {
|
|
183
|
-
return { type: 'array', items: { type: 'unknown' } };
|
|
184
|
-
}
|
|
185
|
-
if (value !== null && typeof value === 'object') {
|
|
186
|
-
return { type: 'object', properties: {} };
|
|
187
|
-
}
|
|
188
|
-
return getPrimitiveShape(value);
|
|
189
|
-
}
|
|
190
|
-
if (Array.isArray(value)) {
|
|
191
|
-
const itemShapes = value
|
|
192
|
-
.slice(0, MAX_ARRAY_ITEMS)
|
|
193
|
-
.map(item => buildShape(item, depth + 1));
|
|
194
|
-
return {
|
|
195
|
-
type: 'array',
|
|
196
|
-
items: mergeShapes(itemShapes)
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
if (value !== null && typeof value === 'object') {
|
|
200
|
-
const properties = {};
|
|
201
|
-
const entries = Object.entries(value).slice(0, MAX_OBJECT_PROPERTIES);
|
|
202
|
-
for (const [name, propertyValue] of entries) {
|
|
203
|
-
properties[name] = buildShape(propertyValue, depth + 1);
|
|
204
|
-
}
|
|
205
|
-
return { type: 'object', properties };
|
|
206
|
-
}
|
|
207
|
-
return getPrimitiveShape(value);
|
|
208
|
-
}
|
|
209
|
-
function mergeObjectShapes(nodes) {
|
|
210
|
-
const propertyNames = new Set();
|
|
211
|
-
for (const node of nodes) {
|
|
212
|
-
for (const name of Object.keys(node.properties ?? {})) {
|
|
213
|
-
propertyNames.add(name);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
const properties = {};
|
|
217
|
-
for (const name of [...propertyNames].slice(0, MAX_OBJECT_PROPERTIES)) {
|
|
218
|
-
const propertyShapes = nodes
|
|
219
|
-
.map(node => node.properties?.[name])
|
|
220
|
-
.filter((node) => Boolean(node));
|
|
221
|
-
properties[name] = mergeShapes(propertyShapes);
|
|
222
|
-
}
|
|
223
|
-
return { type: 'object', properties };
|
|
224
|
-
}
|
|
225
|
-
function mergeArrayShapes(nodes) {
|
|
226
|
-
return {
|
|
227
|
-
type: 'array',
|
|
228
|
-
items: mergeShapes(nodes
|
|
229
|
-
.map(node => node.items)
|
|
230
|
-
.filter((node) => Boolean(node)))
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
function mergeShapes(nodes) {
|
|
234
|
-
if (nodes.length === 0) {
|
|
235
|
-
return { type: 'unknown' };
|
|
236
|
-
}
|
|
237
|
-
const hasNull = nodes.some(node => node.type === 'null');
|
|
238
|
-
const nonNullNodes = nodes.filter(node => node.type !== 'null');
|
|
239
|
-
const effectiveNodes = nonNullNodes.length > 0 ? nonNullNodes : nodes;
|
|
240
|
-
const firstType = effectiveNodes[0].type;
|
|
241
|
-
const sameType = effectiveNodes.every(node => node.type === firstType);
|
|
242
|
-
if (!sameType) {
|
|
243
|
-
return { type: 'unknown', nullable: hasNull || undefined };
|
|
244
|
-
}
|
|
245
|
-
let merged;
|
|
246
|
-
if (firstType === 'object') {
|
|
247
|
-
merged = mergeObjectShapes(effectiveNodes);
|
|
248
|
-
}
|
|
249
|
-
else if (firstType === 'array') {
|
|
250
|
-
merged = mergeArrayShapes(effectiveNodes);
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
merged = { type: firstType };
|
|
254
|
-
}
|
|
255
|
-
if (hasNull && merged.type !== 'null') {
|
|
256
|
-
merged.nullable = true;
|
|
257
|
-
}
|
|
258
|
-
return merged;
|
|
259
|
-
}
|
|
260
|
-
function saveApiResponseShapeForRequest(identity, responseBody) {
|
|
261
|
-
const root = getCacheableJsonRoot(responseBody);
|
|
262
|
-
if (root === undefined) {
|
|
263
|
-
return false;
|
|
264
|
-
}
|
|
265
|
-
const shape = buildShape(root);
|
|
266
|
-
if (shape.type !== 'object' && shape.type !== 'array') {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
const normalizedIdentity = normalizeIdentity(identity);
|
|
270
|
-
const cache = loadCache(normalizedIdentity.rootPath);
|
|
271
|
-
const key = buildCacheKey(normalizedIdentity);
|
|
272
|
-
cache.entries[key] = {
|
|
273
|
-
method: normalizedIdentity.method,
|
|
274
|
-
target: redactSensitiveTargetParts(normalizedIdentity.target),
|
|
275
|
-
targetHash: normalizedIdentity.targetHash,
|
|
276
|
-
sourceFile: normalizedIdentity.sourceFile,
|
|
277
|
-
sourceLine: normalizedIdentity.sourceLine,
|
|
278
|
-
environment: normalizedIdentity.environment,
|
|
279
|
-
cachedAt: new Date().toISOString(),
|
|
280
|
-
shape
|
|
281
|
-
};
|
|
282
|
-
return saveCache(normalizedIdentity.rootPath, cache);
|
|
283
|
-
}
|
|
284
|
-
function findBestEntry(entries, identity) {
|
|
285
|
-
const exactLineEntries = entries.filter(entry => entry.sourceLine === identity.sourceLine);
|
|
286
|
-
const linePreferredEntries = exactLineEntries.length > 0 ? exactLineEntries : entries;
|
|
287
|
-
const exactEnvironment = identity.environment
|
|
288
|
-
? linePreferredEntries.find(entry => entry.environment === identity.environment)
|
|
289
|
-
: undefined;
|
|
290
|
-
if (exactEnvironment) {
|
|
291
|
-
return exactEnvironment;
|
|
292
|
-
}
|
|
293
|
-
const noEnvironment = linePreferredEntries.find(entry => !entry.environment);
|
|
294
|
-
if (noEnvironment) {
|
|
295
|
-
return noEnvironment;
|
|
296
|
-
}
|
|
297
|
-
return linePreferredEntries
|
|
298
|
-
.slice()
|
|
299
|
-
.sort((a, b) => b.cachedAt.localeCompare(a.cachedAt))[0];
|
|
300
|
-
}
|
|
301
|
-
function getCachedApiResponseShapeForRequest(identity) {
|
|
302
|
-
const normalizedIdentity = normalizeIdentity(identity);
|
|
303
|
-
const cache = loadCache(normalizedIdentity.rootPath);
|
|
304
|
-
const exactKey = buildCacheKey(normalizedIdentity);
|
|
305
|
-
const exactEntry = cache.entries[exactKey];
|
|
306
|
-
if (exactEntry) {
|
|
307
|
-
return exactEntry;
|
|
308
|
-
}
|
|
309
|
-
const candidates = Object.values(cache.entries).filter(entry => entry.sourceFile === normalizedIdentity.sourceFile &&
|
|
310
|
-
entry.method === normalizedIdentity.method &&
|
|
311
|
-
entry.targetHash === normalizedIdentity.targetHash);
|
|
312
|
-
return findBestEntry(candidates, normalizedIdentity);
|
|
313
|
-
}
|
|
314
|
-
function isNumericSegment(segment) {
|
|
315
|
-
return /^\d+$/.test(segment);
|
|
316
|
-
}
|
|
317
|
-
function getApiResponseShapeNodeAtPath(shape, pathSegments) {
|
|
318
|
-
let current = shape;
|
|
319
|
-
for (const segment of pathSegments) {
|
|
320
|
-
if (!current) {
|
|
321
|
-
return undefined;
|
|
322
|
-
}
|
|
323
|
-
if (current.type === 'array') {
|
|
324
|
-
current = current.items;
|
|
325
|
-
if (isNumericSegment(segment)) {
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
if (current?.type !== 'object') {
|
|
330
|
-
return undefined;
|
|
331
|
-
}
|
|
332
|
-
current = current.properties?.[segment];
|
|
333
|
-
}
|
|
334
|
-
return current;
|
|
335
|
-
}
|
|
336
|
-
function isPathSafePropertyName(name) {
|
|
337
|
-
return /^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(name);
|
|
338
|
-
}
|
|
339
|
-
function getApiResponseShapeProperties(node) {
|
|
340
|
-
if (!node) {
|
|
341
|
-
return [];
|
|
342
|
-
}
|
|
343
|
-
if (node.type === 'array') {
|
|
344
|
-
const itemProperties = getApiResponseShapeProperties(node.items);
|
|
345
|
-
return [
|
|
346
|
-
{ name: 'count', shape: { type: 'number' } },
|
|
347
|
-
...itemProperties
|
|
348
|
-
];
|
|
349
|
-
}
|
|
350
|
-
if (node.type !== 'object' || !node.properties) {
|
|
351
|
-
return [];
|
|
352
|
-
}
|
|
353
|
-
return Object.entries(node.properties)
|
|
354
|
-
.filter(([name]) => isPathSafePropertyName(name))
|
|
355
|
-
.map(([name, shape]) => ({ name, shape }))
|
|
356
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
357
|
-
}
|
|
358
|
-
function getApiResponseShapeTypeSummary(node) {
|
|
359
|
-
let summary;
|
|
360
|
-
if (node.type === 'array') {
|
|
361
|
-
const itemSummary = node.items ? getApiResponseShapeTypeSummary(node.items) : 'unknown';
|
|
362
|
-
summary = `array<${itemSummary}>`;
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
summary = node.type;
|
|
366
|
-
}
|
|
367
|
-
return node.nullable ? `${summary} | null` : summary;
|
|
368
|
-
}
|
|
369
|
-
function parseApiResponseBodyPathForCompletion(suffix) {
|
|
370
|
-
const pathSuffix = suffix.startsWith('[') ? `.${suffix}` : suffix;
|
|
371
|
-
if (!pathSuffix.startsWith('.')) {
|
|
372
|
-
return undefined;
|
|
373
|
-
}
|
|
374
|
-
const normalized = pathSuffix.replace(/\[(\d+)\]/g, '.$1');
|
|
375
|
-
if (!normalized.startsWith('.')) {
|
|
376
|
-
return undefined;
|
|
377
|
-
}
|
|
378
|
-
const withoutLeadingDot = normalized.slice(1);
|
|
379
|
-
const endsAtSeparator = normalized.endsWith('.');
|
|
380
|
-
const rawParts = withoutLeadingDot.split('.');
|
|
381
|
-
const nonEmptyParts = rawParts.filter(part => part !== '');
|
|
382
|
-
if (endsAtSeparator) {
|
|
383
|
-
return {
|
|
384
|
-
pathSegments: nonEmptyParts,
|
|
385
|
-
partial: ''
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
const partial = nonEmptyParts.at(-1) ?? '';
|
|
389
|
-
return {
|
|
390
|
-
pathSegments: nonEmptyParts.slice(0, -1),
|
|
391
|
-
partial
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
//# sourceMappingURL=apiResponseIntellisenseCache.js.map
|