norn-cli 1.2.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cli.js +240 -125
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to the "Norn" extension will be documented in this file.
4
4
 
5
+ ## [1.3.0] - 2026-02-07
6
+
7
+ ### Added
8
+ - **Retry and Backoff**: Automatically retry failed HTTP requests with configurable backoff
9
+ - Syntax: `GET url retry 3 backoff 200 ms` (retry up to 3 times with 200ms linear backoff)
10
+ - Works with direct URLs, named endpoints, and named requests
11
+ - Supports time units: `ms`, `s`, `seconds`, `milliseconds`
12
+ - Response panel shows retry indicator: 🔄 `retried 2x`
13
+ - CLI shows retry attempts in real-time
14
+ - Retries on 5xx errors, 429 rate limiting, and network failures
15
+
16
+ ### Improved
17
+ - **Syntax highlighting**: Variable references `{{var}}` now highlighted inside print strings
18
+ - **Better pattern recognition**: `test sequence` correctly terminates request blocks in all contexts
19
+
5
20
  ## [1.2.5] - 2026-02-07
6
21
 
7
22
  ### Improved
package/dist/cli.js CHANGED
@@ -13286,6 +13286,31 @@ function stripInlineComment(line2) {
13286
13286
  }
13287
13287
  return line2;
13288
13288
  }
13289
+ function extractRetryOptions(line2) {
13290
+ let cleanedLine = line2;
13291
+ let retryCount;
13292
+ let backoffMs;
13293
+ const retryMatch = line2.match(/\bretry\s+(\d+)\b/i);
13294
+ if (retryMatch) {
13295
+ retryCount = parseInt(retryMatch[1], 10);
13296
+ cleanedLine = cleanedLine.replace(retryMatch[0], "").trim();
13297
+ }
13298
+ const backoffMatch = line2.match(/\bbackoff\s+(\d+(?:\.\d+)?)\s*(s|ms|seconds?|milliseconds?)?\b/i);
13299
+ if (backoffMatch) {
13300
+ const value = parseFloat(backoffMatch[1]);
13301
+ const unit = (backoffMatch[2] || "ms").toLowerCase();
13302
+ if (unit === "s" || unit.startsWith("second")) {
13303
+ backoffMs = value * 1e3;
13304
+ } else {
13305
+ backoffMs = value;
13306
+ }
13307
+ cleanedLine = cleanedLine.replace(backoffMatch[0], "").trim();
13308
+ }
13309
+ if (retryCount !== void 0 && backoffMs === void 0) {
13310
+ backoffMs = 1e3;
13311
+ }
13312
+ return { cleanedLine, retryCount, backoffMs };
13313
+ }
13289
13314
  function extractNamedRequests(text) {
13290
13315
  const lines = text.split("\n");
13291
13316
  const namedRequests = [];
@@ -13441,7 +13466,9 @@ function parserHttpRequest(text, variables = {}) {
13441
13466
  if (requestLineIndex === -1) {
13442
13467
  throw new Error("No valid HTTP method found");
13443
13468
  }
13444
- const requestLine = stripInlineComment(allLines[requestLineIndex].trim());
13469
+ let requestLine = stripInlineComment(allLines[requestLineIndex].trim());
13470
+ const { cleanedLine, retryCount, backoffMs } = extractRetryOptions(requestLine);
13471
+ requestLine = cleanedLine;
13445
13472
  const [method, ...urlParts] = requestLine.split(" ");
13446
13473
  let url2 = urlParts.join(" ");
13447
13474
  if (url2.startsWith('"') && url2.endsWith('"') || url2.startsWith("'") && url2.endsWith("'")) {
@@ -13490,7 +13517,7 @@ function parserHttpRequest(text, variables = {}) {
13490
13517
  body = void 0;
13491
13518
  }
13492
13519
  }
13493
- return { method: method.toUpperCase(), url: url2, headers, body };
13520
+ return { method: method.toUpperCase(), url: url2, headers, body, retryCount, backoffMs };
13494
13521
  }
13495
13522
  function extractImports(text) {
13496
13523
  const lines = text.split("\n");
@@ -19463,114 +19490,157 @@ async function getAllCookies(jar) {
19463
19490
  secure: Boolean(c.secure)
19464
19491
  }));
19465
19492
  }
