norn-cli 1.10.4 → 1.10.6

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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  These instructions apply to all conversations about the Norn VS Code extension.
4
4
 
5
+ ## Repo Boundary
6
+
7
+ These instructions apply only inside `/Users/petercrest/Worktable/Projects/vsApi`.
8
+
9
+ - Do **not** apply them to standalone work in sibling repos such as `/Users/petercrest/Worktable/Projects/norn_website`.
10
+ - If a task is website-only, prefer the website repo's local instructions instead.
11
+ - Only use these rules for cross-repo work when the task explicitly includes `vsApi`.
12
+
5
13
  ## CLI Support is Mandatory
6
14
 
7
15
  Every feature implementation must work in both:
package/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ All notable changes to the "Norn" extension will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.10.6] - 2026-03-28
8
+
9
+ ### Fixed
10
+ - **Structured Variable Storage**:
11
+ - Kept structured runtime values as real objects instead of JSON strings so the debugger, assertions, and path access all see the same types.
12
+ - Preserved compatibility for existing string-backed values when resolving paths.
13
+
14
+ ### Changed
15
+ - **Docs**:
16
+ - Updated the advanced website docs to note that structured values stay as objects in memory.
17
+
18
+ ## [1.10.5] - 2026-03-28
19
+
20
+ ### Improved
21
+ - **URL-Encoded Request Bodies (Extension + CLI)**:
22
+ - Added stricter shared validation for `application/x-www-form-urlencoded` request bodies so malformed lines now fail fast with clear preflight errors in both the editor and CLI.
23
+ - Added support for single-line ampersand-delimited form bodies such as `key1=value1&key2=value2`.
24
+ - Preserved raw-request header-group workflows by stripping standalone header-group lines from the request body before validation and send.
25
+
26
+ ### Fixed
27
+ - **Imported Parameterized Sequences**:
28
+ - Fixed `.norn` import resolution so imported helper sequences keep their parameter lists and can be invoked correctly from other files.
29
+
30
+ - **Form Body Syntax Highlighting**:
31
+ - Fixed `.norn` syntax coloring so URL-encoded body lines are highlighted correctly after inline headers in raw request blocks.
32
+
7
33
  ## [1.10.4] - 2026-03-21
8
34
 
9
35
  ### Improved
package/dist/cli.js CHANGED
@@ -102749,8 +102749,9 @@ ${resolvedContent}`);
102749
102749
  });
102750
102750
  } else {
102751
102751
  sequenceSources.set(lowerName, absolutePath);
102752
+ const resolvedDeclaration = substituteVariables(seq.declaration, importedVariables);
102752
102753
  const resolvedContent = substituteVariables(seq.content, importedVariables);
102753
- importedContents.push(`sequence ${seq.name}
102754
+ importedContents.push(`${resolvedDeclaration}
102754
102755
  ${resolvedContent}
102755
102756
  end sequence`);
102756
102757
  }
@@ -102781,17 +102782,20 @@ function extractSequencesFromText(text) {
102781
102782
  let currentSequence = null;
102782
102783
  for (let i = 0; i < lines.length; i++) {
102783
102784
  const line2 = lines[i].trim();
102784
- const sequenceMatch = line2.match(/^sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)$/);
102785
+ const lineWithoutComment = stripInlineComment(line2);
102786
+ const sequenceMatch = lineWithoutComment.match(/^sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(\s*\([^)]*\))?\s*$/);
102785
102787
  if (sequenceMatch) {
102786
102788
  currentSequence = {
102787
102789
  name: sequenceMatch[1],
102790
+ declaration: `sequence ${sequenceMatch[1]}${sequenceMatch[2] || ""}`,
102788
102791
  lines: []
102789
102792
  };
102790
102793
  continue;
102791
102794
  }
