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
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ * CLI reporters index - re-exports all reporter functions
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
17
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ __exportStar(require("./junit"), exports);
21
+ __exportStar(require("./html"), exports);
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ /**
3
+ * JUnit XML reporter for CI/CD integration
4
+ *
5
+ * Generates JUnit XML format that's compatible with:
6
+ * - Azure DevOps (ADO)
7
+ * - Jenkins
8
+ * - GitHub Actions
9
+ * - GitLab CI
10
+ * - CircleCI
11
+ * - Most CI/CD platforms
12
+ */
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.generateJUnitReport = generateJUnitReport;
48
+ exports.generateJUnitReportFromResponse = generateJUnitReportFromResponse;
49
+ const fs = __importStar(require("fs"));
50
+ const redaction_1 = require("../redaction");
51
+ /**
52
+ * Escape XML special characters
53
+ */
54
+ function escapeXml(text) {
55
+ return text
56
+ .replace(/&/g, '&amp;')
57
+ .replace(/</g, '&lt;')
58
+ .replace(/>/g, '&gt;')
59
+ .replace(/"/g, '&quot;')
60
+ .replace(/'/g, '&apos;');
61
+ }
62
+ /**
63
+ * Format duration as seconds with 3 decimal places
64
+ */
65
+ function formatDurationSeconds(ms) {
66
+ return (ms / 1000).toFixed(3);
67
+ }
68
+ /**
69
+ * Generate JUnit XML report from sequence results
70
+ */
71
+ function generateJUnitReport(results, options) {
72
+ const { outputPath, redaction, suiteName = 'Norn Tests' } = options;
73
+ const totalTests = results.reduce((sum, r) => sum + Math.max(1, r.assertionResults?.length || 0), 0);
74
+ const totalFailures = results.reduce((sum, r) => sum + (r.assertionResults?.filter(a => !a.passed).length || (r.success ? 0 : 1)), 0);
75
+ const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
76
+ const totalErrors = results.filter(r => r.errors.length > 0 && r.success === false).length;
77
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
78
+ xml += `<testsuites name="${escapeXml(suiteName)}" tests="${totalTests}" failures="${totalFailures}" errors="${totalErrors}" time="${formatDurationSeconds(totalDuration)}">\n`;
79
+ for (const result of results) {
80
+ xml += generateTestSuite(result, redaction);
81
+ }
82
+ xml += '</testsuites>\n';
83
+ fs.writeFileSync(outputPath, xml, 'utf-8');
84
+ }
85
+ /**
86
+ * Generate a testsuite element for a sequence
87
+ */
88
+ function generateTestSuite(result, redaction) {
89
+ const assertions = result.assertionResults || [];
90
+ const testCount = Math.max(1, assertions.length);
91
+ const failures = assertions.filter(a => !a.passed).length + (result.success ? 0 : (assertions.length === 0 ? 1 : 0));
92
+ let xml = ` <testsuite name="${escapeXml(result.name)}" tests="${testCount}" failures="${failures}" errors="0" time="${formatDurationSeconds(result.duration)}">\n`;
93
+ if (assertions.length > 0) {
94
+ // Create a testcase for each assertion
95
+ for (const assertion of assertions) {
96
+ xml += generateTestCase(result.name, assertion, redaction);
97
+ }
98
+ }
99
+ else {
100
+ // No assertions - create a single testcase for the sequence
101
+ xml += generateSequenceTestCase(result, redaction);
102
+ }
103
+ // Add system-out with request/response details
104
+ xml += generateSystemOut(result, redaction);
105
+ // Add system-err with errors
106
+ if (result.errors.length > 0) {
107
+ xml += ` <system-err><![CDATA[${result.errors.map(e => (0, redaction_1.redactString)(e, redaction)).join('\n')}]]></system-err>\n`;
108
+ }
109
+ xml += ' </testsuite>\n';
110
+ return xml;
111
+ }
112
+ /**
113
+ * Generate a testcase element for an assertion
114
+ */
115
+ function generateTestCase(sequenceName, assertion, redaction) {
116
+ const testName = assertion.message || assertion.expression;
117
+ const className = sequenceName;
118
+ let xml = ` <testcase name="${escapeXml(testName)}" classname="${escapeXml(className)}">\n`;
119
+ if (!assertion.passed) {
120
+ const message = assertion.error || `Expected ${assertion.rightExpression || assertion.rightValue}, got ${JSON.stringify(assertion.leftValue)}`;
121
+ xml += ` <failure message="${escapeXml((0, redaction_1.redactString)(message, redaction))}" type="AssertionError">\n`;
122
+ xml += `<![CDATA[Expression: ${assertion.expression}\n`;
123
+ xml += `Expected: ${JSON.stringify(assertion.rightValue)}\n`;
124
+ xml += `Actual: ${JSON.stringify(assertion.leftValue)}]]>\n`;
125
+ xml += ' </failure>\n';
126
+ }
127
+ xml += ' </testcase>\n';
128
+ return xml;
129
+ }
130
+ /**
131
+ * Generate a testcase element for a sequence without assertions
132
+ */
133
+ function generateSequenceTestCase(result, redaction) {
134
+ let xml = ` <testcase name="${escapeXml(result.name)}" classname="${escapeXml(result.name)}" time="${formatDurationSeconds(result.duration)}">\n`;
135
+ if (!result.success) {
136
+ const errorMessages = result.errors.length > 0
137
+ ? result.errors.map(e => (0, redaction_1.redactString)(e, redaction)).join('; ')
138
+ : 'Sequence failed';
139
+ xml += ` <failure message="${escapeXml(errorMessages)}" type="SequenceError">\n`;
140
+ xml += `<![CDATA[${errorMessages}]]>\n`;
141
+ xml += ' </failure>\n';
142
+ }
143
+ xml += ' </testcase>\n';
144
+ return xml;
145
+ }
146
+ /**
147
+ * Generate system-out with request/response details
148
+ */
149
+ function generateSystemOut(result, redaction) {
150
+ const lines = [];
151
+ for (const step of result.steps) {
152
+ if (step.type === 'request' && step.response) {
153
+ const url = step.requestUrl ? (0, redaction_1.redactUrl)(step.requestUrl, redaction) : 'unknown';
154
+ lines.push(`[${step.requestMethod || 'REQUEST'}] ${url}`);
155
+ lines.push(` Status: ${step.response.status} ${step.response.statusText}`);
156
+ lines.push(` Duration: ${step.response.duration}ms`);
157
+ if (step.response.body) {
158
+ const bodyStr = typeof step.response.body === 'object'
159
+ ? JSON.stringify((0, redaction_1.redactBody)(step.response.body, redaction), null, 2)
160
+ : (0, redaction_1.redactString)(String(step.response.body), redaction);
161
+ // Truncate long bodies
162
+ if (bodyStr.length > 1000) {
163
+ lines.push(` Body: ${bodyStr.substring(0, 1000)}... (truncated)`);
164
+ }
165
+ else {
166
+ lines.push(` Body: ${bodyStr}`);
167
+ }
168
+ }
169
+ lines.push('');
170
+ }
171
+ else if (step.type === 'print' && step.print) {
172
+ lines.push(`[PRINT] ${step.print.title}`);
173
+ if (step.print.body) {
174
+ lines.push(` ${step.print.body}`);
175
+ }
176
+ }
177
+ else if (step.type === 'mcp' && step.mcp) {
178
+ if (step.mcp.operation === 'list') {
179
+ lines.push(`[MCP LIST] ${step.mcp.serverAlias}`);
180
+ lines.push(` Tools: ${(step.mcp.tools || []).map(tool => tool.name).join(', ')}`);
181
+ }
182
+ else {
183
+ lines.push(`[MCP CALL] ${step.mcp.serverAlias}.${step.mcp.toolName || ''}`);
184
+ if (step.mcp.result?.text) {
185
+ lines.push(` Text: ${(0, redaction_1.redactString)(step.mcp.result.text, redaction)}`);
186
+ }
187
+ if (step.mcp.result?.structuredContent) {
188
+ lines.push(` Structured: ${JSON.stringify(step.mcp.result.structuredContent)}`);
189
+ }
190
+ }
191
+ }
192
+ }
193
+ if (lines.length === 0) {
194
+ return '';
195
+ }
196
+ return ` <system-out><![CDATA[${lines.join('\n')}]]></system-out>\n`;
197
+ }
198
+ /**
199
+ * Generate JUnit report from a single HTTP response (for non-sequence runs)
200
+ */
201
+ function generateJUnitReportFromResponse(response, testName, options) {
202
+ const { outputPath, redaction, suiteName = 'Norn Tests' } = options;
203
+ const isSuccess = response.status >= 200 && response.status < 300;
204
+ const failures = isSuccess ? 0 : 1;
205
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
206
+ xml += `<testsuites name="${escapeXml(suiteName)}" tests="1" failures="${failures}" errors="0" time="${formatDurationSeconds(response.duration)}">\n`;
207
+ xml += ` <testsuite name="${escapeXml(testName)}" tests="1" failures="${failures}" errors="0" time="${formatDurationSeconds(response.duration)}">\n`;
208
+ xml += ` <testcase name="${escapeXml(testName)}" classname="${escapeXml(testName)}" time="${formatDurationSeconds(response.duration)}">\n`;
209
+ if (!isSuccess) {
210
+ xml += ` <failure message="HTTP ${response.status} ${response.statusText}" type="HttpError">\n`;
211
+ xml += `<![CDATA[Status: ${response.status} ${response.statusText}`;
212
+ if (response.body) {
213
+ const bodyStr = typeof response.body === 'object'
214
+ ? JSON.stringify((0, redaction_1.redactBody)(response.body, redaction), null, 2)
215
+ : (0, redaction_1.redactString)(String(response.body), redaction);
216
+ xml += `\nResponse: ${bodyStr}`;
217
+ }
218
+ xml += ']]>\n';
219
+ xml += ' </failure>\n';
220
+ }
221
+ xml += ' </testcase>\n';
222
+ xml += ' </testsuite>\n';
223
+ xml += '</testsuites>\n';
224
+ fs.writeFileSync(outputPath, xml, 'utf-8');
225
+ }
226
+ //# sourceMappingURL=junit.js.map
@@ -0,0 +1,351 @@
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.HttpCodeLensProvider = void 0;
37
+ exports.updateCodeLensCoverage = updateCodeLensCoverage;
38
+ const vscode = __importStar(require("vscode"));
39
+ const environmentProvider_1 = require("./environmentProvider");
40
+ const envFileSecrets_1 = require("./secrets/envFileSecrets");
41
+ const crypto_1 = require("./secrets/crypto");
42
+ const environmentParser_1 = require("./environmentParser");
43
+ const nornenvRegionRefactor_1 = require("./nornenvRegionRefactor");
44
+ // Cached coverage percentage for CodeLens display
45
+ /**
46
+ * Update the cached coverage for CodeLens display
47
+ */
48
+ function updateCodeLensCoverage(percentage, total, covered) {
49
+ void percentage;
50
+ void total;
51
+ void covered;
52
+ }
53
+ /**
54
+ * Checks if a sequence declaration has only optional parameters (all have defaults).
55
+ * Returns true if no parameters or all parameters have defaults.
56
+ */
57
+ function canRunSequenceWithoutArgs(line) {
58
+ // Check if there are parameters
59
+ const parenMatch = line.match(/\(([^)]*)\)/);
60
+ if (!parenMatch) {
61
+ // No parameters - can run
62
+ return true;
63
+ }
64
+ const paramsStr = parenMatch[1].trim();
65
+ if (!paramsStr) {
66
+ // Empty parens - can run
67
+ return true;
68
+ }
69
+ // Split by comma (simple split, not handling quoted strings with commas)
70
+ const params = paramsStr.split(',');
71
+ // Check each parameter has a default (contains =)
72
+ for (const param of params) {
73
+ const trimmed = param.trim();
74
+ if (!trimmed) {
75
+ continue;
76
+ }
77
+ // Must contain = for a default value
78
+ if (!trimmed.includes('=')) {
79
+ return false;
80
+ }
81
+ }
82
+ return true;
83
+ }
84
+ /**
85
+ * Checks for @data(...) or @theory(...) annotations directly above a sequence declaration.
86
+ * This allows CodeLens execution for parameterized sequences with required parameters.
87
+ */
88
+ function hasTheoryAnnotationsAbove(document, sequenceLine) {
89
+ const dataOrTheoryPattern = /^@(data|theory)\s*\(/;
90
+ const decoratorPattern = /^@/;
91
+ for (let i = sequenceLine - 1; i >= 0; i--) {
92
+ const line = document.lineAt(i).text.trim();
93
+ // Allow blank lines between decorators and sequence declaration
94
+ if (!line) {
95
+ continue;
96
+ }
97
+ // Stop as soon as the decorator block ends
98
+ if (!decoratorPattern.test(line)) {
99
+ break;
100
+ }
101
+ if (dataOrTheoryPattern.test(line)) {
102
+ return true;
103
+ }
104
+ }
105
+ return false;
106
+ }
107
+ class HttpCodeLensProvider {
108
+ _onDidChangeCodeLenses = new vscode.EventEmitter();
109
+ onDidChangeCodeLenses = this._onDidChangeCodeLenses.event;
110
+ constructor() {
111
+ // Refresh code lenses when environment changes
112
+ vscode.commands.registerCommand('norn.refreshCodeLenses', () => {
113
+ this._onDidChangeCodeLenses.fire();
114
+ });
115
+ }
116
+ /**
117
+ * Gets the environment display text for CodeLens
118
+ * Returns 'add' if no env file exists, environment name if it does
119
+ */
120
+ getEnvDisplay(documentPath) {
121
+ const envFile = (0, environmentProvider_1.findEnvFileFromPath)(documentPath);
122
+ if (!envFile) {
123
+ return { text: '+ Add env file', hasEnvFile: false };
124
+ }
125
+ const activeEnv = (0, environmentProvider_1.getActiveEnvironment)(documentPath);
126
+ const availableEnvs = (0, environmentProvider_1.getAvailableEnvironments)(envFile);
127
+ if (availableEnvs.length === 0) {
128
+ return { text: 'env: (empty)', hasEnvFile: true };
129
+ }
130
+ return { text: activeEnv ? `env: ${activeEnv}` : 'env: none', hasEnvFile: true };
131
+ }
132
+ provideCodeLenses(document) {
133
+ const codeLenses = [];
134
+ const methodRegex = /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/;
135
+ // Match sequence (with optional test keyword) and optional parameters
136
+ const sequenceRegex = /^(?:test\s+)?sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?$/;
137
+ // Match swagger statement with optional quotes around URL (only in .nornapi files)
138
+ const swaggerRegex = /^swagger\s+["']?(https?:\/\/[^\s"']+)["']?\s*$/;
139
+ // Match matchesSchema assertions: assert $1.body matchesSchema "./schema.json"
140
+ // Note: .+? is non-greedy to not consume matchesSchema
141
+ const matchesSchemaRegex = /^\s*assert\s+.+?\s+matchesSchema\s+["']?([^"']+)["']?/i;
142
+ const envInfo = this.getEnvDisplay(document.uri.fsPath);
143
+ const isNornApiFile = document.languageId === 'nornapi';
144
+ const isNornFile = document.languageId === 'norn';
145
+ const isNornenvFile = (0, envFileSecrets_1.isNornenvDocumentLike)(document.languageId, document.uri.fsPath);
146
+ const secretLinesByNumber = isNornenvFile
147
+ ? new Map((0, envFileSecrets_1.extractSecretLines)(document.getText(), document.uri.fsPath).map(secret => [secret.lineNumber, secret]))
148
+ : new Map();
149
+ const nornenvConfig = isNornenvFile ? (0, environmentParser_1.parseEnvFile)(document.getText(), document.uri.fsPath) : undefined;
150
+ // Region-pattern refactor — single top-of-file lens when a flat S × R matrix is detected.
151
+ if (isNornenvFile && nornenvConfig) {
152
+ const pattern = (0, nornenvRegionRefactor_1.detectRegionPattern)(nornenvConfig, document.getText());
153
+ if (pattern) {
154
+ const { liftedToStage, liftedToRegion, leafSpecific } = pattern.summary;
155
+ const topRange = new vscode.Range(0, 0, 0, 0);
156
+ codeLenses.push(new vscode.CodeLens(topRange, {
157
+ title: `$(sparkle) Refactor ${pattern.cells.length} envs into ${pattern.stages.length}+${pattern.regions.length} templates`,
158
+ command: 'norn.nornenv.refactorRegionPattern',
159
+ arguments: [document.uri.fsPath],
160
+ tooltip: `Detected an S × R pattern (${pattern.stages.join(', ')} × ${pattern.regions.join(', ')}). `
161
+ + `Refactor will lift ${liftedToStage} vars to stage templates, ${liftedToRegion} to region templates, and keep ${leafSpecific} as leaf-specific.`
162
+ }));
163
+ }
164
+ }
165
+ // Track if we're inside a sequence
166
+ let insideSequence = false;
167
+ for (let i = 0; i < document.lineCount; i++) {
168
+ const line = document.lineAt(i);
169
+ const trimmedText = line.text.trim();
170
+ if (isNornenvFile) {
171
+ const headerMatch = trimmedText.match(/^\[env:([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s+extends\s+([^\]]+))?\]\s*$/i);
172
+ if (headerMatch) {
173
+ const envName = headerMatch[1];
174
+ const parents = (headerMatch[2] ?? '')
175
+ .split(',')
176
+ .map(parent => parent.trim())
177
+ .filter(parent => parent.length > 0);
178
+ const range = new vscode.Range(i, 0, i, line.text.length);
179
+ const activeEnv = (0, environmentProvider_1.getActiveEnvironment)(document.uri.fsPath);
180
+ if (activeEnv !== envName) {
181
+ codeLenses.push(new vscode.CodeLens(range, {
182
+ title: '$(debug-start) Activate',
183
+ command: 'norn.nornenv.activate',
184
+ arguments: [envName, document.uri.fsPath],
185
+ tooltip: `Use '${envName}' as the active Norn environment`
186
+ }));
187
+ }
188
+ if (parents.length > 0) {
189
+ const inheritedCount = nornenvConfig
190
+ ? (0, environmentParser_1.resolveInheritedVariableDetails)(envName, nornenvConfig).size
191
+ : 0;
192
+ codeLenses.push(new vscode.CodeLens(range, {
193
+ title: `$(list-tree) Peek inherited (${inheritedCount})`,
194
+ command: 'norn.nornenv.peekInherited',
195
+ arguments: [envName, document.uri.fsPath],
196
+ tooltip: `Show variables inherited by '${envName}'`
197
+ }));
198
+ }
199
+ continue;
200
+ }
201
+ const secret = secretLinesByNumber.get(i);
202
+ if (!secret) {
203
+ continue;
204
+ }
205
+ const range = new vscode.Range(i, 0, i, line.text.length);
206
+ if (!secret.encrypted) {
207
+ codeLenses.push(new vscode.CodeLens(range, {
208
+ title: '$(lock) Encrypt Secret',
209
+ command: 'norn.encryptSecretAtLine',
210
+ arguments: [document.uri.fsPath, i],
211
+ tooltip: 'Encrypt this secret in-place'
212
+ }));
213
+ continue;
214
+ }
215
+ const parsed = (0, crypto_1.parseEncryptedSecretValue)(secret.value);
216
+ const hasValidCiphertext = parsed.ok;
217
+ codeLenses.push(new vscode.CodeLens(range, {
218
+ title: '$(eye) View Decrypted',
219
+ command: 'norn.viewDecryptedSecretAtLine',
220
+ arguments: [document.uri.fsPath, i],
221
+ tooltip: 'Reveal decrypted value in a temporary editor'
222
+ }));
223
+ codeLenses.push(new vscode.CodeLens(range, {
224
+ title: '$(sync) Rotate Secret',
225
+ command: 'norn.rotateSecretAtLine',
226
+ arguments: [document.uri.fsPath, i],
227
+ tooltip: hasValidCiphertext
228
+ ? 'Replace with a new encrypted value'
229
+ : 'Replace malformed secret with a new encrypted value'
230
+ }));
231
+ codeLenses.push(new vscode.CodeLens(range, {
232
+ title: '$(trash) Delete Secret',
233
+ command: 'norn.deleteSecretAtLine',
234
+ arguments: [document.uri.fsPath, i],
235
+ tooltip: 'Remove this secret declaration line'
236
+ }));
237
+ continue;
238
+ }
239
+ // Check for swagger statement (only in .nornapi files)
240
+ if (isNornApiFile) {
241
+ const swaggerMatch = trimmedText.match(swaggerRegex);
242
+ if (swaggerMatch) {
243
+ const range = new vscode.Range(i, 0, i, line.text.length);
244
+ codeLenses.push(new vscode.CodeLens(range, {
245
+ title: '$(debug-start) Import Endpoints',
246
+ command: 'norn.importSwagger',
247
+ arguments: [swaggerMatch[1], i],
248
+ tooltip: `Parse OpenAPI spec and generate endpoints`
249
+ }));
250
+ // Add Generate Schemas CodeLens
251
+ codeLenses.push(new vscode.CodeLens(range, {
252
+ title: '$(file-code) Generate Schemas',
253
+ command: 'norn.generateSchemasFromSwagger',
254
+ arguments: [swaggerMatch[1], document.uri.fsPath],
255
+ tooltip: 'Generate JSON Schema files from OpenAPI response definitions'
256
+ }));
257
+ // Add file-scoped coverage CodeLens
258
+ codeLenses.push(new vscode.CodeLens(range, {
259
+ title: '$(graph) Show Coverage',
260
+ command: 'norn.showCoverage',
261
+ arguments: [document.uri.fsPath],
262
+ tooltip: 'Show API coverage for this .nornapi file (includes this folder and subfolders only).'
263
+ }));
264
+ continue;
265
+ }
266
+ }
267
+ // Check for matchesSchema assertions (in .norn files)
268
+ if (isNornFile) {
269
+ const schemaMatch = trimmedText.match(matchesSchemaRegex);
270
+ if (schemaMatch) {
271
+ const schemaPath = schemaMatch[1];
272
+ const range = new vscode.Range(i, 0, i, line.text.length);
273
+ // Open Contract lens
274
+ codeLenses.push(new vscode.CodeLens(range, {
275
+ title: '$(file-code) Open Contract',
276
+ command: 'norn.openSchemaFile',
277
+ arguments: [schemaPath, document.uri.fsPath],
278
+ tooltip: `Open schema file: ${schemaPath}`
279
+ }));
280
+ // View Contract Report lens
281
+ codeLenses.push(new vscode.CodeLens(range, {
282
+ title: '$(checklist) View Contract',
283
+ command: 'norn.viewContractReport',
284
+ arguments: [i, document.uri.fsPath, schemaPath],
285
+ tooltip: 'Run validation and show contract comparison report'
286
+ }));
287
+ continue;
288
+ }
289
+ }
290
+ // Check for sequence start
291
+ const sequenceMatch = trimmedText.match(sequenceRegex);
292
+ if (sequenceMatch) {
293
+ insideSequence = true;
294
+ const range = new vscode.Range(i, 0, i, line.text.length);
295
+ // Show Run Sequence if parameters are all optional, or values are provided via @data/@theory.
296
+ const canRunWithoutArgs = canRunSequenceWithoutArgs(trimmedText);
297
+ const hasTheoryAnnotations = hasTheoryAnnotationsAbove(document, i);
298
+ if (canRunWithoutArgs || hasTheoryAnnotations) {
299
+ // Run Sequence lens
300
+ codeLenses.push(new vscode.CodeLens(range, {
301
+ title: '▶ Run Sequence',
302
+ command: 'norn.runSequence',
303
+ arguments: [i],
304
+ tooltip: `Run all requests in sequence "${sequenceMatch[1]}"`
305
+ }));
306
+ // Debug Sequence lens
307
+ codeLenses.push(new vscode.CodeLens(range, {
308
+ title: '▷ Debug Sequence',
309
+ command: 'norn.debugSequence',
310
+ arguments: [i, document.uri.fsPath, sequenceMatch[1]],
311
+ tooltip: `Debug sequence "${sequenceMatch[1]}" with breakpoints and stepping`
312
+ }));
313
+ // Environment lens
314
+ codeLenses.push(new vscode.CodeLens(range, {
315
+ title: envInfo.text,
316
+ command: envInfo.hasEnvFile ? 'norn.selectEnvironmentAndRefresh' : 'norn.createEnvFile',
317
+ arguments: envInfo.hasEnvFile ? [document.uri.fsPath] : undefined,
318
+ tooltip: envInfo.hasEnvFile ? 'Click to change environment' : 'Click to create .nornenv file'
319
+ }));
320
+ }
321
+ continue;
322
+ }
323
+ // Check for sequence end
324
+ if (trimmedText === 'end sequence') {
325
+ insideSequence = false;
326
+ continue;
327
+ }
328
+ // Only show "Send Request" for requests OUTSIDE of sequences
329
+ if (!insideSequence && methodRegex.test(line.text)) {
330
+ const range = new vscode.Range(i, 0, i, line.text.length);
331
+ // Send Request lens
332
+ codeLenses.push(new vscode.CodeLens(range, {
333
+ title: '▶ Send Request',
334
+ command: 'norn.sendRequest',
335
+ arguments: [i],
336
+ tooltip: 'Send this HTTP request'
337
+ }));
338
+ // Environment lens
339
+ codeLenses.push(new vscode.CodeLens(range, {
340
+ title: envInfo.text,
341
+ command: envInfo.hasEnvFile ? 'norn.selectEnvironmentAndRefresh' : 'norn.createEnvFile',
342
+ arguments: envInfo.hasEnvFile ? [document.uri.fsPath] : undefined,
343
+ tooltip: envInfo.hasEnvFile ? 'Click to change environment' : 'Click to create .nornenv file'
344
+ }));
345
+ }
346
+ }
347
+ return codeLenses;
348
+ }
349
+ }
350
+ exports.HttpCodeLensProvider = HttpCodeLensProvider;
351
+ //# sourceMappingURL=codeLensProvider.js.map