norn-cli 1.4.3 → 1.4.4
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/.norn-cache/swagger-body-intellisense.json +1 -1
- package/CHANGELOG.md +25 -0
- package/dist/cli.js +361 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the "Norn" extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.4.4] - 2026-02-24
|
|
6
|
+
|
|
7
|
+
### Improved
|
|
8
|
+
- **Error UX (Extension + CLI)**:
|
|
9
|
+
- Added structured, more actionable request/sequence error messages with environment-aware hints.
|
|
10
|
+
- Improved preflight validation so unresolved variables and invalid URLs are caught before request execution.
|
|
11
|
+
- Standardized error formatting across extension response panel and CLI output.
|
|
12
|
+
|
|
13
|
+
- **Response Panel Error Readability**:
|
|
14
|
+
- Sequence errors/warnings now render as timeline entries (same visual style as execution steps) and appear at the end of the run.
|
|
15
|
+
- Error details are grouped into clearer sections with primary fields surfaced first.
|
|
16
|
+
|
|
17
|
+
- **Diagnostics Coverage and Persistence**:
|
|
18
|
+
- Expanded `.norn` editor diagnostics for malformed statements and invalid command combinations.
|
|
19
|
+
- Added workspace-wide diagnostic refresh so Explorer error badges persist after files are closed.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **Diagnostics False Positives**:
|
|
23
|
+
- Fixed false squiggles for valid request body variable lines (for example `payload` used as a request body).
|
|
24
|
+
- Fixed false squiggles for endpoint calls with parameters (for example `GET MultiParam(hello, world)` and named args).
|
|
25
|
+
- Fixed false squiggles for commented-out empty variable references (`{{}}`) in comment lines.
|
|
26
|
+
|
|
27
|
+
- **Preflight Validation Regression**:
|
|
28
|
+
- Fixed request preflight validation incorrectly blocking some `GET`/`HEAD` requests due to unresolved placeholders detected in parsed body content.
|
|
29
|
+
|
|
5
30
|
## [1.4.3] - 2026-02-22
|
|
6
31
|
|
|
7
32
|
### Improved
|
package/dist/cli.js
CHANGED
|
@@ -26208,6 +26208,29 @@ var CookieJar = class _CookieJar {
|
|
|
26208
26208
|
}
|
|
26209
26209
|
};
|
|
26210
26210
|
|
|
26211
|
+
// src/errors/nornError.ts
|
|
26212
|
+
var NornError = class extends Error {
|
|
26213
|
+
category;
|
|
26214
|
+
code;
|
|
26215
|
+
hint;
|
|
26216
|
+
details;
|
|
26217
|
+
context;
|
|
26218
|
+
cause;
|
|
26219
|
+
constructor(options) {
|
|
26220
|
+
super(options.message);
|
|
26221
|
+
this.name = "NornError";
|
|
26222
|
+
this.category = options.category;
|
|
26223
|
+
this.code = options.code;
|
|
26224
|
+
this.hint = options.hint;
|
|
26225
|
+
this.details = Array.isArray(options.details) ? options.details : options.details ? [options.details] : void 0;
|
|
26226
|
+
this.context = options.context;
|
|
26227
|
+
this.cause = options.cause;
|
|
26228
|
+
}
|
|
26229
|
+
};
|
|
26230
|
+
function isNornError(error) {
|
|
26231
|
+
return error instanceof NornError;
|
|
26232
|
+
}
|
|
26233
|
+
|
|
26211
26234
|
// src/httpClient.ts
|
|
26212
26235
|
var sharedCookieJar = new CookieJar();
|
|
26213
26236
|
function createCookieJar() {
|
|
@@ -26368,8 +26391,41 @@ async function sendRequestWithJar(request, jar, retryOptions) {
|
|
|
26368
26391
|
return result;
|
|
26369
26392
|
} catch (error) {
|
|
26370
26393
|
const errorMessage = error.message || String(error);
|
|
26371
|
-
|
|
26372
|
-
|
|
26394
|
+
const effectiveUrl = currentUrl || request.url;
|
|
26395
|
+
const isUrlError = /invalid url/i.test(errorMessage) || /unsupported protocol/i.test(errorMessage);
|
|
26396
|
+
const isAxiosNetworkError = Boolean(error?.isAxiosError) && !error?.response;
|
|
26397
|
+
if (isUrlError) {
|
|
26398
|
+
lastError = new NornError({
|
|
26399
|
+
category: "url",
|
|
26400
|
+
code: "http-invalid-url",
|
|
26401
|
+
message: `Invalid request URL: ${effectiveUrl}`,
|
|
26402
|
+
details: errorMessage,
|
|
26403
|
+
hint: "Check the URL and any variables used to build it before sending the request.",
|
|
26404
|
+
context: {
|
|
26405
|
+
source: "httpClient",
|
|
26406
|
+
method: currentMethod,
|
|
26407
|
+
url: effectiveUrl
|
|
26408
|
+
},
|
|
26409
|
+
cause: error
|
|
26410
|
+
});
|
|
26411
|
+
} else if (isAxiosNetworkError) {
|
|
26412
|
+
lastError = new NornError({
|
|
26413
|
+
category: "network",
|
|
26414
|
+
code: "http-network-error",
|
|
26415
|
+
message: `Network request failed.`,
|
|
26416
|
+
details: [`${errorMessage}`, `URL used: ${effectiveUrl}`],
|
|
26417
|
+
hint: "Check connectivity, DNS/host availability, TLS settings, and the request URL.",
|
|
26418
|
+
context: {
|
|
26419
|
+
source: "httpClient",
|
|
26420
|
+
method: currentMethod,
|
|
26421
|
+
url: effectiveUrl
|
|
26422
|
+
},
|
|
26423
|
+
cause: error
|
|
26424
|
+
});
|
|
26425
|
+
} else {
|
|
26426
|
+
lastError = new Error(`${errorMessage}
|
|
26427
|
+
URL used: ${effectiveUrl}`);
|
|
26428
|
+
}
|
|
26373
26429
|
if (attempt < totalAttempts) {
|
|
26374
26430
|
const waitMs = backoffMs * attempt;
|
|
26375
26431
|
retriedErrors.push(`Attempt ${attempt}: ${errorMessage}`);
|
|
@@ -26758,6 +26814,202 @@ function parsePropertyAssignment(line2) {
|
|
|
26758
26814
|
};
|
|
26759
26815
|
}
|
|
26760
26816
|
|
|
26817
|
+
// src/errors/formatError.ts
|
|
26818
|
+
function mergeContext(base, overrides) {
|
|
26819
|
+
if (!base && !overrides) {
|
|
26820
|
+
return void 0;
|
|
26821
|
+
}
|
|
26822
|
+
return {
|
|
26823
|
+
...base || {},
|
|
26824
|
+
...overrides || {},
|
|
26825
|
+
environment: {
|
|
26826
|
+
...base?.environment || {},
|
|
26827
|
+
...overrides?.environment || {}
|
|
26828
|
+
}
|
|
26829
|
+
};
|
|
26830
|
+
}
|
|
26831
|
+
function normalizeKnownError(error, context) {
|
|
26832
|
+
if (isNornError(error)) {
|
|
26833
|
+
if (!context) {
|
|
26834
|
+
return error;
|
|
26835
|
+
}
|
|
26836
|
+
return new NornError({
|
|
26837
|
+
category: error.category,
|
|
26838
|
+
code: error.code,
|
|
26839
|
+
message: error.message,
|
|
26840
|
+
hint: error.hint,
|
|
26841
|
+
details: error.details,
|
|
26842
|
+
context: mergeContext(error.context, context),
|
|
26843
|
+
cause: error.cause
|
|
26844
|
+
});
|
|
26845
|
+
}
|
|
26846
|
+
if (error instanceof Error) {
|
|
26847
|
+
const msg = error.message || String(error);
|
|
26848
|
+
if (msg === "No valid HTTP method found") {
|
|
26849
|
+
return new NornError({
|
|
26850
|
+
category: "syntax",
|
|
26851
|
+
code: "request-missing-method",
|
|
26852
|
+
message: "Could not parse request: no valid HTTP method found.",
|
|
26853
|
+
hint: "Start the request with a valid HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS).",
|
|
26854
|
+
context: mergeContext(void 0, context)
|
|
26855
|
+
});
|
|
26856
|
+
}
|
|
26857
|
+
if (msg.startsWith("Unknown endpoint: ")) {
|
|
26858
|
+
const endpointName = msg.slice("Unknown endpoint: ".length).trim();
|
|
26859
|
+
return new NornError({
|
|
26860
|
+
category: "validation",
|
|
26861
|
+
code: "unknown-endpoint",
|
|
26862
|
+
message: `Unknown endpoint '${endpointName}'.`,
|
|
26863
|
+
hint: "Import a .nornapi file that defines this endpoint, or fix the endpoint name.",
|
|
26864
|
+
context: mergeContext(void 0, context)
|
|
26865
|
+
});
|
|
26866
|
+
}
|
|
26867
|
+
if (/invalid url/i.test(msg) || /unsupported protocol/i.test(msg)) {
|
|
26868
|
+
return new NornError({
|
|
26869
|
+
category: "url",
|
|
26870
|
+
code: "invalid-url",
|
|
26871
|
+
message: `Invalid request URL.`,
|
|
26872
|
+
details: msg,
|
|
26873
|
+
hint: "Check the request URL and any substituted variables (for example {{baseUrl}}).",
|
|
26874
|
+
context: mergeContext(void 0, context)
|
|
26875
|
+
});
|
|
26876
|
+
}
|
|
26877
|
+
return error;
|
|
26878
|
+
}
|
|
26879
|
+
return new Error(String(error));
|
|
26880
|
+
}
|
|
26881
|
+
function formatUserFacingError(error, context) {
|
|
26882
|
+
const normalized = normalizeKnownError(error, context);
|
|
26883
|
+
if (!isNornError(normalized)) {
|
|
26884
|
+
if (normalized instanceof Error) {
|
|
26885
|
+
return normalized.message;
|
|
26886
|
+
}
|
|
26887
|
+
return String(normalized);
|
|
26888
|
+
}
|
|
26889
|
+
const lines = [normalized.message];
|
|
26890
|
+
const ctx = normalized.context;
|
|
26891
|
+
if (ctx?.stepIndex !== void 0) {
|
|
26892
|
+
lines.push(`Step: ${ctx.stepIndex}`);
|
|
26893
|
+
}
|
|
26894
|
+
if (ctx?.requestName) {
|
|
26895
|
+
lines.push(`Request: ${ctx.requestName}`);
|
|
26896
|
+
} else if (ctx?.method || ctx?.url) {
|
|
26897
|
+
const requestLine = [ctx.method, ctx.url].filter(Boolean).join(" ");
|
|
26898
|
+
if (requestLine) {
|
|
26899
|
+
lines.push(`Request: ${requestLine}`);
|
|
26900
|
+
}
|
|
26901
|
+
}
|
|
26902
|
+
if (ctx?.filePath) {
|
|
26903
|
+
lines.push(`File: ${ctx.filePath}`);
|
|
26904
|
+
}
|
|
26905
|
+
if (ctx?.environment) {
|
|
26906
|
+
const env3 = ctx.environment;
|
|
26907
|
+
if (env3.hasEnvFile) {
|
|
26908
|
+
const active = env3.activeEnvironment ?? "none";
|
|
26909
|
+
const available = env3.availableEnvironments?.length ? env3.availableEnvironments.join(", ") : "none";
|
|
26910
|
+
lines.push(`Environment: ${active} (available: ${available})`);
|
|
26911
|
+
}
|
|
26912
|
+
}
|
|
26913
|
+
if (normalized.details) {
|
|
26914
|
+
for (const detail of normalized.details) {
|
|
26915
|
+
lines.push(detail);
|
|
26916
|
+
}
|
|
26917
|
+
}
|
|
26918
|
+
if (normalized.hint) {
|
|
26919
|
+
lines.push(`Hint: ${normalized.hint}`);
|
|
26920
|
+
}
|
|
26921
|
+
return lines.join("\n");
|
|
26922
|
+
}
|
|
26923
|
+
|
|
26924
|
+
// src/requestValidation.ts
|
|
26925
|
+
var PLACEHOLDER_REGEX = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
|
|
26926
|
+
function collectUnresolvedPlaceholders(text) {
|
|
26927
|
+
if (!text) {
|
|
26928
|
+
return [];
|
|
26929
|
+
}
|
|
26930
|
+
const names = /* @__PURE__ */ new Set();
|
|
26931
|
+
let match;
|
|
26932
|
+
PLACEHOLDER_REGEX.lastIndex = 0;
|
|
26933
|
+
while ((match = PLACEHOLDER_REGEX.exec(text)) !== null) {
|
|
26934
|
+
names.add(match[1]);
|
|
26935
|
+
}
|
|
26936
|
+
return [...names];
|
|
26937
|
+
}
|
|
26938
|
+
function buildMissingVariableHint(missingVars, context) {
|
|
26939
|
+
const env3 = context?.environment;
|
|
26940
|
+
if (env3?.hasEnvFile && env3.availableEnvironments && env3.availableEnvironments.length > 0 && !env3.activeEnvironment) {
|
|
26941
|
+
return `A .nornenv file was found but no environment is selected. Select one of: ${env3.availableEnvironments.join(", ")}.`;
|
|
26942
|
+
}
|
|
26943
|
+
if (env3?.hasEnvFile && env3.activeEnvironment) {
|
|
26944
|
+
return `Check that ${missingVars.map((v) => `'${v}'`).join(", ")} exist in the active environment '${env3.activeEnvironment}' or as file-level variables.`;
|
|
26945
|
+
}
|
|
26946
|
+
return `Define ${missingVars.length === 1 ? "the variable" : "the variables"} ${missingVars.map((v) => `'${v}'`).join(", ")} before sending the request.`;
|
|
26947
|
+
}
|
|
26948
|
+
function buildRequestContext(parsed, context) {
|
|
26949
|
+
return {
|
|
26950
|
+
...context,
|
|
26951
|
+
method: context?.method || parsed.method,
|
|
26952
|
+
url: context?.url || parsed.url,
|
|
26953
|
+
environment: context?.environment
|
|
26954
|
+
};
|
|
26955
|
+
}
|
|
26956
|
+
function describeUnresolvedLocation(label, vars) {
|
|
26957
|
+
if (vars.length === 0) {
|
|
26958
|
+
return void 0;
|
|
26959
|
+
}
|
|
26960
|
+
return `${label}: unresolved ${vars.length === 1 ? "variable" : "variables"} ${vars.map((v) => `{{${v}}}`).join(", ")}`;
|
|
26961
|
+
}
|
|
26962
|
+
function shouldValidateBodyPlaceholders(parsed) {
|
|
26963
|
+
const method = String(parsed.method || "").toUpperCase();
|
|
26964
|
+
return method !== "GET" && method !== "HEAD";
|
|
26965
|
+
}
|
|
26966
|
+
function validatePreparedRequest(parsed, context) {
|
|
26967
|
+
const urlVars = collectUnresolvedPlaceholders(parsed.url);
|
|
26968
|
+
const headerVars = [...new Set(Object.values(parsed.headers).flatMap((value) => collectUnresolvedPlaceholders(value)))];
|
|
26969
|
+
const bodyVars = shouldValidateBodyPlaceholders(parsed) ? collectUnresolvedPlaceholders(parsed.body) : [];
|
|
26970
|
+
const missingVars = [.../* @__PURE__ */ new Set([...urlVars, ...headerVars, ...bodyVars])];
|
|
26971
|
+
if (missingVars.length > 0) {
|
|
26972
|
+
const details = [
|
|
26973
|
+
describeUnresolvedLocation("URL", urlVars),
|
|
26974
|
+
describeUnresolvedLocation("Headers", headerVars),
|
|
26975
|
+
describeUnresolvedLocation("Body", bodyVars)
|
|
26976
|
+
].filter((d) => Boolean(d));
|
|
26977
|
+
throw new NornError({
|
|
26978
|
+
category: "variable-resolution",
|
|
26979
|
+
code: "unresolved-request-variables",
|
|
26980
|
+
message: `Request could not be prepared: unresolved ${missingVars.length === 1 ? "variable" : "variables"} ${missingVars.map((v) => `{{${v}}}`).join(", ")}.`,
|
|
26981
|
+
details,
|
|
26982
|
+
hint: buildMissingVariableHint(missingVars, context),
|
|
26983
|
+
context: {
|
|
26984
|
+
...buildRequestContext(parsed, context),
|
|
26985
|
+
unresolvedVariables: missingVars
|
|
26986
|
+
}
|
|
26987
|
+
});
|
|
26988
|
+
}
|
|
26989
|
+
if (!parsed.url || !parsed.url.trim()) {
|
|
26990
|
+
throw new NornError({
|
|
26991
|
+
category: "url",
|
|
26992
|
+
code: "missing-request-url",
|
|
26993
|
+
message: "Request URL is missing.",
|
|
26994
|
+
hint: "Add a URL after the HTTP method (for example: GET https://api.example.com/path).",
|
|
26995
|
+
context: buildRequestContext(parsed, context)
|
|
26996
|
+
});
|
|
26997
|
+
}
|
|
26998
|
+
try {
|
|
26999
|
+
new URL(parsed.url);
|
|
27000
|
+
} catch (error) {
|
|
27001
|
+
throw new NornError({
|
|
27002
|
+
category: "url",
|
|
27003
|
+
code: "invalid-request-url",
|
|
27004
|
+
message: `Invalid request URL: ${parsed.url}`,
|
|
27005
|
+
details: error instanceof Error ? error.message : String(error),
|
|
27006
|
+
hint: "Check the URL format and any variables used to build it.",
|
|
27007
|
+
context: buildRequestContext(parsed, context),
|
|
27008
|
+
cause: error
|
|
27009
|
+
});
|
|
27010
|
+
}
|
|
27011
|
+
}
|
|
27012
|
+
|
|
26761
27013
|
// src/sequenceRunner.ts
|
|
26762
27014
|
init_assertionRunner();
|
|
26763
27015
|
function applyHeaderGroupsToRequest(parsed, requestText, headerGroups, variables) {
|
|
@@ -26818,6 +27070,9 @@ function applyHeaderGroupsToRequest(parsed, requestText, headerGroups, variables
|
|
|
26818
27070
|
}
|
|
26819
27071
|
return result;
|
|
26820
27072
|
}
|
|
27073
|
+
function indentMultiline(value, prefix = " ") {
|
|
27074
|
+
return value.split("\n").map((line2) => `${prefix}${line2}`).join("\n");
|
|
27075
|
+
}
|
|
26821
27076
|
function isRunNamedRequestCommand(line2) {
|
|
26822
27077
|
const trimmed = line2.trim();
|
|
26823
27078
|
if (!trimmed.toLowerCase().startsWith("run ")) {
|
|
@@ -27930,7 +28185,7 @@ function getValueByPath(response, path5) {
|
|
|
27930
28185
|
return void 0;
|
|
27931
28186
|
}
|
|
27932
28187
|
}
|
|
27933
|
-
async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, workingDir, fullDocumentText, onProgress, callStack, sequenceArgs, apiDefinitions, tagFilterOptions, sequenceSources) {
|
|
28188
|
+
async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, workingDir, fullDocumentText, onProgress, callStack, sequenceArgs, apiDefinitions, tagFilterOptions, sequenceSources, executionContext) {
|
|
27934
28189
|
const startTime = Date.now();
|
|
27935
28190
|
const responses = [];
|
|
27936
28191
|
const scriptResults = [];
|
|
@@ -27960,6 +28215,16 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
27960
28215
|
}
|
|
27961
28216
|
};
|
|
27962
28217
|
const runtimeVariables = { ...fileVariables, ...sequenceArgs };
|
|
28218
|
+
const buildRequestValidationContext = (stepNumber, method, url2, requestName) => ({
|
|
28219
|
+
source: "sequence",
|
|
28220
|
+
filePath: executionContext?.filePath,
|
|
28221
|
+
sequenceName: executionContext?.sequenceName,
|
|
28222
|
+
stepIndex: stepNumber,
|
|
28223
|
+
method,
|
|
28224
|
+
url: url2,
|
|
28225
|
+
requestName,
|
|
28226
|
+
environment: executionContext?.environment
|
|
28227
|
+
});
|
|
27963
28228
|
let requestIndex = 0;
|
|
27964
28229
|
const ifStack = [];
|
|
27965
28230
|
const shouldSkip = () => ifStack.length > 0 && ifStack.some((v) => !v);
|
|
@@ -28341,6 +28606,10 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
28341
28606
|
}
|
|
28342
28607
|
}
|
|
28343
28608
|
const requestParsed = parserHttpRequest(requestText, runtimeVariables);
|
|
28609
|
+
validatePreparedRequest(
|
|
28610
|
+
requestParsed,
|
|
28611
|
+
buildRequestValidationContext(stepIdx + 1, requestParsed.method, requestParsed.url, `var ${parsed.varName}`)
|
|
28612
|
+
);
|
|
28344
28613
|
const retryOpts = parsed.retryCount ? {
|
|
28345
28614
|
retryCount: parsed.retryCount,
|
|
28346
28615
|
backoffMs: parsed.backoffMs || 1e3,
|
|
@@ -28372,8 +28641,13 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
28372
28641
|
}
|
|
28373
28642
|
}
|
|
28374
28643
|
} catch (error) {
|
|
28375
|
-
|
|
28376
|
-
|
|
28644
|
+
const userMessage = formatUserFacingError(
|
|
28645
|
+
error,
|
|
28646
|
+
buildRequestValidationContext(stepIdx + 1, void 0, void 0, `var ${parsed.varName}`)
|
|
28647
|
+
);
|
|
28648
|
+
let errorDetails = `Request failed for var ${parsed.varName}:
|
|
28649
|
+
${indentMultiline(userMessage)}`;
|
|
28650
|
+
if (requestDescription && !/\nRequest:\s/.test(userMessage)) {
|
|
28377
28651
|
errorDetails += `
|
|
28378
28652
|
Request: ${requestDescription}`;
|
|
28379
28653
|
}
|
|
@@ -28558,8 +28832,9 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
28558
28832
|
// Pass API definitions for endpoint resolution
|
|
28559
28833
|
tagFilterOptions,
|
|
28560
28834
|
// Pass tag filter options for nested sequence filtering
|
|
28561
|
-
sequenceSources
|
|
28835
|
+
sequenceSources,
|
|
28562
28836
|
// Pass sequence sources for nested sequences
|
|
28837
|
+
executionContext
|
|
28563
28838
|
);
|
|
28564
28839
|
reportProgress(stepIdx, "sequenceEnd", `Finished sequence ${targetName}`, void 0, targetName);
|
|
28565
28840
|
for (const subResponse of subResult.responses) {
|
|
@@ -28674,6 +28949,10 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
28674
28949
|
}
|
|
28675
28950
|
requestUrl = requestParsed.url;
|
|
28676
28951
|
requestMethod = requestParsed.method;
|
|
28952
|
+
validatePreparedRequest(
|
|
28953
|
+
requestParsed,
|
|
28954
|
+
buildRequestValidationContext(stepIdx + 1, requestMethod, requestUrl, targetName)
|
|
28955
|
+
);
|
|
28677
28956
|
const effectiveRetryCount = parsed.retryCount ?? requestParsed.retryCount;
|
|
28678
28957
|
const effectiveBackoffMs = parsed.backoffMs ?? requestParsed.backoffMs ?? 1e3;
|
|
28679
28958
|
const retryOpts = effectiveRetryCount ? {
|
|
@@ -28704,8 +28983,13 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
28704
28983
|
}
|
|
28705
28984
|
}
|
|
28706
28985
|
} catch (error) {
|
|
28707
|
-
|
|
28708
|
-
|
|
28986
|
+
const userMessage = formatUserFacingError(
|
|
28987
|
+
error,
|
|
28988
|
+
buildRequestValidationContext(stepIdx + 1, requestMethod || void 0, requestUrl || void 0, targetName)
|
|
28989
|
+
);
|
|
28990
|
+
let errorDetails = `Named request "${targetName}" failed:
|
|
28991
|
+
${indentMultiline(userMessage)}`;
|
|
28992
|
+
if (requestUrl && !/\nRequest:\s/.test(userMessage)) {
|
|
28709
28993
|
errorDetails += `
|
|
28710
28994
|
Request: ${requestMethod} ${requestUrl}`;
|
|
28711
28995
|
}
|
|
@@ -28774,6 +29058,10 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
28774
29058
|
const requestParsed = parserHttpRequest(namedRequest.content, runtimeVariables);
|
|
28775
29059
|
requestUrl = requestParsed.url;
|
|
28776
29060
|
requestMethod = requestParsed.method;
|
|
29061
|
+
validatePreparedRequest(
|
|
29062
|
+
requestParsed,
|
|
29063
|
+
buildRequestValidationContext(stepIdx + 1, requestMethod, requestUrl, `${varName} = run ${sequenceName}`)
|
|
29064
|
+
);
|
|
28777
29065
|
const effectiveRetryCount = parsed.retryCount ?? requestParsed.retryCount;
|
|
28778
29066
|
const effectiveBackoffMs = parsed.backoffMs ?? requestParsed.backoffMs ?? 1e3;
|
|
28779
29067
|
const retryOpts = effectiveRetryCount ? {
|
|
@@ -28811,8 +29099,13 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
28811
29099
|
}
|
|
28812
29100
|
}
|
|
28813
29101
|
} catch (error) {
|
|
28814
|
-
|
|
28815
|
-
|
|
29102
|
+
const userMessage = formatUserFacingError(
|
|
29103
|
+
error,
|
|
29104
|
+
buildRequestValidationContext(stepIdx + 1, requestMethod || void 0, requestUrl || void 0, `${varName} = run ${sequenceName}`)
|
|
29105
|
+
);
|
|
29106
|
+
let errorDetails = `Named request "${sequenceName}" failed:
|
|
29107
|
+
${indentMultiline(userMessage)}`;
|
|
29108
|
+
if (requestUrl && !/\nRequest:\s/.test(userMessage)) {
|
|
28816
29109
|
errorDetails += `
|
|
28817
29110
|
Request: ${requestMethod} ${requestUrl}`;
|
|
28818
29111
|
}
|
|
@@ -28890,8 +29183,9 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
28890
29183
|
// Pass API definitions for endpoint resolution
|
|
28891
29184
|
tagFilterOptions,
|
|
28892
29185
|
// Pass tag filter options for nested sequence filtering
|
|
28893
|
-
sequenceSources
|
|
29186
|
+
sequenceSources,
|
|
28894
29187
|
// Pass sequence sources for nested sequences
|
|
29188
|
+
executionContext
|
|
28895
29189
|
);
|
|
28896
29190
|
reportProgress(stepIdx, "sequenceEnd", `Finished sequence ${sequenceName}`, void 0, sequenceName);
|
|
28897
29191
|
for (const subResponse of subResult.responses) {
|
|
@@ -29030,6 +29324,10 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
29030
29324
|
}
|
|
29031
29325
|
}
|
|
29032
29326
|
const { retryCount, backoffMs } = extractRetryOptions(step.content);
|
|
29327
|
+
validatePreparedRequest(
|
|
29328
|
+
parsed,
|
|
29329
|
+
buildRequestValidationContext(stepIdx + 1, parsed.method, parsed.url)
|
|
29330
|
+
);
|
|
29033
29331
|
const retryOpts = retryCount ?? parsed.retryCount ? {
|
|
29034
29332
|
retryCount: retryCount ?? parsed.retryCount ?? 0,
|
|
29035
29333
|
backoffMs: backoffMs ?? parsed.backoffMs ?? 1e3,
|
|
@@ -29058,8 +29356,13 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
29058
29356
|
}
|
|
29059
29357
|
}
|
|
29060
29358
|
} catch (error) {
|
|
29061
|
-
|
|
29062
|
-
|
|
29359
|
+
const userMessage = formatUserFacingError(
|
|
29360
|
+
error,
|
|
29361
|
+
buildRequestValidationContext(stepIdx + 1)
|
|
29362
|
+
);
|
|
29363
|
+
let errorDetails = `Request ${requestIndex} failed:
|
|
29364
|
+
${indentMultiline(userMessage)}`;
|
|
29365
|
+
if (requestDescription && !/\nRequest:\s/.test(userMessage)) {
|
|
29063
29366
|
errorDetails += `
|
|
29064
29367
|
Request: ${requestDescription}`;
|
|
29065
29368
|
}
|
|
@@ -30396,6 +30699,13 @@ function mergeSecrets(targetNames, targetValues, sourceNames, sourceValues) {
|
|
|
30396
30699
|
targetValues.set(name, value);
|
|
30397
30700
|
}
|
|
30398
30701
|
}
|
|
30702
|
+
function buildCliEnvironmentValidationContext(resolvedEnv, selectedEnv) {
|
|
30703
|
+
return {
|
|
30704
|
+
hasEnvFile: Boolean(resolvedEnv.envFilePath),
|
|
30705
|
+
activeEnvironment: selectedEnv,
|
|
30706
|
+
availableEnvironments: resolvedEnv.availableEnvironments
|
|
30707
|
+
};
|
|
30708
|
+
}
|
|
30399
30709
|
function generateTimestamp() {
|
|
30400
30710
|
const now = /* @__PURE__ */ new Date();
|
|
30401
30711
|
const year = now.getFullYear();
|
|
@@ -30534,7 +30844,7 @@ Examples:
|
|
|
30534
30844
|
norn api-tests.norn --no-redact # Show all data (no redaction)
|
|
30535
30845
|
`);
|
|
30536
30846
|
}
|
|
30537
|
-
async function runSingleRequest(fileContent, variables, cookieJar, apiDefinitions) {
|
|
30847
|
+
async function runSingleRequest(fileContent, variables, cookieJar, apiDefinitions, filePath, envContext) {
|
|
30538
30848
|
const lines = fileContent.split("\n");
|
|
30539
30849
|
const requestLines = [];
|
|
30540
30850
|
let foundRequestLine = false;
|
|
@@ -30602,8 +30912,16 @@ async function runSingleRequest(fileContent, variables, cookieJar, apiDefinition
|
|
|
30602
30912
|
headers: combinedHeaders,
|
|
30603
30913
|
body: apiRequest.body
|
|
30604
30914
|
};
|
|
30915
|
+
validatePreparedRequest(parsed2, {
|
|
30916
|
+
source: "cli",
|
|
30917
|
+
filePath,
|
|
30918
|
+
method: parsed2.method,
|
|
30919
|
+
url: parsed2.url,
|
|
30920
|
+
environment: envContext
|
|
30921
|
+
});
|
|
30605
30922
|
return await sendRequestWithJar(parsed2, cookieJar);
|
|
30606
30923
|
}
|
|
30924
|
+
throw new Error(`Unknown endpoint: ${apiRequest.endpointName}`);
|
|
30607
30925
|
}
|
|
30608
30926
|
}
|
|
30609
30927
|
let parsed = parserHttpRequest(requestContent, variables);
|
|
@@ -30649,6 +30967,13 @@ async function runSingleRequest(fileContent, variables, cookieJar, apiDefinition
|
|
|
30649
30967
|
}
|
|
30650
30968
|
parsed.url = parsed.url.trim();
|
|
30651
30969
|
}
|
|
30970
|
+
validatePreparedRequest(parsed, {
|
|
30971
|
+
source: "cli",
|
|
30972
|
+
filePath,
|
|
30973
|
+
method: parsed.method,
|
|
30974
|
+
url: parsed.url,
|
|
30975
|
+
environment: envContext
|
|
30976
|
+
});
|
|
30652
30977
|
return await sendRequestWithJar(parsed, cookieJar);
|
|
30653
30978
|
}
|
|
30654
30979
|
function discoverNornFiles(dirPath) {
|
|
@@ -30689,7 +31014,7 @@ function formatCaseLabel(params) {
|
|
|
30689
31014
|
});
|
|
30690
31015
|
return `[${parts.join(", ")}]`;
|
|
30691
31016
|
}
|
|
30692
|
-
async function runAllSequences(fileContent, variables, cookieJar, workingDir, apiDefinitions, tagFilterOptions, sequenceSources) {
|
|
31017
|
+
async function runAllSequences(fileContent, variables, cookieJar, workingDir, apiDefinitions, tagFilterOptions, sequenceSources, executionContext) {
|
|
30693
31018
|
const allSequences = extractSequences(fileContent);
|
|
30694
31019
|
const results = [];
|
|
30695
31020
|
const testSequences = allSequences.filter((seq) => seq.isTest);
|
|
@@ -30734,7 +31059,8 @@ async function runAllSequences(fileContent, variables, cookieJar, workingDir, ap
|
|
|
30734
31059
|
caseArgs,
|
|
30735
31060
|
apiDefinitions,
|
|
30736
31061
|
tagFilterOptions,
|
|
30737
|
-
sequenceSources
|
|
31062
|
+
sequenceSources,
|
|
31063
|
+
{ ...executionContext, sequenceName: seq.name }
|
|
30738
31064
|
);
|
|
30739
31065
|
result.name = `${seq.name}${caseLabel}`;
|
|
30740
31066
|
results.push(result);
|
|
@@ -30759,7 +31085,8 @@ async function runAllSequences(fileContent, variables, cookieJar, workingDir, ap
|
|
|
30759
31085
|
defaultArgs,
|
|
30760
31086
|
apiDefinitions,
|
|
30761
31087
|
tagFilterOptions,
|
|
30762
|
-
sequenceSources
|
|
31088
|
+
sequenceSources,
|
|
31089
|
+
{ ...executionContext, sequenceName: seq.name }
|
|
30763
31090
|
);
|
|
30764
31091
|
result.name = seq.name;
|
|
30765
31092
|
results.push(result);
|
|
@@ -30823,6 +31150,7 @@ async function main() {
|
|
|
30823
31150
|
if (options.sequence || options.request) {
|
|
30824
31151
|
const filePath = filesToRun[0];
|
|
30825
31152
|
const resolvedEnv = resolveEnvironmentForPath(filePath, options.env);
|
|
31153
|
+
const envValidationContext = buildCliEnvironmentValidationContext(resolvedEnv, options.env);
|
|
30826
31154
|
if (resolvedEnv.envNotFound) {
|
|
30827
31155
|
console.error(`Error: Environment '${resolvedEnv.envNotFound}' not found in .nornenv`);
|
|
30828
31156
|
console.error(`Available environments: ${resolvedEnv.availableEnvironments.join(", ") || "none"}`);
|
|
@@ -30876,7 +31204,7 @@ ${fileContent}` : fileContent;
|
|
|
30876
31204
|
if (options.verbose) {
|
|
30877
31205
|
console.log(colors.info(`Running request: ${options.request}`));
|
|
30878
31206
|
}
|
|
30879
|
-
const response = await runSingleRequest(targetReq.content, variables, cookieJar, apiDefinitions);
|
|
31207
|
+
const response = await runSingleRequest(targetReq.content, variables, cookieJar, apiDefinitions, filePath, envValidationContext);
|
|
30880
31208
|
if (options.output === "json") {
|
|
30881
31209
|
console.log(JSON.stringify({ success: response.status >= 200 && response.status < 400, results: [response] }, null, 2));
|
|
30882
31210
|
} else {
|
|
@@ -30918,7 +31246,8 @@ ${fileContent}` : fileContent;
|
|
|
30918
31246
|
defaultArgs,
|
|
30919
31247
|
apiDefinitions,
|
|
30920
31248
|
tagFilterOptions,
|
|
30921
|
-
importResult.sequenceSources
|
|
31249
|
+
importResult.sequenceSources,
|
|
31250
|
+
{ filePath, sequenceName: targetSeq.name, environment: envValidationContext }
|
|
30922
31251
|
);
|
|
30923
31252
|
seqResult.name = targetSeq.name;
|
|
30924
31253
|
if (options.output === "json") {
|
|
@@ -30959,6 +31288,7 @@ ${fileContent}` : fileContent;
|
|
|
30959
31288
|
}
|
|
30960
31289
|
for (const filePath of filesToRun) {
|
|
30961
31290
|
const resolvedEnv = resolveEnvironmentForPath(filePath, options.env);
|
|
31291
|
+
const envValidationContext = buildCliEnvironmentValidationContext(resolvedEnv, options.env);
|
|
30962
31292
|
if (resolvedEnv.envNotFound) {
|
|
30963
31293
|
console.error(`Error: Environment '${resolvedEnv.envNotFound}' not found in .nornenv`);
|
|
30964
31294
|
console.error(`Available environments: ${resolvedEnv.availableEnvironments.join(", ") || "none"}`);
|
|
@@ -31011,7 +31341,16 @@ ${fileContent}` : fileContent;
|
|
|
31011
31341
|
\u2501\u2501\u2501 ${relPath} \u2501\u2501\u2501`));
|
|
31012
31342
|
}
|
|
31013
31343
|
const apiDefinitions = (importResult.headerGroups?.length || 0) > 0 || (importResult.endpoints?.length || 0) > 0 ? { headerGroups: importResult.headerGroups || [], endpoints: importResult.endpoints || [] } : void 0;
|
|
31014
|
-
const seqResults = await runAllSequences(
|
|
31344
|
+
const seqResults = await runAllSequences(
|
|
31345
|
+
fileContentWithImports,
|
|
31346
|
+
variables,
|
|
31347
|
+
cookieJar,
|
|
31348
|
+
workingDir,
|
|
31349
|
+
apiDefinitions,
|
|
31350
|
+
tagFilterOptions,
|
|
31351
|
+
importResult.sequenceSources,
|
|
31352
|
+
{ filePath, environment: envValidationContext }
|
|
31353
|
+
);
|
|
31015
31354
|
for (const result2 of seqResults) {
|
|
31016
31355
|
result2.sourceFile = filePath;
|
|
31017
31356
|
allResults.push(result2);
|
|
@@ -31131,7 +31470,8 @@ ${colors.error("Errors:")}`);
|
|
|
31131
31470
|
}
|
|
31132
31471
|
}
|
|
31133
31472
|
main().catch((error) => {
|
|
31134
|
-
|
|
31473
|
+
const message = formatUserFacingError(error, { source: "cli" });
|
|
31474
|
+
console.error("Fatal error:", message);
|
|
31135
31475
|
process.exit(1);
|
|
31136
31476
|
});
|
|
31137
31477
|
/*! Bundled license information:
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "norn-cli",
|
|
3
3
|
"displayName": "Norn - REST Client",
|
|
4
4
|
"description": "A powerful REST client for making HTTP requests with sequences, variables, scripts, and cookie support",
|
|
5
|
-
"version": "1.4.
|
|
5
|
+
"version": "1.4.4",
|
|
6
6
|
"publisher": "Norn-PeterKrustanov",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Peter Krastanov"
|