102792
- if (line2 === "end sequence" && currentSequence) {
102795
+ if (lineWithoutComment === "end sequence" && currentSequence) {
102793
102796
  sequences.push({
102794
102797
  name: currentSequence.name,
102798
+ declaration: currentSequence.declaration,
102795
102799
  content: currentSequence.lines.join("\n")
102796
102800
  });
102797
102801
  currentSequence = null;
@@ -108597,6 +108601,88 @@ function getVerifyTlsCertificates() {
108597
108601
  return verifyTlsCertificates;
108598
108602
  }
108599
108603
 
108604
+ // src/formUrlEncoded.ts
108605
+ function parseEqualsField(segment) {
108606
+ const eqIndex = segment.indexOf("=");
108607
+ if (eqIndex <= 0) {
108608
+ return null;
108609
+ }
108610
+ return {
108611
+ key: segment.substring(0, eqIndex).trim(),
108612
+ value: segment.substring(eqIndex + 1).trim()
108613
+ };
108614
+ }
108615
+ function isFormUrlEncodedContentType(contentType) {
108616
+ return Boolean(contentType && contentType.toLowerCase().includes("application/x-www-form-urlencoded"));
108617
+ }
108618
+ function parseFormUrlEncodedLines(lines) {
108619
+ const fields = [];
108620
+ const errors = [];
108621
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
108622
+ const line2 = lines[lineIndex].trim();
108623
+ if (!line2) {
108624
+ continue;
108625
+ }
108626
+ const firstEqIndex = line2.indexOf("=");
108627
+ const firstColonIndex = line2.indexOf(":");
108628
+ if (firstEqIndex > 0 && (firstColonIndex === -1 || firstEqIndex < firstColonIndex)) {
108629
+ if (line2.includes("&")) {
108630
+ const segments = line2.split("&").map((segment) => segment.trim());
108631
+ const hasInvalidSegment = segments.some((segment) => !parseEqualsField(segment));
108632
+ if (hasInvalidSegment) {
108633
+ errors.push({
108634
+ lineIndex,
108635
+ line: line2,
108636
+ message: "Ampersand-delimited form body lines must use key=value for every segment."
108637
+ });
108638
+ continue;
108639
+ }
108640
+ for (const segment of segments) {
108641
+ const field2 = parseEqualsField(segment);
108642
+ if (field2) {
108643
+ fields.push(field2);
108644
+ }
108645
+ }
108646
+ continue;
108647
+ }
108648
+ const field = parseEqualsField(line2);
108649
+ if (field) {
108650
+ fields.push(field);
108651
+ continue;
108652
+ }
108653
+ }
108654
+ if (firstColonIndex > 0) {
108655
+ fields.push({
108656
+ key: line2.substring(0, firstColonIndex).trim(),
108657
+ value: line2.substring(firstColonIndex + 1).trim()
108658
+ });
108659
+ continue;
108660
+ }
108661
+ errors.push({
108662
+ lineIndex,
108663
+ line: line2,
108664
+ message: "Expected key=value, key: value, or ampersand-delimited key=value pairs."
108665
+ });
108666
+ }
108667
+ return { fields, errors };
108668
+ }
108669
+ function parseFormUrlEncodedBody(body) {
108670
+ if (!body) {
108671
+ return { fields: [], errors: [] };
108672
+ }
108673
+ return parseFormUrlEncodedLines(body.split("\n"));
108674
+ }
108675
+ function encodeFormUrlEncodedBody(body) {
108676
+ const { fields, errors } = parseFormUrlEncodedBody(body);
108677
+ if (errors.length > 0) {
108678
+ return { encodedBody: "", errors };
108679
+ }
108680
+ return {
108681
+ encodedBody: fields.map((field) => `${encodeURIComponent(field.key)}=${encodeURIComponent(field.value)}`).join("&"),
108682
+ errors: []
108683
+ };
108684
+ }
108685
+
108600
108686
  // src/httpClient.ts
108601
108687
  var sharedCookieJar = new CookieJar();
108602
108688
  var insecureHttpsAgent = new https2.Agent({ rejectUnauthorized: false });
@@ -108661,24 +108747,23 @@ async function sendRequestWithJar(request, jar, retryOptions) {
108661
108747
  (key) => key.toLowerCase() === "content-type"
108662
108748
  );
108663
108749
  const contentTypeValue = contentType ? headers[contentType] : "";
108664
- if (contentTypeValue.includes("application/x-www-form-urlencoded")) {
108665
- const lines = currentBody.split("\n").map((line2) => line2.trim()).filter((line2) => line2);
108666
- const params = lines.map((line2) => {
108667
- if (line2.includes("=")) {
108668
- const eqIndex = line2.indexOf("=");
108669
- const key = line2.substring(0, eqIndex).trim();
108670
- const value = line2.substring(eqIndex + 1).trim();
108671
- return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
108672
- }
108673
- const colonIndex = line2.indexOf(":");
108674
- if (colonIndex > 0) {
108675
- const key = line2.substring(0, colonIndex).trim();
108676
- const value = line2.substring(colonIndex + 1).trim();
108677
- return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
108678
- }
108679
- return encodeURIComponent(line2);
108680
- });
108681
- data = params.join("&");
108750
+ if (isFormUrlEncodedContentType(contentTypeValue)) {
108751
+ const encoded = encodeFormUrlEncodedBody(currentBody);
108752
+ if (encoded.errors.length > 0) {
108753
+ throw new NornError({
108754
+ category: "syntax",
108755
+ code: "invalid-form-urlencoded-body",
108756
+ message: "Request body is not valid application/x-www-form-urlencoded syntax.",
108757
+ details: encoded.errors.map((error) => `Body line ${error.lineIndex + 1}: ${error.message} (${error.line})`),
108758
+ hint: "Use key=value, key: value, or a single line like key1=value1&key2=value2.",
108759
+ context: {
108760
+ source: "httpClient",
108761
+ method: currentMethod,
108762
+ url: currentUrl
108763
+ }
108764
+ });
108765
+ }
108766
+ data = encoded.encodedBody;
108682
108767
  } else if (contentTypeValue.includes("application/json")) {
108683
108768
  try {
108684
108769
  data = JSON.parse(currentBody);
@@ -109333,6 +109418,15 @@ function buildRequestContext(parsed, context) {
109333
109418
  environment: context?.environment
109334
109419
  };
109335
109420
  }
109421
+ function getHeaderValueCaseInsensitive(headers, headerName) {
109422
+ const targetName = headerName.toLowerCase();
109423
+ for (const [name, value] of Object.entries(headers)) {
109424
+ if (name.toLowerCase() === targetName) {
109425
+ return value;
109426
+ }
109427
+ }
109428
+ return void 0;
109429
+ }
109336
109430
  function describeUnresolvedLocation(label, vars) {
109337
109431
  if (vars.length === 0) {
109338
109432
  return void 0;
@@ -109388,6 +109482,20 @@ function validatePreparedRequest(parsed, context) {
109388
109482
  cause: error
109389
109483
  });
109390
109484
  }
109485
+ const contentType = getHeaderValueCaseInsensitive(parsed.headers, "Content-Type");
109486
+ if (isFormUrlEncodedContentType(contentType)) {
109487
+ const { errors } = parseFormUrlEncodedBody(parsed.body);
109488
+ if (errors.length > 0) {
109489
+ throw new NornError({
109490
+ category: "syntax",
109491
+ code: "invalid-form-urlencoded-body",
109492
+ message: "Request could not be prepared: invalid application/x-www-form-urlencoded body syntax.",
109493
+ details: errors.map((error) => `Body line ${error.lineIndex + 1}: ${error.message} (${error.line})`),
109494
+ hint: "Use key=value, key: value, or a single line like key1=value1&key2=value2.",
109495
+ context: buildRequestContext(parsed, context)
109496
+ });
109497
+ }
109498
+ }
109391
109499
  }
109392
109500
 
109393
109501
  // src/sequenceRunner.ts
@@ -110257,13 +110365,17 @@ function applyHeaderGroupsToRequest(parsed, requestText, headerGroups, variables
110257
110365
  const headerGroupNames = headerGroups.map((hg) => hg.name);
110258
110366
  const headerGroupHeaders = {};
110259
110367
  const foundGroups = [];
110368
+ const cleanedBodyLines = [];
110369
+ let sawRequestLine = false;
110370
+ let inBodySection = false;
110260
110371
  for (const line2 of lines) {
110261
110372
  const trimmed = line2.trim();
110262
110373
  if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("var ")) {
110263
110374
  continue;
110264
110375
  }
110265
110376
  const methodMatch = trimmed.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
110266
- if (methodMatch) {
110377
+ if (!sawRequestLine && methodMatch) {
110378
+ sawRequestLine = true;
110267
110379
  const afterMethod = methodMatch[2];
110268
110380
  const tokens = afterMethod.split(/\s+/);
110269
110381
  for (let i = tokens.length - 1; i >= 0; i--) {
@@ -110275,18 +110387,22 @@ function applyHeaderGroupsToRequest(parsed, requestText, headerGroups, variables
110275
110387
  }
110276
110388
  continue;
110277
110389
  }
110278
- if (/^[A-Za-z0-9\-_]+\s*:\s*.+$/.test(trimmed)) {
110390
+ if (!sawRequestLine) {
110279
110391
  continue;
110280
110392
  }
110281
- if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith('"')) {
110282
- continue;
110283
- }
110284
- const potentialGroups = trimmed.split(/\s+/);
110285
- for (const groupName of potentialGroups) {
110286
- if (headerGroupNames.includes(groupName)) {
110287
- foundGroups.push(groupName);
110393
+ if (!inBodySection) {
110394
+ if (/^[A-Za-z0-9\-_]+\s*:\s*.+$/.test(trimmed)) {
110395
+ continue;
110396
+ }
110397
+ const potentialGroups = trimmed.split(/\s+/).filter(Boolean);
110398
+ const matchingGroups = potentialGroups.filter((groupName) => headerGroupNames.includes(groupName));
110399
+ if (matchingGroups.length > 0 && matchingGroups.length === potentialGroups.length) {
110400
+ foundGroups.push(...matchingGroups);
110401
+ continue;
110288
110402
  }
110403
+ inBodySection = true;
110289
110404
  }
110405
+ cleanedBodyLines.push(trimmed);
110290
110406
  }
110291
110407
  for (const groupName of foundGroups) {
110292
110408
  const group = headerGroups.find((hg) => hg.name === groupName);
@@ -110303,7 +110419,8 @@ function applyHeaderGroupsToRequest(parsed, requestText, headerGroups, variables
110303
110419
  modifiedUrl = modifiedUrl.trim();
110304
110420
  const result = {
110305
110421
  ...parsed,
110306
- url: modifiedUrl
110422
+ url: modifiedUrl,
110423
+ body: cleanedBodyLines.length > 0 ? substituteVariables(cleanedBodyLines.join("\n"), variables) : void 0
110307
110424
  };
110308
110425
  if (Object.keys(headerGroupHeaders).length > 0) {
110309
110426
  result.headers = { ...headerGroupHeaders, ...parsed.headers };
@@ -110370,13 +110487,7 @@ function resolveReturnExpression(expr, runtimeVariables) {
110370
110487
  return { key: varName, value: varValue };
110371
110488
  }
110372
110489
  try {
110373
- let current = varValue;
110374
- if (typeof current === "string") {
110375
- try {
110376
- current = JSON.parse(current);
110377
- } catch {
110378
- }
110379
- }
110490
+ let current = parseJsonBackedValue(varValue);
110380
110491
  const normalizedPath = pathPart.replace(/^\.|^\[/, (m) => m === "." ? "" : "[").replace(/\[(\d+)\]/g, ".$1");
110381
110492
  const parts = normalizedPath.split(".").filter((p) => p !== "");
110382
110493
  for (const part of parts) {
@@ -110419,7 +110530,7 @@ function buildParsedNamedRequest(requestContent, runtimeVariables, apiDefinition
110419
110530
  const resolvedParams = {};
110420
110531
  for (const [paramName, paramValue] of Object.entries(apiRequest.params)) {
110421
110532
  if (runtimeVariables[paramValue] !== void 0) {
110422
- resolvedParams[paramName] = String(runtimeVariables[paramValue]);
110533
+ resolvedParams[paramName] = valueToString2(runtimeVariables[paramValue]);
110423
110534
  } else {
110424
110535
  resolvedParams[paramName] = substituteVariables(paramValue, runtimeVariables);
110425
110536
  }
@@ -110726,6 +110837,16 @@ function bindSequenceArguments(params, args, runtimeVariables) {
110726
110837
  }
110727
110838
  return { variables: result };
110728
110839
  }
110840
+ function parseJsonBackedValue(value) {
110841
+ if (typeof value !== "string") {
110842
+ return value;
110843
+ }
110844
+ try {
110845
+ return JSON.parse(value);
110846
+ } catch {
110847
+ return value;
110848
+ }
110849
+ }
110729
110850
  function getVariableValueByPath(path14, variables) {
110730
110851
  const parts = path14.split(".");
110731
110852
  let current = variables;
@@ -110734,9 +110855,8 @@ function getVariableValueByPath(path14, variables) {
110734
110855
  return "";
110735
110856
  }
110736
110857
  if (typeof current === "string") {
110737
- try {
110738
- current = JSON.parse(current);
110739
- } catch {
110858
+ current = parseJsonBackedValue(current);
110859
+ if (typeof current === "string") {
110740
110860
  return "";
110741
110861
  }
110742
110862
  }
@@ -110745,10 +110865,7 @@ function getVariableValueByPath(path14, variables) {
110745
110865
  if (current === null || current === void 0) {
110746
110866
  return "";
110747
110867
  }
110748
- if (typeof current === "object") {
110749
- return JSON.stringify(current);
110750
- }
110751
- return String(current);
110868
+ return current;
110752
110869
  }
110753
110870
  function parseTagFilter(filterStr) {
110754
110871
  const match = filterStr.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)(?:\(([^)]+)\))?$/);
@@ -111080,16 +111197,8 @@ function evaluateValueExpression(expr, runtimeVariables) {
111080
111197
  }
111081
111198
  const varValue = runtimeVariables[varName];
111082
111199
  if (pathPart) {
111083
- let dataToNavigate;
111084
- if (typeof varValue === "object" && varValue !== null) {
111085
- dataToNavigate = varValue;
111086
- } else if (typeof varValue === "string") {
111087
- try {
111088
- dataToNavigate = JSON.parse(varValue);
111089
- } catch {
111090
- return { value: varValue, error: `Cannot parse '${varName}' as JSON for path access` };
111091
- }
111092
- } else {
111200
+ const dataToNavigate = parseJsonBackedValue(varValue);
111201
+ if (typeof dataToNavigate !== "object" || dataToNavigate === null) {
111093
111202
  return { value: String(varValue), error: `Cannot access path on non-object value` };
111094
111203
  }
111095
111204
  const path14 = pathPart.replace(/^\./, "").replace(/\[(\d+)\]/g, ".$1");
@@ -111981,7 +112090,7 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
111981
112090
  duration: Date.now() - startTime
111982
112091
  };
111983
112092
  }
111984
- runtimeVariables[parsed.varName] = JSON.stringify(result.data);
112093
+ runtimeVariables[parsed.varName] = result.data;
111985
112094
  reportProgress(stepIdx, "json", `json: ${parsed.varName} = ${parsed.filePath}`, stepResult);
111986
112095
  continue;
111987
112096
  }
@@ -112014,9 +112123,10 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
112014
112123
  };
112015
112124
  }
112016
112125
  let jsonObj;
112017
- try {
112018
- jsonObj = JSON.parse(runtimeVariables[parsed.varName]);
112019
- } catch {
112126
+ const existingValue = parseJsonBackedValue(runtimeVariables[parsed.varName]);
112127
+ if (typeof existingValue === "object" && existingValue !== null) {
112128
+ jsonObj = existingValue;
112129
+ } else {
112020
112130
  errors.push(`Step ${stepIdx + 1}: Variable '${parsed.varName}' is not a valid JSON object`);
112021
112131
  return {
112022
112132
  name: "",
@@ -112037,12 +112147,9 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
112037
112147
  const pathPart = bareVarMatch[2] || "";
112038
112148
  let value = runtimeVariables[varName];
112039
112149
  if (typeof value === "string") {
112040
- try {
112041
- const parsed2 = JSON.parse(value);
112042
- if (typeof parsed2 === "object" && parsed2 !== null) {
112043
- value = parsed2;
112044
- }
112045
- } catch {
112150
+ const parsedValue = parseJsonBackedValue(value);
112151
+ if (typeof parsedValue === "object" && parsedValue !== null) {
112152
+ value = parsedValue;
112046
112153
  }
112047
112154
  }
112048
112155
  if (pathPart) {
@@ -112074,7 +112181,7 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
112074
112181
  duration: Date.now() - startTime
112075
112182
  };
112076
112183
  }
112077
- runtimeVariables[parsed.varName] = JSON.stringify(jsonObj);
112184
+ runtimeVariables[parsed.varName] = jsonObj;
112078
112185
  reportProgress(stepIdx, "propAssign", `${parsed.varName}.${parsed.propertyPath} = ${parsed.value}`, void 0);
112079
112186
  continue;
112080
112187
  }
@@ -112787,7 +112894,7 @@ ${indentMultiline(userMessage)}`;
112787
112894
  body: response.body,
112788
112895
  headers: response.headers
112789
112896
  };
112790
- runtimeVariables[varName] = JSON.stringify(capturedResponse);
112897
+ runtimeVariables[varName] = capturedResponse;
112791
112898
  const relevantCaptures = captures.filter((c) => c.afterRequest === requestIndex);
112792
112899
  for (const capture of relevantCaptures) {
112793
112900
  const value = getValueByPath(response, capture.path);
@@ -112921,7 +113028,7 @@ ${indentMultiline(userMessage)}`;
112921
113028
  returnedObj[resolved.key] = resolved.value;
112922
113029
  }
112923
113030
  }
112924
- runtimeVariables[varName] = JSON.stringify(returnedObj);
113031
+ runtimeVariables[varName] = returnedObj;
112925
113032
  }
112926
113033
  } else {
112927
113034
  runtimeVariables[varName] = "";
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.10.4",
5
+ "version": "1.10.6",
6
6
  "publisher": "Norn-PeterKrustanov",
7
7
  "author": {
8
8
  "name": "Peter Krastanov"