norn-cli 2.2.2 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.claude/settings.local.json +18 -0
  2. package/.claude/skills/norn-social-campaign/SKILL.md +70 -0
  3. package/CHANGELOG.md +22 -1
  4. package/LICENSE +20 -29
  5. package/README.md +32 -1
  6. package/demos/nornenv-region-refactor/README.md +64 -0
  7. package/demos/nornenv-showcase/README.md +62 -0
  8. package/demos/nornenv-showcase/norn.config.json +16 -0
  9. package/demos/nornenv-showcase/showcase.norn +70 -0
  10. package/demos/nornenv-showcase/showcase.nornapi +26 -0
  11. package/demos/nornenv-showcase/showcase.nornsql +20 -0
  12. package/dist/cli.js +564 -54
  13. package/out/apiResponseIntellisenseCache.js +394 -0
  14. package/out/assertionRunner.js +567 -0
  15. package/out/cacheDir.js +136 -0
  16. package/out/chatParticipant.js +763 -0
  17. package/out/cli/colors.js +127 -0
  18. package/out/cli/formatters/assertion.js +102 -0
  19. package/out/cli/formatters/index.js +23 -0
  20. package/out/cli/formatters/response.js +106 -0
  21. package/out/cli/formatters/summary.js +246 -0
  22. package/out/cli/redaction.js +237 -0
  23. package/out/cli/reporters/html.js +689 -0
  24. package/out/cli/reporters/index.js +22 -0
  25. package/out/cli/reporters/junit.js +226 -0
  26. package/out/codeLensProvider.js +351 -0
  27. package/out/compareContentProvider.js +85 -0
  28. package/out/completionProvider.js +3739 -0
  29. package/out/contractAssertionSummary.js +225 -0
  30. package/out/contractDecorationProvider.js +243 -0
  31. package/out/coverageCalculator.js +879 -0
  32. package/out/coveragePanel.js +597 -0
  33. package/out/debug/breakpointResolver.js +84 -0
  34. package/out/debug/breakpoints.js +52 -0
  35. package/out/debug/nornDebugAdapter.js +166 -0
  36. package/out/debug/nornDebugSession.js +613 -0
  37. package/out/debug/sequenceLocationIndex.js +77 -0
  38. package/out/debug/types.js +3 -0
  39. package/out/deepClone.js +21 -0
  40. package/out/diagnosticProvider.js +2554 -0
  41. package/out/environmentParser.js +736 -0
  42. package/out/environmentProvider.js +544 -0
  43. package/out/environmentTemplates.js +146 -0
  44. package/out/errors/formatError.js +113 -0
  45. package/out/errors/nornError.js +29 -0
  46. package/out/formUrlEncoded.js +89 -0
  47. package/out/httpClient.js +348 -0
  48. package/out/httpRuntimeOptions.js +16 -0
  49. package/out/importErrors.js +31 -0
  50. package/out/inlayHintResolver.js +70 -0
  51. package/out/jsonFileReader.js +323 -0
  52. package/out/mcpClient.js +193 -0
  53. package/out/mcpConfig.js +184 -0
  54. package/out/mcpToolIntellisenseCache.js +96 -0
  55. package/out/mcpToolSchema.js +50 -0
  56. package/out/nornConfig.js +132 -0
  57. package/out/nornHoverProvider.js +124 -0
  58. package/out/nornInlayHintsProvider.js +191 -0
  59. package/out/nornPrompt.js +755 -0
  60. package/out/nornSqlParser.js +286 -0
  61. package/out/nornapiHoverProvider.js +135 -0
  62. package/out/nornapiInlayHintsProvider.js +94 -0
  63. package/out/nornapiParser.js +324 -0
  64. package/out/nornenvCodeActionProvider.js +101 -0
  65. package/out/nornenvDecorationProvider.js +239 -0
  66. package/out/nornenvFoldingProvider.js +63 -0
  67. package/out/nornenvHoverProvider.js +114 -0
  68. package/out/nornenvInlayHintsProvider.js +99 -0
  69. package/out/nornenvLanguageModel.js +187 -0
  70. package/out/nornenvRegionRefactor.js +267 -0
  71. package/out/nornsqlHoverProvider.js +95 -0
  72. package/out/nornsqlInlayHintsProvider.js +114 -0
  73. package/out/parser.js +839 -0
  74. package/out/pathAccess.js +28 -0
  75. package/out/postmanImportPanel.js +732 -0
  76. package/out/postmanImportPlanner.js +1155 -0
  77. package/out/postmanImportSidebarView.js +532 -0
  78. package/out/quotedString.js +35 -0
  79. package/out/requestPreparation.js +179 -0
  80. package/out/requestValidation.js +146 -0
  81. package/out/responsePanel.js +7754 -0
  82. package/out/schemaGenerator.js +562 -0
  83. package/out/scriptRunner.js +419 -0
  84. package/out/secrets/cliSecrets.js +415 -0
  85. package/out/secrets/crypto.js +105 -0
  86. package/out/secrets/envFileSecrets.js +177 -0
  87. package/out/secrets/keyStore.js +259 -0
  88. package/out/sequenceDeclaration.js +15 -0
  89. package/out/sequenceRunner.js +3590 -0
  90. package/out/sqlAdapterRunner.js +122 -0
  91. package/out/sqlBuiltInAdapters.js +604 -0
  92. package/out/sqlConfig.js +184 -0
  93. package/out/starterCatalog.js +554 -0
  94. package/out/stringUtils.js +25 -0
  95. package/out/swaggerBodyIntellisenseCache.js +114 -0
  96. package/out/swaggerParser.js +464 -0
  97. package/out/testProvider.js +767 -0
  98. package/out/theoryCaseLoader.js +113 -0
  99. package/out/validationCache.js +211 -0
  100. package/package.json +38 -11
  101. package/.kanbn/index.md +0 -31
  102. package/.kanbn/tasks/book-first-mentor-session.md +0 -13
  103. package/.kanbn/tasks/decide-what-success-in-a-pilot-looks-like.md +0 -9
  104. package/.kanbn/tasks/do-5-customer-conversations.md +0 -9
  105. package/.kanbn/tasks/finalise-the-one-line-pitch.md +0 -11
  106. package/.kanbn/tasks/interview-script.md +0 -49
  107. package/.kanbn/tasks/make-a-list-of-10-people-to-speak-to.md +0 -11
  108. package/.kanbn/tasks/prepare-your-customer-interview-questions.md +0 -11
  109. package/.kanbn/tasks/recruit-2/342/200/2233-pilot-users.md +0 -9
  110. package/.kanbn/tasks/refine-your-pitch.md +0 -9
  111. package/.kanbn/tasks/use-the-shiplight-website-as-a-template-to-improve-norn-website.md +0 -9
  112. package/.kanbn/tasks/write-down-repeated-wording.md +0 -9
  113. package/.kanbn/tasks/write-the-one-pager.md +0 -27