19466
- async function sendRequest(request) {
19467
- return sendRequestWithJar(request, sharedCookieJar);
19493
+ async function sendRequest(request, retryOptions) {
19494
+ return sendRequestWithJar(request, sharedCookieJar, retryOptions);
19468
19495
  }
19469
- async function sendRequestWithJar(request, jar) {
19470
- const startTime = Date.now();
19471
- const maxRedirects = 10;
19472
- let redirectCount = 0;
19473
- let currentUrl = request.url;
19474
- let currentMethod = request.method;
19475
- let currentBody = request.body;
19476
- let finalResponse = null;
19477
- try {
19478
- while (redirectCount <= maxRedirects) {
19479
- const existingCookies = await jar.getCookies(currentUrl);
19480
- const cookieHeader = existingCookies.map((c) => `${c.key}=${c.value}`).join("; ");
19481
- const headers = { ...request.headers };
19482
- if (cookieHeader) {
19483
- headers["Cookie"] = cookieHeader;
19484
- }
19485
- let data = void 0;
19486
- if (currentBody) {
19487
- const contentType = Object.keys(headers).find(
19488
- (key) => key.toLowerCase() === "content-type"
19489
- );
19490
- const contentTypeValue = contentType ? headers[contentType] : "";
19491
- if (contentTypeValue.includes("application/x-www-form-urlencoded")) {
19492
- const lines = currentBody.split("\n").map((line2) => line2.trim()).filter((line2) => line2);
19493
- const params = lines.map((line2) => {
19494
- if (line2.includes("=")) {
19495
- const eqIndex = line2.indexOf("=");
19496
- const key = line2.substring(0, eqIndex).trim();
19497
- const value = line2.substring(eqIndex + 1).trim();
19498
- return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
19496
+ function sleep(ms) {
19497
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
19498
+ }
19499
+ function shouldRetry(response, error) {
19500
+ if (error) return true;
19501
+ if (response && (response.status >= 500 || response.status === 429)) return true;
19502
+ return false;
19503
+ }
19504
+ async function sendRequestWithJar(request, jar, retryOptions) {
19505
+ const totalAttempts = (retryOptions?.retryCount ?? 0) + 1;
19506
+ const backoffMs = retryOptions?.backoffMs ?? 1e3;
19507
+ const retriedErrors = [];
19508
+ let lastError = null;
19509
+ let attemptsMade = 0;
19510
+ for (let attempt = 1; attempt <= totalAttempts; attempt++) {
19511
+ attemptsMade = attempt;
19512
+ const startTime = Date.now();
19513
+ const maxRedirects = 10;
19514
+ let redirectCount = 0;
19515
+ let currentUrl = request.url;
19516
+ let currentMethod = request.method;
19517
+ let currentBody = request.body;
19518
+ let finalResponse = null;
19519
+ try {
19520
+ while (redirectCount <= maxRedirects) {
19521
+ const existingCookies = await jar.getCookies(currentUrl);
19522
+ const cookieHeader = existingCookies.map((c) => `${c.key}=${c.value}`).join("; ");
19523
+ const headers = { ...request.headers };
19524
+ if (cookieHeader) {
19525
+ headers["Cookie"] = cookieHeader;
19526
+ }
19527
+ let data = void 0;
19528
+ if (currentBody) {
19529
+ const contentType = Object.keys(headers).find(
19530
+ (key) => key.toLowerCase() === "content-type"
19531
+ );
19532
+ const contentTypeValue = contentType ? headers[contentType] : "";
19533
+ if (contentTypeValue.includes("application/x-www-form-urlencoded")) {
19534
+ const lines = currentBody.split("\n").map((line2) => line2.trim()).filter((line2) => line2);
19535
+ const params = lines.map((line2) => {
19536
+ if (line2.includes("=")) {
19537
+ const eqIndex = line2.indexOf("=");
19538
+ const key = line2.substring(0, eqIndex).trim();
19539
+ const value = line2.substring(eqIndex + 1).trim();
19540
+ return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
19541
+ }
19542
+ const colonIndex = line2.indexOf(":");
19543
+ if (colonIndex > 0) {
19544
+ const key = line2.substring(0, colonIndex).trim();
19545
+ const value = line2.substring(colonIndex + 1).trim();
19546
+ return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
19547
+ }
19548
+ return encodeURIComponent(line2);
19549
+ });
19550
+ data = params.join("&");
19551
+ } else if (contentTypeValue.includes("application/json")) {
19552
+ try {
19553
+ data = JSON.parse(currentBody);
19554
+ } catch {
19555
+ data = currentBody;
19499
19556
  }
19500
- const colonIndex = line2.indexOf(":");
19501
- if (colonIndex > 0) {
19502
- const key = line2.substring(0, colonIndex).trim();
19503
- const value = line2.substring(colonIndex + 1).trim();
19504
- return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
19557
+ } else {
19558
+ try {
19559
+ data = JSON.parse(currentBody);
19560
+ } catch {
19561
+ data = currentBody;
19505
19562
  }
19506
- return encodeURIComponent(line2);
19507
- });
19508
- data = params.join("&");
19509
- } else if (contentTypeValue.includes("application/json")) {
19510
- try {
19511
- data = JSON.parse(currentBody);
19512
- } catch {
19513
- data = currentBody;
19514
19563
  }
19515
- } else {
19516
- try {
19517
- data = JSON.parse(currentBody);
19518
- } catch {
19519
- data = currentBody;
19564
+ }
19565
+ const response = await axios_default({
19566
+ method: currentMethod,
19567
+ url: currentUrl,
19568
+ headers,
19569
+ data,
19570
+ timeout: 3e4,
19571
+ maxRedirects: 0,
19572
+ validateStatus: () => true
19573
+ });
19574
+ const setCookieHeader = response.headers["set-cookie"];
19575
+ if (setCookieHeader) {
19576
+ const cookies2 = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
19577
+ for (const cookieStr of cookies2) {
19578
+ try {
19579
+ await jar.setCookie(cookieStr, currentUrl);
19580
+ } catch {
19581
+ }
19520
19582
  }
19521
19583
  }
19522
- }
19523
- const response = await axios_default({
19524
- method: currentMethod,
19525
- url: currentUrl,
19526
- headers,
19527
- data,
19528
- timeout: 3e4,
19529
- maxRedirects: 0,
19530
- // Disable automatic redirects
19531
- validateStatus: () => true
19532
- });
19533
- const setCookieHeader = response.headers["set-cookie"];
19534
- if (setCookieHeader) {
19535
- const cookies2 = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
19536
- for (const cookieStr of cookies2) {
19537
- try {
19538
- await jar.setCookie(cookieStr, currentUrl);
19539
- } catch {
19584
+ if (response.status >= 300 && response.status < 400 && response.headers["location"]) {
19585
+ redirectCount++;
19586
+ const location = response.headers["location"];
19587
+ currentUrl = new URL(location, currentUrl).toString();
19588
+ if (currentMethod !== "GET" && currentMethod !== "HEAD") {
19589
+ currentMethod = "GET";
19590
+ currentBody = void 0;
19540
19591
  }
19592
+ continue;
19541
19593
  }
19594
+ finalResponse = response;
19595
+ break;
19542
19596
  }
19543
- if (response.status >= 300 && response.status < 400 && response.headers["location"]) {
19544
- redirectCount++;
19545
- const location = response.headers["location"];
19546
- currentUrl = new URL(location, currentUrl).toString();
19547
- if (currentMethod !== "GET" && currentMethod !== "HEAD") {
19548
- currentMethod = "GET";
19549
- currentBody = void 0;
19597
+ if (!finalResponse) {
19598
+ throw new Error("Too many redirects");
19599
+ }
19600
+ if (shouldRetry(finalResponse, null) && attempt < totalAttempts) {
19601
+ const waitMs = backoffMs * attempt;
19602
+ const errorMessage = `HTTP ${finalResponse.status} ${finalResponse.statusText}`;
19603
+ retriedErrors.push(`Attempt ${attempt}: ${errorMessage}`);
19604
+ if (retryOptions?.onRetry) {
19605
+ retryOptions.onRetry(attempt, totalAttempts, errorMessage, waitMs);
19550
19606
  }
19607
+ await sleep(waitMs);
19551
19608
  continue;
19552
19609
  }
19553
- finalResponse = response;
19554
- break;
19555
- }
19556
- if (!finalResponse) {
19557
- throw new Error("Too many redirects");
19610
+ const cookies = await getAllCookies(jar);
19611
+ const result = {
19612
+ status: finalResponse.status,
19613
+ statusText: finalResponse.statusText,
19614
+ headers: finalResponse.headers,
19615
+ body: finalResponse.data,
19616
+ duration: Date.now() - startTime,
19617
+ cookies
19618
+ };
19619
+ if (attemptsMade > 1 || totalAttempts > 1) {
19620
+ result.retryInfo = {
19621
+ attemptsMade,
19622
+ totalAttempts,
19623
+ retriedErrors
19624
+ };
19625
+ }
19626
+ return result;
19627
+ } catch (error) {
19628
+ const errorMessage = error.message || String(error);
19629
+ lastError = new Error(`${errorMessage}
19630
+ URL used: ${currentUrl || request.url}`);
19631
+ if (attempt < totalAttempts) {
19632
+ const waitMs = backoffMs * attempt;
19633
+ retriedErrors.push(`Attempt ${attempt}: ${errorMessage}`);
19634
+ if (retryOptions?.onRetry) {
19635
+ retryOptions.onRetry(attempt, totalAttempts, errorMessage, waitMs);
19636
+ }
19637
+ await sleep(waitMs);
19638
+ continue;
19639
+ }
19640
+ throw lastError;
19558
19641
  }
19559
- const cookies = await getAllCookies(jar);
19560
- return {
19561
- status: finalResponse.status,
19562
- statusText: finalResponse.statusText,
19563
- headers: finalResponse.headers,
19564
- body: finalResponse.data,
19565
- duration: Date.now() - startTime,
19566
- cookies
19567
- };
19568
- } catch (error) {
19569
- const originalError = error.message || String(error);
19570
- const urlUsed = currentUrl || request.url;
19571
- throw new Error(`${originalError}
19572
- URL used: ${urlUsed}`);
19573
19642
  }
19643
+ throw lastError || new Error("Request failed");
19574
19644
  }
19575
19645
 
19576
19646
  // src/scriptRunner.ts
@@ -19955,17 +20025,20 @@ function isRunNamedRequestCommand(line2) {
19955
20025
  if (/^(bash|js|powershell)\s+/i.test(afterRun)) {
19956
20026
  return false;
19957
20027
  }
19958
- return /^[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?$/.test(afterRun);
20028
+ return /^[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?(?:\s+retry\s+\d+)?(?:\s+backoff\s+\d+(?:\.\d+)?\s*(?:s|ms|seconds?|milliseconds?)?)?$/i.test(afterRun);
19959
20029
  }
19960
20030
  function parseRunNamedRequestCommand(line2) {
19961
20031
  const trimmed = line2.trim();
19962
- const match = trimmed.match(/^run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(([^)]*)\))?$/i);
20032
+ const { cleanedLine, retryCount, backoffMs } = extractRetryOptions(trimmed);
20033
+ const match = cleanedLine.match(/^run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(([^)]*)\))?$/i);
19963
20034
  if (!match) {
19964
20035
  return null;
19965
20036
  }
19966
20037
  return {
19967
20038
  name: match[1],
19968
- args: parseRunArguments(match[2] || "")
20039
+ args: parseRunArguments(match[2] || ""),
20040
+ retryCount,
20041
+ backoffMs
19969
20042
  };
19970
20043
  }
19971
20044
  function isReturnStatement(line2) {
@@ -20061,7 +20134,7 @@ function parseRunArguments(argsStr) {
20061
20134
  }
20062
20135
  function isVarRunSequenceCommand(line2) {
20063
20136
  const trimmed = line2.trim();
20064
- if (!/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?\s*$/i.test(trimmed)) {
20137
+ if (!/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?(?:\s+retry\s+\d+)?(?:\s+backoff\s+\d+(?:\.\d+)?\s*(?:s|ms|seconds?|milliseconds?)?)?\s*$/i.test(trimmed)) {
20065
20138
  return false;
20066
20139
  }
20067
20140
  const match = trimmed.match(/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+([a-zA-Z_][a-zA-Z0-9_-]*)/i);
@@ -20075,14 +20148,17 @@ function isVarRunSequenceCommand(line2) {
20075
20148
  }
20076
20149
  function parseVarRunSequenceCommand(line2) {
20077
20150
  const trimmed = line2.trim();
20078
- const match = trimmed.match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(([^)]*)\))?\s*$/i);
20151
+ const { cleanedLine, retryCount, backoffMs } = extractRetryOptions(trimmed);
20152
+ const match = cleanedLine.match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(([^)]*)\))?\s*$/i);
20079
20153
  if (!match) {
20080
20154
  return null;
20081
20155
  }
20082
20156
  return {
20083
20157
  varName: match[1],
20084
20158
  sequenceName: match[2],
20085
- args: parseRunArguments(match[3] || "")
20159
+ args: parseRunArguments(match[3] || ""),
20160
+ retryCount,
20161
+ backoffMs
20086
20162
  };
20087
20163
  }
20088
20164
  function bindSequenceArguments(params, args, runtimeVariables) {
@@ -20434,10 +20510,13 @@ function parseVarRequestCommand(line2) {
20434
20510
  if (!match) {
20435
20511
  return null;
20436
20512
  }
20513
+ const { cleanedLine: url2, retryCount, backoffMs } = extractRetryOptions(match[3].trim());
20437
20514
  return {
20438
20515
  varName: match[1],
20439
20516
  method: match[2].toUpperCase(),
20440
- url: match[3].trim()
20517
+ url: url2,
20518
+ retryCount,
20519
+ backoffMs
20441
20520
  };
20442
20521
  }
20443
20522
  function isVarAssignCommand(line2) {
@@ -20668,7 +20747,7 @@ function valueToString2(value) {
20668
20747
  }
20669
20748
  return String(value);
20670
20749
  }
20671
- function sleep(ms) {
20750
+ function sleep2(ms) {
20672
20751
  return new Promise((resolve4) => setTimeout(resolve4, ms));
20673
20752
  }
20674
20753
  function isIfCommand(line2) {
@@ -21147,7 +21226,7 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
21147
21226
  if (step.type === "wait") {
21148
21227
  const durationMs = parseWaitCommand(step.content);
21149
21228
  reportProgress(stepIdx, "wait", `wait: ${durationMs}ms`);
21150
- await sleep(durationMs);
21229
+ await sleep2(durationMs);
21151
21230
  const stepResult = {
21152
21231
  type: "print",
21153
21232
  stepIndex: stepIdx,
@@ -21388,7 +21467,14 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
21388
21467
  requestText += "\n" + headerLines.join("\n");
21389
21468
  }
21390
21469
  const requestParsed = parserHttpRequest(requestText, runtimeVariables);
21391
- const response = cookieJar ? await sendRequestWithJar(requestParsed, cookieJar) : await sendRequest(requestParsed);
21470
+ const retryOpts = parsed.retryCount ? {
21471
+ retryCount: parsed.retryCount,
21472
+ backoffMs: parsed.backoffMs || 1e3,
21473
+ onRetry: (attempt, total, error, waitMs) => {
21474
+ reportProgress(stepIdx, "request", `[Retry ${attempt}/${total}] ${requestDescription} - waiting ${waitMs}ms`, void 0);
21475
+ }
21476
+ } : void 0;
21477
+ const response = cookieJar ? await sendRequestWithJar(requestParsed, cookieJar, retryOpts) : await sendRequest(requestParsed, retryOpts);
21392
21478
  addResponse(response);
21393
21479
  responseIndexToVariable.set(responses.length, parsed.varName);
21394
21480
  runtimeVariables[parsed.varName] = response;
@@ -21641,20 +21727,29 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
21641
21727
  let requestUrl = "";
21642
21728
  let requestMethod = "";
21643
21729
  try {
21644
- const parsed2 = parserHttpRequest(namedRequest.content, runtimeVariables);
21645
- requestUrl = parsed2.url;
21646
- requestMethod = parsed2.method;
21647
- const response = cookieJar ? await sendRequestWithJar(parsed2, cookieJar) : await sendRequest(parsed2);
21730
+ const requestParsed = parserHttpRequest(namedRequest.content, runtimeVariables);
21731
+ requestUrl = requestParsed.url;
21732
+ requestMethod = requestParsed.method;
21733
+ const effectiveRetryCount = parsed.retryCount ?? requestParsed.retryCount;
21734
+ const effectiveBackoffMs = parsed.backoffMs ?? requestParsed.backoffMs ?? 1e3;
21735
+ const retryOpts = effectiveRetryCount ? {
21736
+ retryCount: effectiveRetryCount,
21737
+ backoffMs: effectiveBackoffMs,
21738
+ onRetry: (attempt, total, error, waitMs) => {
21739
+ reportProgress(stepIdx, "request", `[Retry ${attempt}/${total}] run ${targetName} - waiting ${waitMs}ms`, void 0);
21740
+ }
21741
+ } : void 0;
21742
+ const response = cookieJar ? await sendRequestWithJar(requestParsed, cookieJar, retryOpts) : await sendRequest(requestParsed, retryOpts);
21648
21743
  addResponse(response);
21649
21744
  const stepResult = {
21650
21745
  type: "request",
21651
21746
  stepIndex: stepIdx,
21652
21747
  response,
21653
- requestMethod: parsed2.method,
21654
- requestUrl: parsed2.url
21748
+ requestMethod: requestParsed.method,
21749
+ requestUrl: requestParsed.url
21655
21750
  };
21656
21751
  orderedSteps.push(stepResult);
21657
- reportProgress(stepIdx, "namedRequest", `run ${targetName} \u2192 ${parsed2.method} ${parsed2.url}`, stepResult);
21752
+ reportProgress(stepIdx, "namedRequest", `run ${targetName} \u2192 ${requestParsed.method} ${requestParsed.url}`, stepResult);
21658
21753
  const relevantCaptures = captures.filter((c) => c.afterRequest === requestIndex);
21659
21754
  for (const capture of relevantCaptures) {
21660
21755
  const value = getValueByPath(response, capture.path);
@@ -21732,20 +21827,29 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
21732
21827
  let requestUrl = "";
21733
21828
  let requestMethod = "";
21734
21829
  try {
21735
- const parsed2 = parserHttpRequest(namedRequest.content, runtimeVariables);
21736
- requestUrl = parsed2.url;
21737
- requestMethod = parsed2.method;
21738
- const response = cookieJar ? await sendRequestWithJar(parsed2, cookieJar) : await sendRequest(parsed2);
21830
+ const requestParsed = parserHttpRequest(namedRequest.content, runtimeVariables);
21831
+ requestUrl = requestParsed.url;
21832
+ requestMethod = requestParsed.method;
21833
+ const effectiveRetryCount = parsed.retryCount ?? requestParsed.retryCount;
21834
+ const effectiveBackoffMs = parsed.backoffMs ?? requestParsed.backoffMs ?? 1e3;
21835
+ const retryOpts = effectiveRetryCount ? {
21836
+ retryCount: effectiveRetryCount,
21837
+ backoffMs: effectiveBackoffMs,
21838
+ onRetry: (attempt, total, error, waitMs) => {
21839
+ reportProgress(stepIdx, "request", `[Retry ${attempt}/${total}] var ${varName} = run ${sequenceName} - waiting ${waitMs}ms`, void 0);
21840
+ }
21841
+ } : void 0;
21842
+ const response = cookieJar ? await sendRequestWithJar(requestParsed, cookieJar, retryOpts) : await sendRequest(requestParsed, retryOpts);
21739
21843
  addResponse(response);
21740
21844
  const stepResult = {
21741
21845
  type: "request",
21742
21846
  stepIndex: stepIdx,
21743
21847
  response,
21744
- requestMethod: parsed2.method,
21745
- requestUrl: parsed2.url
21848
+ requestMethod: requestParsed.method,
21849
+ requestUrl: requestParsed.url
21746
21850
  };
21747
21851
  orderedSteps.push(stepResult);
21748
- reportProgress(stepIdx, "namedRequest", `var ${varName} = run ${sequenceName} \u2192 ${parsed2.method} ${parsed2.url}`, stepResult);
21852
+ reportProgress(stepIdx, "namedRequest", `var ${varName} = run ${sequenceName} \u2192 ${requestParsed.method} ${requestParsed.url}`, stepResult);
21749
21853
  const capturedResponse = {
21750
21854
  status: response.status,
21751
21855
  statusText: response.statusText,
@@ -21951,7 +22055,15 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
21951
22055
  parsed = parserHttpRequest(step.content, runtimeVariables);
21952
22056
  requestDescription = `${parsed.method} ${parsed.url}`;
21953
22057
  }
21954
- const response = cookieJar ? await sendRequestWithJar(parsed, cookieJar) : await sendRequest(parsed);
22058
+ const { retryCount, backoffMs } = extractRetryOptions(step.content);
22059
+ const retryOpts = retryCount ?? parsed.retryCount ? {
22060
+ retryCount: retryCount ?? parsed.retryCount ?? 0,
22061
+ backoffMs: backoffMs ?? parsed.backoffMs ?? 1e3,
22062
+ onRetry: (attempt, total, error, waitMs) => {
22063
+ reportProgress(stepIdx, "request", `[Retry ${attempt}/${total}] ${requestDescription} - waiting ${waitMs}ms`, void 0);
22064
+ }
22065
+ } : void 0;
22066
+ const response = cookieJar ? await sendRequestWithJar(parsed, cookieJar, retryOpts) : await sendRequest(parsed, retryOpts);
21955
22067
  addResponse(response);
21956
22068
  const stepResult = {
21957
22069
  type: "request",
@@ -22264,12 +22376,14 @@ function formatResponse(response, options) {
22264
22376
  const icon = isSuccess ? colors.checkmark : colors.cross;
22265
22377
  if (method && url2) {
22266
22378
  const displayUrl = redactUrl(url2, redaction);
22379
+ const retryInfo = response.retryInfo && response.retryInfo.attemptsMade > 1 ? ` ${colors.warning(`[retried ${response.retryInfo.attemptsMade - 1}x]`)}` : "";
22267
22380
  lines.push(
22268
- `${prefix}${icon} ${colors.method(method)} ${displayUrl} ${statusColor(`(${response.status} ${response.statusText})`)} ${colors.duration(formatDuration(response.duration))}`
22381
+ `${prefix}${icon} ${colors.method(method)} ${displayUrl} ${statusColor(`(${response.status} ${response.statusText})`)} ${colors.duration(formatDuration(response.duration))}${retryInfo}`
22269
22382
  );
22270
22383
  } else {
22384
+ const retryInfo = response.retryInfo && response.retryInfo.attemptsMade > 1 ? ` ${colors.warning(`[retried ${response.retryInfo.attemptsMade - 1}x]`)}` : "";
22271
22385
  lines.push(
22272
- `${prefix}${statusColor(`${response.status} ${response.statusText}`)} ${colors.duration(`(${formatDuration(response.duration)})`)}`
22386
+ `${prefix}${statusColor(`${response.status} ${response.statusText}`)} ${colors.duration(`(${formatDuration(response.duration)})`)}${retryInfo}`
22273
22387
  );
22274
22388
  }
22275
22389
  if (verbose || showDetails) {
@@ -22319,11 +22433,12 @@ function formatResponseCompact(response, options) {
22319
22433
  const statusColor = getStatusColor(colors, response.status);
22320
22434
  const isSuccess = response.status >= 200 && response.status < 300;
22321
22435
  const icon = isSuccess ? colors.checkmark : colors.cross;
22436
+ const retryInfo = response.retryInfo && response.retryInfo.attemptsMade > 1 ? ` ${colors.warning(`[retried ${response.retryInfo.attemptsMade - 1}x]`)}` : "";
22322
22437
  if (method && url2) {
22323
22438
  const displayUrl = redactUrl(url2, redaction);
22324
- return `${prefix}${icon} ${colors.method(method)} ${displayUrl} ${statusColor(`${response.status}`)} ${colors.duration(formatDuration(response.duration))}`;
22439
+ return `${prefix}${icon} ${colors.method(method)} ${displayUrl} ${statusColor(`${response.status}`)} ${colors.duration(formatDuration(response.duration))}${retryInfo}`;
22325
22440
  }
22326
- return `${prefix}${icon} ${statusColor(`${response.status} ${response.statusText}`)} ${colors.duration(formatDuration(response.duration))}`;
22441
+ return `${prefix}${icon} ${statusColor(`${response.status} ${response.statusText}`)} ${colors.duration(formatDuration(response.duration))}${retryInfo}`;
22327
22442
  }
22328
22443
 
22329
22444
  // src/cli/formatters/assertion.ts
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.2.5",
5
+ "version": "1.3.0",
6
6
  "publisher": "Norn-PeterKrustanov",
7
7
  "author": {
8
8
  "name": "Peter Krastanov"