package/out/parser.js ADDED
@@ -0,0 +1,839 @@
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.extractRetryOptions = extractRetryOptions;
37
+ exports.extractNamedRequests = extractNamedRequests;
38
+ exports.getNamedRequest = getNamedRequest;
39
+ exports.extractVariables = extractVariables;
40
+ exports.extractFileLevelVariables = extractFileLevelVariables;
41
+ exports.attachEnvironmentScope = attachEnvironmentScope;
42
+ exports.copyEnvironmentScope = copyEnvironmentScope;
43
+ exports.substituteVariables = substituteVariables;
44
+ exports.parserHttpRequest = parserHttpRequest;
45
+ exports.getRequestBlockAtLine = getRequestBlockAtLine;
46
+ exports.extractImports = extractImports;
47
+ exports.resolveImports = resolveImports;
48
+ const path = __importStar(require("path"));
49
+ const nornapiParser_1 = require("./nornapiParser");
50
+ const nornSqlParser_1 = require("./nornSqlParser");
51
+ const pathAccess_1 = require("./pathAccess");
52
+ const stringUtils_1 = require("./stringUtils");
53
+ /**
54
+ * Extract retry and backoff options from a request line.
55
+ * Syntax: GET "URL" retry 3 backoff 500 ms
56
+ * Returns the cleaned line (without retry/backoff) and the parsed options.
57
+ */
58
+ function extractRetryOptions(line) {
59
+ let cleanedLine = line;
60
+ let retryCount;
61
+ let backoffMs;
62
+ // Extract retry count: "retry 3" or "retry 3 "
63
+ const retryMatch = line.match(/\bretry\s+(\d+)\b/i);
64
+ if (retryMatch) {
65
+ retryCount = parseInt(retryMatch[1], 10);
66
+ cleanedLine = cleanedLine.replace(retryMatch[0], '').trim();
67
+ }
68
+ // Extract backoff: "backoff 500 ms" or "backoff 2s" or "backoff 1000"
69
+ const backoffMatch = line.match(/\bbackoff\s+(\d+(?:\.\d+)?)\s*(s|ms|seconds?|milliseconds?)?\b/i);
70
+ if (backoffMatch) {
71
+ const value = parseFloat(backoffMatch[1]);
72
+ const unit = (backoffMatch[2] || 'ms').toLowerCase();
73
+ if (unit === 's' || unit.startsWith('second')) {
74
+ backoffMs = value * 1000;
75
+ }
76
+ else {
77
+ backoffMs = value;
78
+ }
79
+ cleanedLine = cleanedLine.replace(backoffMatch[0], '').trim();
80
+ }
81
+ // Default backoff if retry specified but not backoff
82
+ if (retryCount !== undefined && backoffMs === undefined) {
83
+ backoffMs = 1000; // Default 1 second
84
+ }
85
+ return { cleanedLine, retryCount, backoffMs };
86
+ }
87
+ /**
88
+ * Extracts all named requests from the document.
89
+ * Named requests are declared as:
90
+ * [RequestName] (no spaces allowed)
91
+ * or legacy: [Name: RequestName]
92
+ * HTTP_METHOD URL
93
+ * ...
94
+ */
95
+ function extractNamedRequests(text) {
96
+ const lines = text.split('\n');
97
+ const namedRequests = [];
98
+ // Match [SomeName] or [Name: SomeName] - name must not contain spaces
99
+ const nameRegex = /^\[(?:Name:\s*)?([a-zA-Z_][a-zA-Z0-9_-]*)\]$/;
100
+ const methodRegex = /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i;
101
+ for (let i = 0; i < lines.length; i++) {
102
+ const line = lines[i].trim();
103
+ const nameMatch = line.match(nameRegex);
104
+ if (nameMatch) {
105
+ const name = nameMatch[1].trim();
106
+ const contentStartLine = i + 1;
107
+ // Find the end of this request (next [Name], sequence, ###, or end of file)
108
+ let endLine = lines.length - 1;
109
+ for (let j = contentStartLine; j < lines.length; j++) {
110
+ const scanLine = lines[j].trim();
111
+ if (isNamedRequestDeclarationLine(scanLine) ||
112
+ isSequenceStartDeclaration(scanLine) ||
113
+ isSequenceEndDeclaration(scanLine) ||
114
+ scanLine.startsWith('###')) {
115
+ endLine = j - 1;
116
+ break;
117
+ }
118
+ }
119
+ // Trim trailing empty lines
120
+ while (endLine > contentStartLine && lines[endLine].trim() === '') {
121
+ endLine--;
122
+ }
123
+ const content = lines.slice(contentStartLine, endLine + 1).join('\n');
124
+ // Only add if there's actual content with an HTTP method
125
+ if (content.split('\n').some(l => methodRegex.test(l.trim()))) {
126
+ namedRequests.push({
127
+ name,
128
+ content,
129
+ startLine: i,
130
+ endLine
131
+ });
132
+ }
133
+ // Skip to end of this request
134
+ i = endLine;
135
+ }
136
+ }
137
+ return namedRequests;
138
+ }
139
+ /**
140
+ * Gets a named request by its name
141
+ */
142
+ function getNamedRequest(text, name) {
143
+ const requests = extractNamedRequests(text);
144
+ return requests.find(r => r.name === name ||
145
+ r.name.toLowerCase() === name.toLowerCase());
146
+ }
147
+ /**
148
+ * Patterns for runtime-computed variable values that should not be treated as static vars.
149
+ */
150
+ const RUNTIME_VARIABLE_VALUE_PATTERNS = [
151
+ /^run\s+/i, // var x = run ... (scripts/sequences)
152
+ /^\$\d+/, // var x = $1... (response captures)
153
+ /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i // var x = METHOD url (request captures)
154
+ ];
155
+ function isRuntimeComputedVariableValue(value) {
156
+ return RUNTIME_VARIABLE_VALUE_PATTERNS.some(pattern => pattern.test(value));
157
+ }
158
+ function isSequenceStartDeclaration(line) {
159
+ return /^(?:test\s+)?sequence\s+[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?\s*$/i.test(line);
160
+ }
161
+ function isSequenceEndDeclaration(line) {
162
+ return /^end\s+sequence\s*$/i.test(line);
163
+ }
164
+ /**
165
+ * Extracts all STATIC variables from the document.
166
+ * Variables are declared as: var variableName = value
167
+ *
168
+ * Does NOT extract runtime-computed values:
169
+ * - var x = run ... (script commands)
170
+ * - var x = $1... (response captures)
171
+ * - var x = GET/POST/... url (request captures)
172
+ */
173
+ function extractVariables(text) {
174
+ const variables = {};
175
+ // Allow optional leading whitespace for indented variables (inside sequences)
176
+ const variableRegex = /^\s*var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/gm;
177
+ let match;
178
+ while ((match = variableRegex.exec(text)) !== null) {
179
+ let value = match[2].trim();
180
+ // Skip runtime-computed values
181
+ const isRuntimeValue = isRuntimeComputedVariableValue(value);
182
+ if (isRuntimeValue) {
183
+ continue;
184
+ }
185
+ // Strip quotes from string literals
186
+ if ((value.startsWith('"') && value.endsWith('"')) ||
187
+ (value.startsWith("'") && value.endsWith("'"))) {
188
+ value = value.slice(1, -1);
189
+ }
190
+ variables[match[1]] = value;
191
+ }
192
+ return variables;
193
+ }
194
+ /**
195
+ * Extracts only file-level variables (outside sequences).
196
+ * Used for diagnostics to understand variable scope.
197
+ */
198
+ function extractFileLevelVariables(text) {
199
+ const variables = {};
200
+ const lines = text.split('\n');
201
+ const variableRegex = /^\s*var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
202
+ // Use depth so malformed files don't accidentally leak inner vars to file scope.
203
+ let sequenceDepth = 0;
204
+ for (const line of lines) {
205
+ const trimmed = line.trim();
206
+ // Track sequence boundaries
207
+ if (isSequenceStartDeclaration(trimmed)) {
208
+ sequenceDepth++;
209
+ continue;
210
+ }
211
+ if (isSequenceEndDeclaration(trimmed)) {
212
+ sequenceDepth = Math.max(0, sequenceDepth - 1);
213
+ continue;
214
+ }
215
+ // Only extract variables outside sequences
216
+ if (sequenceDepth === 0) {
217
+ const match = line.match(variableRegex);
218
+ if (match) {
219
+ let value = match[2].trim();
220
+ // Skip runtime-computed values
221
+ if (isRuntimeComputedVariableValue(value)) {
222
+ continue;
223
+ }
224
+ // Strip quotes from string literals
225
+ if ((value.startsWith('"') && value.endsWith('"')) ||
226
+ (value.startsWith("'") && value.endsWith("'"))) {
227
+ value = value.slice(1, -1);
228
+ }
229
+ variables[match[1]] = value;
230
+ }
231
+ }
232
+ }
233
+ return variables;
234
+ }
235
+ /**
236
+ * Gets a value from an object using a dot-notation path.
237
+ * Supports array indexing with brackets: obj.array[0].property
238
+ */
239
+ function getNestedValue(obj, path) {
240
+ return (0, pathAccess_1.getNestedPathValue)(obj, path);
241
+ }
242
+ /**
243
+ * Converts a value to a string for use in variable substitution.
244
+ */
245
+ function valueToString(value) {
246
+ if (value === null) {
247
+ return 'null';
248
+ }
249
+ if (value === undefined) {
250
+ return '';
251
+ }
252
+ if (typeof value === 'object') {
253
+ return JSON.stringify(value);
254
+ }
255
+ return String(value);
256
+ }
257
+ const ENV_SCOPE_KEY = '$env';
258
+ function attachEnvironmentScope(variables, envVariables) {
259
+ if (!envVariables) {
260
+ return variables;
261
+ }
262
+ Object.defineProperty(variables, ENV_SCOPE_KEY, {
263
+ value: envVariables,
264
+ enumerable: false,
265
+ configurable: true,
266
+ writable: false
267
+ });
268
+ return variables;
269
+ }
270
+ function copyEnvironmentScope(target, source) {
271
+ const envScope = source[ENV_SCOPE_KEY];
272
+ if (envScope && typeof envScope === 'object') {
273
+ attachEnvironmentScope(target, envScope);
274
+ }
275
+ return target;
276
+ }
277
+ /**
278
+ * Substitutes variables in text.
279
+ * Variables are referenced as: {{variableName}} or {{variableName.property.path}}
280
+ * Supports nested property access for JSON objects stored as strings.
281
+ * Also supports object values directly (e.g., from var user = GET responses).
282
+ * Also supports $N response references (e.g., {{$1.status}}, {{$2.body.id}})
283
+ * Also supports explicit environment references (e.g., {{$env.baseUrl}})
284
+ */
285
+ function substituteVariables(text, variables) {
286
+ // Match {{varName}} or {{varName.path.to.property}} or {{varName[0].property}}
287
+ // Also match {{$N}} / {{$N.path}} and {{$env.name}}
288
+ return text.replace(/\{\{(\$env|\$\d+|[a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_-]*|\[\d+\])*)\}\}/g, (match, varName, pathPart) => {
289
+ if (!(varName in variables)) {
290
+ return match; // Variable not found, return original
291
+ }
292
+ const value = variables[varName];
293
+ // If value is already an object (e.g., HttpResponse), navigate directly
294
+ if (typeof value === 'object' && value !== null) {
295
+ if (pathPart) {
296
+ const path = pathPart.replace(/^\./, ''); // Remove leading dot
297
+ const nestedValue = getNestedValue(value, path);
298
+ if (nestedValue === undefined && varName === ENV_SCOPE_KEY) {
299
+ return match;
300
+ }
301
+ return valueToString(nestedValue);
302
+ }
303
+ if (varName === ENV_SCOPE_KEY) {
304
+ return match;
305
+ }
306
+ // No path, stringify the whole object
307
+ return valueToString(value);
308
+ }
309
+ // If there's a path and value is a string, try to parse as JSON and navigate
310
+ if (pathPart && typeof value === 'string') {
311
+ try {
312
+ const parsed = JSON.parse(value);
313
+ const path = pathPart.replace(/^\./, ''); // Remove leading dot
314
+ const nestedValue = getNestedValue(parsed, path);
315
+ if (nestedValue === undefined && varName === ENV_SCOPE_KEY) {
316
+ return match;
317
+ }
318
+ return valueToString(nestedValue);
319
+ }
320
+ catch {
321
+ if (varName === ENV_SCOPE_KEY) {
322
+ return match;
323
+ }
324
+ // Not valid JSON, return original value
325
+ return value;
326
+ }
327
+ }
328
+ // Simple substitution
329
+ return String(value);
330
+ });
331
+ }
332
+ /**
333
+ * Extracts a single request block from the text.
334
+ * Stops parsing at ### delimiter (REST Client style separator).
335
+ * A blank line separates headers from body (standard HTTP convention).
336
+ */
337
+ function parserHttpRequest(text, variables = {}) {
338
+ // Substitute variables first
339
+ const substitutedText = substituteVariables(text, variables);
340
+ // Split by ### to get only the first request block
341
+ const requestBlock = substitutedText.split(/^###/m)[0];
342
+ const allLines = requestBlock.split('\n');
343
+ // Find the first line that looks like a request (METHOD URL)
344
+ const methodRegex = /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i;
345
+ let requestLineIndex = -1;
346
+ for (let i = 0; i < allLines.length; i++) {
347
+ const trimmed = allLines[i].trim();
348
+ if (methodRegex.test(trimmed)) {
349
+ requestLineIndex = i;
350
+ break;
351
+ }
352
+ }
353
+ if (requestLineIndex === -1) {
354
+ throw new Error('No valid HTTP method found');
355
+ }
356
+ // Strip inline comments from the request line before parsing
357
+ let requestLine = (0, stringUtils_1.stripInlineComment)(allLines[requestLineIndex].trim());
358
+ // Extract retry/backoff options before parsing method/URL
359
+ const { cleanedLine, retryCount, backoffMs } = extractRetryOptions(requestLine);
360
+ requestLine = cleanedLine;
361
+ const [method, ...urlParts] = requestLine.split(' ');
362
+ let url = urlParts.join(' ');
363
+ // Handle quoted URLs - remove the quotes
364
+ if ((url.startsWith('"') && url.endsWith('"')) || (url.startsWith("'") && url.endsWith("'"))) {
365
+ url = url.slice(1, -1);
366
+ }
367
+ const headers = {};
368
+ let bodyStartIndex = -1;
369
+ let foundBlankLine = false;
370
+ // Parse headers (lines after request line until blank line or non-header)
371
+ // Headers must have format "Name: Value" where Name is a valid token
372
+ const headerRegex = /^([A-Za-z0-9\-_]+)\s*:\s*(.+)$/;
373
+ for (let i = requestLineIndex + 1; i < allLines.length; i++) {
374
+ const line = allLines[i].trim();
375
+ // Skip comment lines and variable declarations in header section
376
+ if (line.startsWith('#') || line.startsWith('var ')) {
377
+ continue;
378
+ }
379
+ // Blank line marks end of headers
380
+ if (line === '') {
381
+ foundBlankLine = true;
382
+ continue;
383
+ }
384
+ // If we've seen a blank line, everything after is body
385
+ if (foundBlankLine) {
386
+ bodyStartIndex = i;
387
+ break;
388
+ }
389
+ const headerMatch = line.match(headerRegex);
390
+ if (headerMatch) {
391
+ headers[headerMatch[1]] = headerMatch[2].trim();
392
+ }
393
+ else {
394
+ // Not a header format, must be start of body
395
+ bodyStartIndex = i;
396
+ break;
397
+ }
398
+ }
399
+ // Extract body - join remaining non-empty, non-comment lines
400
+ // Stop at the next Norn block boundary so later declarations do not leak into the body.
401
+ let body;
402
+ if (bodyStartIndex > 0) {
403
+ const bodyLines = [];
404
+ for (let i = bodyStartIndex; i < allLines.length; i++) {
405
+ const line = allLines[i].trim();
406
+ // Stop at request boundaries
407
+ if (isRequestLine(line) ||
408
+ isNamedRequestDeclarationLine(line) ||
409
+ isSequenceLine(line) ||
410
+ isEndSequenceLine(line) ||
411
+ line.startsWith('###')) {
412
+ break;
413
+ }
414
+ // Include non-empty, non-comment, non-var lines
415
+ if (line && !line.startsWith('#') && !line.startsWith('var ')) {
416
+ bodyLines.push(line);
417
+ }
418
+ }
419
+ body = bodyLines.join('\n').trim();
420
+ if (body === '') {
421
+ body = undefined;
422
+ }
423
+ }
424
+ return { method: method.toUpperCase(), url, headers, body, retryCount, backoffMs };
425
+ }
426
+ const METHOD_REGEX = /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|TRACE|CONNECT)\s+/i;
427
+ /**
428
+ * Checks if a line is the start of a new request (HTTP method line).
429
+ */
430
+ function isRequestLine(line) {
431
+ return METHOD_REGEX.test(line.trim());
432
+ }
433
+ /**
434
+ * Checks if a line is a variable declaration.
435
+ */
436
+ function isVariableLine(line) {
437
+ return /^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=/.test(line.trim());
438
+ }
439
+ /**
440
+ * Checks if a line is the start of a sequence block.
441
+ */
442
+ function isSequenceLine(line) {
443
+ return isSequenceStartDeclaration(line.trim());
444
+ }
445
+ /**
446
+ * Checks if a line is a named request declaration like [MyRequest].
447
+ */
448
+ function isNamedRequestDeclarationLine(line) {
449
+ return /^\[[^\]]+\]\s*$/.test(line.trim());
450
+ }
451
+ /**
452
+ * Checks if a line closes a sequence block.
453
+ */
454
+ function isEndSequenceLine(line) {
455
+ return isSequenceEndDeclaration(line.trim());
456
+ }
457
+ /**
458
+ * Finds the request block at a given line number.
459
+ * Request blocks are detected by HTTP method keywords (GET, POST, etc.).
460
+ * No need for ### separators - we detect boundaries automatically.
461
+ */
462
+ function getRequestBlockAtLine(text, lineNumber) {
463
+ const lines = text.split('\n');
464
+ // First, find the request line at or before the given line number
465
+ let requestStartLine = -1;
466
+ for (let i = lineNumber; i >= 0; i--) {
467
+ if (isRequestLine(lines[i])) {
468
+ requestStartLine = i;
469
+ break;
470
+ }
471
+ }
472
+ // If no request line found above, search below (user might be on a comment above the request)
473
+ if (requestStartLine === -1) {
474
+ for (let i = lineNumber; i < lines.length; i++) {
475
+ if (isRequestLine(lines[i])) {
476
+ requestStartLine = i;
477
+ break;
478
+ }
479
+ }
480
+ }
481
+ if (requestStartLine === -1) {
482
+ return ''; // No request found
483
+ }
484
+ // Find where this request block ends (next request or block declaration)
485
+ let endLine = lines.length;
486
+ for (let i = requestStartLine + 1; i < lines.length; i++) {
487
+ const line = lines[i].trim();
488
+ // End at the next request line, named request, sequence boundary, or ### separator.
489
+ if (isRequestLine(lines[i]) ||
490
+ isNamedRequestDeclarationLine(lines[i]) ||
491
+ isSequenceLine(lines[i]) ||
492
+ isEndSequenceLine(lines[i]) ||
493
+ line.startsWith('###')) {
494
+ endLine = i;
495
+ break;
496
+ }
497
+ }
498
+ // Include any comments/blank lines immediately before the request
499
+ let startLine = requestStartLine;
500
+ for (let i = requestStartLine - 1; i >= 0; i--) {
501
+ const line = lines[i].trim();
502
+ if (line === '' || line.startsWith('#')) {
503
+ // Don't include if it's a ### separator
504
+ if (line.startsWith('###')) {
505
+ break;
506
+ }
507
+ startLine = i;
508
+ }
509
+ else if (isVariableLine(lines[i])) {
510
+ // Stop at variable declarations
511
+ break;
512
+ }
513
+ else if (isRequestLine(lines[i])) {
514
+ // Stop at previous request
515
+ break;
516
+ }
517
+ else {
518
+ break;
519
+ }
520
+ }
521
+ return lines.slice(startLine, endLine).join('\n');
522
+ }
523
+ /**
524
+ * Extracts all import statements from the document.
525
+ * Import syntax:
526
+ * import "path/to/file.norn" (quoted)
527
+ * import './path/to/file.norn' (quoted)
528
+ * import ./path/to/file.norn (unquoted)
529
+ */
530
+ function extractImports(text) {
531
+ const lines = text.split('\n');
532
+ const imports = [];
533
+ // Match import "path" or import 'path' (quoted)
534
+ const quotedImportRegex = /^\s*import\s+["'](.+?)["']\s*$/;
535
+ // Match import path (unquoted - path without spaces)
536
+ const unquotedImportRegex = /^\s*import\s+(\S+)\s*$/;
537
+ for (let i = 0; i < lines.length; i++) {
538
+ const line = lines[i];
539
+ // Try quoted first
540
+ let match = line.match(quotedImportRegex);
541
+ if (match) {
542
+ imports.push({
543
+ path: match[1],
544
+ lineNumber: i
545
+ });
546
+ continue;
547
+ }
548
+ // Try unquoted
549
+ match = line.match(unquotedImportRegex);
550
+ if (match) {
551
+ imports.push({
552
+ path: match[1],
553
+ lineNumber: i
554
+ });
555
+ }
556
+ }
557
+ return imports;
558
+ }
559
+ /**
560
+ * Resolves all imports in a .norn file and returns the combined imported content.
561
+ * Only extracts named requests and sequences from imported files (not variables).
562
+ * Also handles .nornapi imports for header groups and endpoints.
563
+ * Handles circular imports by tracking the active import stack.
564
+ * Re-imports of already loaded files are treated as no-ops.
565
+ *
566
+ * @param text - The source text to extract imports from
567
+ * @param baseDir - The directory of the current file (for resolving relative paths)
568
+ * @param readFile - Function to read file contents (allows async filesystem access)
569
+ * @param alreadyImported - Set of already loaded import paths (for de-duplication)
570
+ * @param importStack - Set of import paths currently being resolved (for true cycle detection)
571
+ */
572
+ async function resolveImports(text, baseDir, readFile, alreadyImported = new Set(), importStack = new Set(), sourceFilePath = path.resolve(baseDir, '__norn_root__.norn'), sqlImportedPathsBySource = new Map(), sqlOperationsBySource = new Map()) {
573
+ const imports = extractImports(text);
574
+ const errors = [];
575
+ const importedContents = [];
576
+ const resolvedPaths = [];
577
+ const headerGroups = [];
578
+ const endpoints = [];
579
+ // Track sources for duplicate detection
580
+ const headerGroupSources = new Map();
581
+ const endpointSources = new Map();
582
+ const namedRequestSources = new Map();
583
+ const sequenceSources = new Map();
584
+ if (!sqlOperationsBySource.has(sourceFilePath)) {
585
+ sqlOperationsBySource.set(sourceFilePath, new Map());
586
+ }
587
+ if (!sqlImportedPathsBySource.has(sourceFilePath)) {
588
+ sqlImportedPathsBySource.set(sourceFilePath, new Set());
589
+ }
590
+ for (const imp of imports) {
591
+ // Resolve the path relative to baseDir
592
+ const absolutePath = path.resolve(baseDir, imp.path);
593
+ if (imp.path.endsWith('.nornsql')) {
594
+ const importedSqlPaths = sqlImportedPathsBySource.get(sourceFilePath);
595
+ if (importedSqlPaths.has(absolutePath)) {
596
+ continue;
597
+ }
598
+ try {
599
+ const content = await readFile(absolutePath);
600
+ importedSqlPaths.add(absolutePath);
601
+ resolvedPaths.push(absolutePath);
602
+ const parsedSql = (0, nornSqlParser_1.parseNornSqlFile)(content, absolutePath);
603
+ for (const parseError of parsedSql.errors) {
604
+ errors.push({
605
+ path: imp.path,
606
+ error: parseError.message,
607
+ lineNumber: parseError.lineNumber >= 0 ? parseError.lineNumber : imp.lineNumber,
608
+ blocking: parseError.blocking
609
+ });
610
+ }
611
+ const scope = sqlOperationsBySource.get(sourceFilePath);
612
+ for (const operation of parsedSql.operations) {
613
+ const lowerName = operation.name.toLowerCase();
614
+ const existing = scope.get(lowerName);
615
+ if (existing) {
616
+ errors.push({
617
+ path: imp.path,
618
+ error: `Duplicate SQL operation '${operation.name}': already defined in '${existing.sourcePath || existing.connectionName}'`,
619
+ lineNumber: imp.lineNumber,
620
+ blocking: true
621
+ });
622
+ continue;
623
+ }
624
+ scope.set(lowerName, operation);
625
+ }
626
+ }
627
+ catch (error) {
628
+ errors.push({
629
+ path: imp.path,
630
+ error: error.message || 'Failed to read file',
631
+ lineNumber: imp.lineNumber,
632
+ blocking: true
633
+ });
634
+ }
635
+ continue;
636
+ }
637
+ // Circular import only when the file is in the active import stack.
638
+ if (importStack.has(absolutePath)) {
639
+ errors.push({
640
+ path: imp.path,
641
+ error: `Circular import detected`,
642
+ lineNumber: imp.lineNumber,
643
+ blocking: true
644
+ });
645
+ continue;
646
+ }
647
+ // Duplicate import of an already loaded file: skip silently.
648
+ if (alreadyImported.has(absolutePath)) {
649
+ continue;
650
+ }
651
+ try {
652
+ const content = await readFile(absolutePath);
653
+ alreadyImported.add(absolutePath);
654
+ importStack.add(absolutePath);
655
+ resolvedPaths.push(absolutePath);
656
+ // Check if this is a .nornapi file
657
+ if (imp.path.endsWith('.nornapi')) {
658
+ // Parse the .nornapi file
659
+ const apiDef = (0, nornapiParser_1.parseNornApiFile)(content);
660
+ // Check for duplicate header groups
661
+ for (const group of apiDef.headerGroups) {
662
+ const existingSource = headerGroupSources.get(group.name);
663
+ if (existingSource) {
664
+ errors.push({
665
+ path: imp.path,
666
+ error: `Duplicate header group '${group.name}': already defined in '${existingSource}'`,
667
+ lineNumber: imp.lineNumber
668
+ });
669
+ }
670
+ else {
671
+ headerGroupSources.set(group.name, imp.path);
672
+ headerGroups.push(group);
673
+ }
674
+ }
675
+ // Check for duplicate endpoints
676
+ for (const endpoint of apiDef.endpoints) {
677
+ const existingSource = endpointSources.get(endpoint.name);
678
+ if (existingSource) {
679
+ errors.push({
680
+ path: imp.path,
681
+ error: `Duplicate endpoint '${endpoint.name}': already defined in '${existingSource}'`,
682
+ lineNumber: imp.lineNumber
683
+ });
684
+ }
685
+ else {
686
+ endpointSources.set(endpoint.name, imp.path);
687
+ endpoints.push(endpoint);
688
+ }
689
+ }
690
+ continue;
691
+ }
692
+ // Regular .norn file - process as before
693
+ // Recursively resolve imports in the imported file
694
+ const importDir = path.dirname(absolutePath);
695
+ const nestedResult = await resolveImports(content, importDir, readFile, alreadyImported, importStack, absolutePath, sqlImportedPathsBySource, sqlOperationsBySource);
696
+ // Add nested errors
697
+ errors.push(...nestedResult.errors);
698
+ resolvedPaths.push(...nestedResult.resolvedPaths);
699
+ for (const [nestedName, nestedSource] of nestedResult.sequenceSources.entries()) {
700
+ if (!sequenceSources.has(nestedName)) {
701
+ sequenceSources.set(nestedName, nestedSource);
702
+ }
703
+ }
704
+ // Add nested API definitions with duplicate checking
705
+ for (const group of nestedResult.headerGroups) {
706
+ const existingSource = headerGroupSources.get(group.name);
707
+ if (existingSource) {
708
+ errors.push({
709
+ path: imp.path,
710
+ error: `Duplicate header group '${group.name}': already defined in '${existingSource}'`,
711
+ lineNumber: imp.lineNumber
712
+ });
713
+ }
714
+ else {
715
+ headerGroupSources.set(group.name, imp.path);
716
+ headerGroups.push(group);
717
+ }
718
+ }
719
+ for (const endpoint of nestedResult.endpoints) {
720
+ const existingSource = endpointSources.get(endpoint.name);
721
+ if (existingSource) {
722
+ errors.push({
723
+ path: imp.path,
724
+ error: `Duplicate endpoint '${endpoint.name}': already defined in '${existingSource}'`,
725
+ lineNumber: imp.lineNumber
726
+ });
727
+ }
728
+ else {
729
+ endpointSources.set(endpoint.name, imp.path);
730
+ endpoints.push(endpoint);
731
+ }
732
+ }
733
+ // Add nested imported content first (dependencies first)
734
+ if (nestedResult.importedContent) {
735
+ importedContents.push(nestedResult.importedContent);
736
+ }
737
+ // Extract only file-level variables to resolve references in imported requests/sequences.
738
+ // Sequence-local vars must stay runtime-evaluated and must not be pre-baked.
739
+ const importedVariables = extractFileLevelVariables(content);
740
+ const importedNamedRequests = extractNamedRequests(content);
741
+ const importedSequences = extractSequencesFromText(content);
742
+ // Reconstruct named requests with variables already substituted (with duplicate checking)
743
+ for (const req of importedNamedRequests) {
744
+ const lowerName = req.name.toLowerCase();
745
+ const existingSource = namedRequestSources.get(lowerName);
746
+ if (existingSource) {
747
+ errors.push({
748
+ path: imp.path,
749
+ error: `Duplicate named request '${req.name}': already defined in '${existingSource}'`,
750
+ lineNumber: imp.lineNumber
751
+ });
752
+ }
753
+ else {
754
+ namedRequestSources.set(lowerName, imp.path);
755
+ const resolvedContent = substituteVariables(req.content, importedVariables);
756
+ importedContents.push(`[${req.name}]\n${resolvedContent}`);
757
+ }
758
+ }
759
+ // Reconstruct sequences with variables already substituted (with duplicate checking)
760
+ for (const seq of importedSequences) {
761
+ const lowerName = seq.name.toLowerCase();
762
+ const existingSource = sequenceSources.get(lowerName);
763
+ if (existingSource) {
764
+ errors.push({
765
+ path: imp.path,
766
+ error: `Duplicate sequence '${seq.name}': already defined in '${existingSource}'`,
767
+ lineNumber: imp.lineNumber
768
+ });
769
+ }
770
+ else {
771
+ // Store the ABSOLUTE path so script paths can be resolved correctly
772
+ sequenceSources.set(lowerName, absolutePath);
773
+ const resolvedDeclaration = substituteVariables(seq.declaration, importedVariables);
774
+ const resolvedContent = substituteVariables(seq.content, importedVariables);
775
+ importedContents.push(`${resolvedDeclaration}\n${resolvedContent}\nend sequence`);
776
+ }
777
+ }
778
+ }
779
+ catch (error) {
780
+ errors.push({
781
+ path: imp.path,
782
+ error: error.message || 'Failed to read file',
783
+ lineNumber: imp.lineNumber
784
+ });
785
+ }
786
+ finally {
787
+ importStack.delete(absolutePath);
788
+ }
789
+ }
790
+ return {
791
+ importedContent: importedContents.join('\n\n'),
792
+ errors,
793
+ resolvedPaths,
794
+ headerGroups,
795
+ endpoints,
796
+ sequenceSources,
797
+ sqlOperationsBySource
798
+ };
799
+ }
800
+ /**
801
+ * Helper function to extract sequences from text without importing sequenceRunner
802
+ * (avoids circular dependencies)
803
+ */
804
+ function extractSequencesFromText(text) {
805
+ const lines = text.split('\n');
806
+ const sequences = [];
807
+ let currentSequence = null;
808
+ for (let i = 0; i < lines.length; i++) {
809
+ const line = lines[i].trim();
810
+ const lineWithoutComment = (0, stringUtils_1.stripInlineComment)(line);
811
+ // Check for sequence start. Preserve the parameter list so imported
812
+ // sequences can still bind arguments when reconstructed.
813
+ const sequenceMatch = lineWithoutComment.match(/^sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(\s*\([^)]*\))?\s*$/);
814
+ if (sequenceMatch) {
815
+ currentSequence = {
816
+ name: sequenceMatch[1],
817
+ declaration: `sequence ${sequenceMatch[1]}${sequenceMatch[2] || ''}`,
818
+ lines: []
819
+ };
820
+ continue;
821
+ }
822
+ // Check for sequence end
823
+ if (lineWithoutComment === 'end sequence' && currentSequence) {
824
+ sequences.push({
825
+ name: currentSequence.name,
826
+ declaration: currentSequence.declaration,
827
+ content: currentSequence.lines.join('\n')
828
+ });
829
+ currentSequence = null;
830
+ continue;
831
+ }
832
+ // Add line to current sequence
833
+ if (currentSequence) {
834
+ currentSequence.lines.push(lines[i]);
835
+ }
836
+ }
837
+ return sequences;
838
+ }
839
+ //# sourceMappingURL=parser.js.map