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,736 @@
1
+ "use strict";
2
+ /**
3
+ * Shared environment file parser — used by both the VS Code extension and the CLI.
4
+ *
5
+ * IMPORTANT: This module must NOT import 'vscode' or any VS Code-specific APIs.
6
+ * It must remain pure Node.js so the CLI can use it without the vscode module.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.ENV_FILENAME = void 0;
43
+ exports.parseEnvFile = parseEnvFile;
44
+ exports.getEnvSearchStartDirectory = getEnvSearchStartDirectory;
45
+ exports.findEnvFileFromPath = findEnvFileFromPath;
46
+ exports.formatNornenvErrorLocation = formatNornenvErrorLocation;
47
+ exports.formatNornenvErrorLocationForSource = formatNornenvErrorLocationForSource;
48
+ exports.resolveNornenvImports = resolveNornenvImports;
49
+ exports.loadAndResolveEnvFile = loadAndResolveEnvFile;
50
+ exports.findExtendsNode = findExtendsNode;
51
+ exports.findExtendsTemplate = findExtendsTemplate;
52
+ exports.resolveEffectiveEnvVariables = resolveEffectiveEnvVariables;
53
+ exports.resolveInheritedVariableDetails = resolveInheritedVariableDetails;
54
+ exports.resolveEffectiveEnvVariableDetails = resolveEffectiveEnvVariableDetails;
55
+ exports.collectAncestorVariableNames = collectAncestorVariableNames;
56
+ exports.detectExtendsCycles = detectExtendsCycles;
57
+ const fs = __importStar(require("fs"));
58
+ const path = __importStar(require("path"));
59
+ const crypto_1 = require("./secrets/crypto");
60
+ const keyStore_1 = require("./secrets/keyStore");
61
+ // ─── Constants ───────────────────────────────────────────────────────────────
62
+ exports.ENV_FILENAME = '.nornenv';
63
+ // ─── Parsing ─────────────────────────────────────────────────────────────────
64
+ const importRegex = /^import\s+["']?(.+?)["']?\s*$/;
65
+ const sectionHeaderRegex = /^\[(env|template):([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s+extends\s+([^\]]+?))?\]\s*$/;
66
+ const identifierRegex = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
67
+ const varRegex = /^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
68
+ const connectionStringRegex = /^connectionString\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
69
+ const secretRegex = /^secret\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
70
+ const secretConnectionStringRegex = /^secret\s+connectionString\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
71
+ /**
72
+ * Parses an `extends a, b, c` clause into a deduped, ordered name list.
73
+ * Returns `[]` if the clause is missing or only contains invalid tokens.
74
+ */
75
+ function parseExtendsClause(clause) {
76
+ if (!clause) {
77
+ return [];
78
+ }
79
+ const seen = new Set();
80
+ const names = [];
81
+ for (const raw of clause.split(',')) {
82
+ const name = raw.trim();
83
+ if (!name || !identifierRegex.test(name) || seen.has(name)) {
84
+ continue;
85
+ }
86
+ seen.add(name);
87
+ names.push(name);
88
+ }
89
+ return names;
90
+ }
91
+ /**
92
+ * Parses the raw content of a .nornenv file into an EnvironmentConfig.
93
+ * Does NOT resolve imports — only extracts them as ImportStatements.
94
+ */
95
+ function parseEnvFile(content, sourceFilePath) {
96
+ const lines = content.split('\n');
97
+ const config = {
98
+ common: {},
99
+ environments: [],
100
+ templates: [],
101
+ secretNames: new Set(),
102
+ secretValues: new Map(),
103
+ imports: [],
104
+ misplacedImports: [],
105
+ secretDeclarations: []
106
+ };
107
+ // The active section is either an env, a template, or null (= common scope).
108
+ let currentSection = null;
109
+ let currentKind = null;
110
+ let seenContent = false; // Track whether we've seen var/secret/section content
111
+ const assignVariable = (varName, varValue) => {
112
+ if (currentSection) {
113
+ currentSection.variables[varName] = varValue;
114
+ }
115
+ else {
116
+ config.common[varName] = varValue;
117
+ }
118
+ };
119
+ const buildSecretDeclaration = (varName, varValue, lineNumber) => ({
120
+ name: varName,
121
+ value: varValue,
122
+ envName: currentKind === 'env' ? currentSection?.name : undefined,
123
+ templateName: currentKind === 'template' ? currentSection?.name : undefined,
124
+ lineNumber,
125
+ filePath: sourceFilePath
126
+ });
127
+ for (let i = 0; i < lines.length; i++) {
128
+ const trimmed = lines[i].trim();
129
+ // Skip empty lines and comments
130
+ if (!trimmed || trimmed.startsWith('#')) {
131
+ continue;
132
+ }
133
+ // Check for import statement
134
+ const importMatch = trimmed.match(importRegex);
135
+ if (importMatch) {
136
+ const importStatement = {
137
+ path: importMatch[1],
138
+ lineNumber: i
139
+ };
140
+ config.imports.push(importStatement);
141
+ if (seenContent) {
142
+ config.misplacedImports.push(importStatement);
143
+ }
144
+ continue;
145
+ }
146
+ seenContent = true;
147
+ // Check for [env:NAME] or [template:NAME] section (with optional `extends a, b` clause)
148
+ const sectionMatch = trimmed.match(sectionHeaderRegex);
149
+ if (sectionMatch) {
150
+ const kind = sectionMatch[1];
151
+ const name = sectionMatch[2];
152
+ const parents = parseExtendsClause(sectionMatch[3]);
153
+ if (kind === 'env') {
154
+ const env = { name, variables: {}, parents };
155
+ config.environments.push(env);
156
+ currentSection = env;
157
+ }
158
+ else {
159
+ const template = { name, variables: {}, parents };
160
+ config.templates.push(template);
161
+ currentSection = template;
162
+ }
163
+ currentKind = kind;
164
+ continue;
165
+ }
166
+ // Check for secret connectionString declaration
167
+ const secretConnectionStringMatch = trimmed.match(secretConnectionStringRegex);
168
+ if (secretConnectionStringMatch) {
169
+ const profileName = secretConnectionStringMatch[1];
170
+ const varName = `${profileName}_connectionString`;
171
+ const varValue = secretConnectionStringMatch[2].trim();
172
+ config.secretDeclarations.push(buildSecretDeclaration(varName, varValue, i));
173
+ config.secretNames.add(varName);
174
+ config.secretValues.set(varName, varValue);
175
+ assignVariable(varName, varValue);
176
+ continue;
177
+ }
178
+ // Check for secret declaration (treated like var but marked for redaction)
179
+ const secretMatch = trimmed.match(secretRegex);
180
+ if (secretMatch) {
181
+ const varName = secretMatch[1];
182
+ const varValue = secretMatch[2].trim();
183
+ config.secretDeclarations.push(buildSecretDeclaration(varName, varValue, i));
184
+ // Track as secret
185
+ config.secretNames.add(varName);
186
+ config.secretValues.set(varName, varValue);
187
+ assignVariable(varName, varValue);
188
+ continue;
189
+ }
190
+ const connectionStringMatch = trimmed.match(connectionStringRegex);
191
+ if (connectionStringMatch) {
192
+ const profileName = connectionStringMatch[1];
193
+ const varName = `${profileName}_connectionString`;
194
+ const varValue = connectionStringMatch[2].trim();
195
+ assignVariable(varName, varValue);
196
+ continue;
197
+ }
198
+ // Check for variable declaration
199
+ const varMatch = trimmed.match(varRegex);
200
+ if (varMatch) {
201
+ const varName = varMatch[1];
202
+ const varValue = varMatch[2].trim();
203
+ assignVariable(varName, varValue);
204
+ }
205
+ }
206
+ return config;
207
+ }
208
+ // ─── File Discovery ──────────────────────────────────────────────────────────
209
+ /**
210
+ * Resolves where environment file discovery should start.
211
+ * If the path points to a directory, search from that directory.
212
+ * If it points to a file (or non-existent path), search from the parent directory.
213
+ */
214
+ function getEnvSearchStartDirectory(targetPath) {
215
+ const resolvedPath = path.resolve(targetPath);
216
+ try {
217
+ const stats = fs.statSync(resolvedPath);
218
+ return stats.isDirectory() ? resolvedPath : path.dirname(resolvedPath);
219
+ }
220
+ catch {
221
+ return path.dirname(resolvedPath);
222
+ }
223
+ }
224
+ /**
225
+ * Finds .nornenv file by walking up from a specific file path.
226
+ */
227
+ function findEnvFileFromPath(filePath) {
228
+ let dir = getEnvSearchStartDirectory(filePath);
229
+ while (true) {
230
+ const envPath = path.join(dir, exports.ENV_FILENAME);
231
+ if (fs.existsSync(envPath)) {
232
+ return envPath;
233
+ }
234
+ const parentDir = path.dirname(dir);
235
+ if (parentDir === dir) {
236
+ break;
237
+ }
238
+ dir = parentDir;
239
+ }
240
+ return undefined;
241
+ }
242
+ function formatNornenvErrorLocation(rootEnvFilePath, errorFilePath, line) {
243
+ const baseDir = path.dirname(rootEnvFilePath);
244
+ const relativePath = path.relative(baseDir, errorFilePath);
245
+ const fileLabel = relativePath && relativePath !== '' ? relativePath : path.basename(errorFilePath);
246
+ return line >= 0 ? `${fileLabel}:${line + 1}` : fileLabel;
247
+ }
248
+ function formatNornenvErrorLocationForSource(sourceFilePath, errorFilePath, line) {
249
+ const envFilePath = findEnvFileFromPath(sourceFilePath);
250
+ return formatNornenvErrorLocation(envFilePath ?? sourceFilePath, errorFilePath, line);
251
+ }
252
+ /**
253
+ * Resolves imports in a .nornenv file, merging variables from all imported files.
254
+ *
255
+ * **Duplicate rules:**
256
+ * - Same variable name in `common` across two files → Error
257
+ * - Same variable name in the same `[env:X]` section across two files → Error
258
+ * - Variable in `common` (file A) + same name in `[env:X]` (file B) → Allowed (env overrides common)
259
+ *
260
+ * @param config - The parsed config of the entry file
261
+ * @param baseDir - The directory of the entry file (for resolving relative paths)
262
+ * @param entryFilePath - The absolute path of the entry file (for error reporting)
263
+ * @param readFile - Function to read file contents (allows async/sync flexibility)
264
+ * @param importStack - Active import chain for circular detection
265
+ * @param alreadyImported - Set of already-resolved absolute paths for deduplication
266
+ */
267
+ function resolveNornenvImports(config, baseDir, entryFilePath, readFile, importStack, alreadyImported) {
268
+ const errors = [];
269
+ const secretErrors = [];
270
+ const resolvedPaths = [];
271
+ const stack = importStack ?? [entryFilePath];
272
+ const visited = alreadyImported ?? new Set([entryFilePath]);
273
+ // Track variable origins for duplicate detection
274
+ // Key: "common:varName" or "env:envName:varName"
275
+ const variableOrigins = new Map();
276
+ // Register the entry file's own variables as originals
277
+ registerVariableOrigins(config, entryFilePath, variableOrigins);
278
+ // Import statements are only allowed at the top of the file.
279
+ for (const imp of config.misplacedImports) {
280
+ errors.push({
281
+ message: `Import statements must appear at the top of the file (before any variable declarations or environment sections).`,
282
+ filePath: entryFilePath,
283
+ line: imp.lineNumber
284
+ });
285
+ }
286
+ // Process each import
287
+ for (const imp of config.imports) {
288
+ const resolvedImportPath = resolveImportPath(imp.path, baseDir);
289
+ // Check file exists
290
+ if (!resolvedImportPath || !fs.existsSync(resolvedImportPath)) {
291
+ errors.push({
292
+ message: `Imported file not found: '${imp.path}'`,
293
+ filePath: entryFilePath,
294
+ line: imp.lineNumber
295
+ });
296
+ continue;
297
+ }
298
+ // Check it's a .nornenv file
299
+ if (!resolvedImportPath.endsWith('.nornenv')) {
300
+ errors.push({
301
+ message: `Import must reference a .nornenv file: '${imp.path}'`,
302
+ filePath: entryFilePath,
303
+ line: imp.lineNumber
304
+ });
305
+ continue;
306
+ }
307
+ const normalizedPath = path.resolve(resolvedImportPath);
308
+ // Check for circular imports
309
+ if (stack.includes(normalizedPath)) {
310
+ // Use paths relative to the entry file's directory for readable cycle display
311
+ const entryDir = path.dirname(stack[0]);
312
+ const cycle = [...stack, normalizedPath]
313
+ .map(p => path.relative(entryDir, p) || path.basename(p))
314
+ .join(' → ');
315
+ errors.push({
316
+ message: `Circular import detected: ${cycle}`,
317
+ filePath: entryFilePath,
318
+ line: imp.lineNumber
319
+ });
320
+ continue;
321
+ }
322
+ // Skip already imported files (deduplication)
323
+ if (visited.has(normalizedPath)) {
324
+ resolvedPaths.push(normalizedPath);
325
+ continue;
326
+ }
327
+ visited.add(normalizedPath);
328
+ // Read and parse the imported file
329
+ let importedContent;
330
+ try {
331
+ importedContent = readFile(normalizedPath);
332
+ }
333
+ catch {
334
+ errors.push({
335
+ message: `Failed to read imported file: '${imp.path}'`,
336
+ filePath: entryFilePath,
337
+ line: imp.lineNumber
338
+ });
339
+ continue;
340
+ }
341
+ const importedConfig = parseEnvFile(importedContent, normalizedPath);
342
+ resolvedPaths.push(normalizedPath);
343
+ // Recursively resolve chained imports
344
+ if (importedConfig.imports.length > 0) {
345
+ const childResult = resolveNornenvImports(importedConfig, path.dirname(normalizedPath), normalizedPath, readFile, [...stack, normalizedPath], visited);
346
+ errors.push(...childResult.errors);
347
+ secretErrors.push(...childResult.secretErrors);
348
+ resolvedPaths.push(...childResult.resolvedPaths);
349
+ // After recursion, importedConfig is already merged with its own imports
350
+ // The childResult.config has all the merged data
351
+ mergeConfigs(config, childResult.config, entryFilePath, normalizedPath, variableOrigins, errors);
352
+ }
353
+ else {
354
+ // No chained imports — merge directly
355
+ mergeConfigs(config, importedConfig, entryFilePath, normalizedPath, variableOrigins, errors);
356
+ }
357
+ }
358
+ return { config, errors, secretErrors, resolvedPaths };
359
+ }
360
+ /**
361
+ * Resolves an import path relative to a base directory.
362
+ */
363
+ function resolveImportPath(importPath, baseDir) {
364
+ // Remove quotes if present
365
+ const cleaned = importPath.replace(/^["']|["']$/g, '');
366
+ return path.resolve(baseDir, cleaned);
367
+ }
368
+ /**
369
+ * Registers variable origins from a config for duplicate tracking.
370
+ */
371
+ function registerVariableOrigins(config, filePath, origins) {
372
+ // We don't have per-variable line info from parseEnvFile, so use -1.
373
+ // Diagnostics provider will map line numbers from the actual document.
374
+ for (const varName of Object.keys(config.common)) {
375
+ origins.set(`common:${varName}`, { filePath, line: -1, varName });
376
+ }
377
+ for (const env of config.environments) {
378
+ for (const varName of Object.keys(env.variables)) {
379
+ origins.set(`env:${env.name}:${varName}`, { filePath, line: -1, varName });
380
+ }
381
+ }
382
+ for (const template of config.templates) {
383
+ for (const varName of Object.keys(template.variables)) {
384
+ origins.set(`template:${template.name}:${varName}`, { filePath, line: -1, varName });
385
+ }
386
+ }
387
+ }
388
+ /**
389
+ * Merges an imported config into the target config, checking for duplicate variables.
390
+ *
391
+ * **Same-layer duplicates are errors. Cross-layer (common vs env) is allowed.**
392
+ */
393
+ function mergeConfigs(target, source, targetFilePath, sourceFilePath, variableOrigins, errors) {
394
+ // Merge common variables
395
+ for (const [varName, varValue] of Object.entries(source.common)) {
396
+ const key = `common:${varName}`;
397
+ const existing = variableOrigins.get(key);
398
+ if (existing) {
399
+ const sourceLabel = toDisplayPath(sourceFilePath, targetFilePath);
400
+ const existingLabel = toDisplayPath(existing.filePath, targetFilePath);
401
+ errors.push({
402
+ message: `Duplicate variable '${varName}' in common section. Found in '${existingLabel}' and '${sourceLabel}'.`,
403
+ filePath: sourceFilePath,
404
+ line: -1 // Line resolved by diagnostics provider
405
+ });
406
+ }
407
+ else {
408
+ target.common[varName] = varValue;
409
+ variableOrigins.set(key, { filePath: sourceFilePath, line: -1, varName });
410
+ }
411
+ }
412
+ // Merge environment sections
413
+ for (const sourceEnv of source.environments) {
414
+ let targetEnv = target.environments.find(e => e.name === sourceEnv.name);
415
+ if (!targetEnv) {
416
+ targetEnv = { name: sourceEnv.name, variables: {}, parents: [...sourceEnv.parents] };
417
+ target.environments.push(targetEnv);
418
+ }
419
+ else {
420
+ // Union parents from both files (preserve order, dedupe).
421
+ for (const parent of sourceEnv.parents) {
422
+ if (!targetEnv.parents.includes(parent)) {
423
+ targetEnv.parents.push(parent);
424
+ }
425
+ }
426
+ }
427
+ for (const [varName, varValue] of Object.entries(sourceEnv.variables)) {
428
+ const key = `env:${sourceEnv.name}:${varName}`;
429
+ const existing = variableOrigins.get(key);
430
+ if (existing) {
431
+ const sourceLabel = toDisplayPath(sourceFilePath, targetFilePath);
432
+ const existingLabel = toDisplayPath(existing.filePath, targetFilePath);
433
+ errors.push({
434
+ message: `Duplicate variable '${varName}' in [env:${sourceEnv.name}] section. Found in '${existingLabel}' and '${sourceLabel}'.`,
435
+ filePath: sourceFilePath,
436
+ line: -1
437
+ });
438
+ }
439
+ else {
440
+ targetEnv.variables[varName] = varValue;
441
+ variableOrigins.set(key, { filePath: sourceFilePath, line: -1, varName });
442
+ }
443
+ }
444
+ }
445
+ // Merge template sections — same duplicate rules as envs.
446
+ for (const sourceTemplate of source.templates) {
447
+ let targetTemplate = target.templates.find(t => t.name === sourceTemplate.name);
448
+ if (!targetTemplate) {
449
+ targetTemplate = { name: sourceTemplate.name, variables: {}, parents: [...sourceTemplate.parents] };
450
+ target.templates.push(targetTemplate);
451
+ }
452
+ else {
453
+ for (const parent of sourceTemplate.parents) {
454
+ if (!targetTemplate.parents.includes(parent)) {
455
+ targetTemplate.parents.push(parent);
456
+ }
457
+ }
458
+ }
459
+ for (const [varName, varValue] of Object.entries(sourceTemplate.variables)) {
460
+ const key = `template:${sourceTemplate.name}:${varName}`;
461
+ const existing = variableOrigins.get(key);
462
+ if (existing) {
463
+ const sourceLabel = toDisplayPath(sourceFilePath, targetFilePath);
464
+ const existingLabel = toDisplayPath(existing.filePath, targetFilePath);
465
+ errors.push({
466
+ message: `Duplicate variable '${varName}' in [template:${sourceTemplate.name}] section. Found in '${existingLabel}' and '${sourceLabel}'.`,
467
+ filePath: sourceFilePath,
468
+ line: -1
469
+ });
470
+ }
471
+ else {
472
+ targetTemplate.variables[varName] = varValue;
473
+ variableOrigins.set(key, { filePath: sourceFilePath, line: -1, varName });
474
+ }
475
+ }
476
+ }
477
+ // Merge secrets
478
+ for (const name of source.secretNames) {
479
+ target.secretNames.add(name);
480
+ }
481
+ for (const [name, value] of source.secretValues) {
482
+ target.secretValues.set(name, value);
483
+ }
484
+ for (const declaration of source.secretDeclarations) {
485
+ target.secretDeclarations.push({ ...declaration, filePath: declaration.filePath ?? sourceFilePath });
486
+ }
487
+ }
488
+ /**
489
+ * Creates a readable path label relative to the entry file being resolved.
490
+ */
491
+ function toDisplayPath(filePath, entryFilePath) {
492
+ const entryDir = path.dirname(entryFilePath);
493
+ const relative = path.relative(entryDir, filePath);
494
+ return relative && relative !== '' ? relative : path.basename(filePath);
495
+ }
496
+ // ─── Convenience: Load + Resolve ─────────────────────────────────────────────
497
+ /**
498
+ * Parses a .nornenv file and resolves all its imports.
499
+ * Returns the fully merged config with any import errors.
500
+ */
501
+ function loadAndResolveEnvFile(filePath) {
502
+ const content = fs.readFileSync(filePath, 'utf-8');
503
+ const config = parseEnvFile(content, filePath);
504
+ if (config.imports.length === 0) {
505
+ const secretErrors = resolveEncryptedSecretValues(config, filePath);
506
+ return { config, errors: [], secretErrors, resolvedPaths: [] };
507
+ }
508
+ const result = resolveNornenvImports(config, path.dirname(filePath), filePath, (p) => fs.readFileSync(p, 'utf-8'));
509
+ result.secretErrors.push(...resolveEncryptedSecretValues(result.config, filePath));
510
+ return result;
511
+ }
512
+ /** Looks up a selectable env or reusable template by name. */
513
+ function findExtendsNode(name, config) {
514
+ const env = config.environments.find(e => e.name === name);
515
+ if (env) {
516
+ return { node: env, kind: 'env' };
517
+ }
518
+ const template = config.templates.find(t => t.name === name);
519
+ if (template) {
520
+ return { node: template, kind: 'template' };
521
+ }
522
+ return undefined;
523
+ }
524
+ /** Looks up a valid `extends` parent. Parents must be templates, never envs. */
525
+ function findExtendsTemplate(name, config) {
526
+ const template = config.templates.find(t => t.name === name);
527
+ return template ? { node: template, kind: 'template' } : undefined;
528
+ }
529
+ /**
530
+ * Walks the template `extends` chain of an env (or template) and returns the merged
531
+ * effective variables: `common` ← parent₁ ← parent₂ ← … ← self.
532
+ *
533
+ * Rules:
534
+ * - Left-to-right walk; right-most parent wins on collisions; self always wins over all parents.
535
+ * - Diamond inheritance applies a shared ancestor only once (first visit wins, then later
536
+ * parents may still override its values).
537
+ * - Cycles are broken silently here (skipped on re-encounter); the diagnostic provider is
538
+ * responsible for surfacing cycles to the user via `detectExtendsCycles`.
539
+ * - Env parents and unknown parent names are skipped silently; the diagnostic provider surfaces those too.
540
+ */
541
+ function resolveEffectiveEnvVariables(envName, config) {
542
+ return Object.fromEntries(Array.from(resolveEffectiveEnvVariableDetails(envName, config).entries())
543
+ .map(([name, detail]) => [name, detail.value]));
544
+ }
545
+ /**
546
+ * Returns inherited variables for an env or template, with the final source that wins for
547
+ * each variable after walking parents left-to-right.
548
+ */
549
+ function resolveInheritedVariableDetails(nodeName, config) {
550
+ const inherited = new Map();
551
+ const visited = new Set();
552
+ const stack = new Set();
553
+ const self = findExtendsNode(nodeName, config);
554
+ if (!self) {
555
+ return inherited;
556
+ }
557
+ const walk = (name) => {
558
+ if (visited.has(name) || stack.has(name)) {
559
+ return;
560
+ }
561
+ const found = findExtendsTemplate(name, config);
562
+ if (!found) {
563
+ return;
564
+ }
565
+ stack.add(name);
566
+ for (const parent of found.node.parents) {
567
+ walk(parent);
568
+ }
569
+ for (const [varName, value] of Object.entries(found.node.variables)) {
570
+ inherited.set(varName, {
571
+ name: varName,
572
+ value,
573
+ sourceKind: found.kind,
574
+ sourceName: found.node.name,
575
+ inherited: true
576
+ });
577
+ }
578
+ visited.add(name);
579
+ stack.delete(name);
580
+ };
581
+ for (const parent of self.node.parents) {
582
+ walk(parent);
583
+ }
584
+ return inherited;
585
+ }
586
+ /**
587
+ * Returns the effective variables for an env with source metadata:
588
+ * `common` ← parent₁ ← parent₂ ← … ← self.
589
+ */
590
+ function resolveEffectiveEnvVariableDetails(envName, config) {
591
+ const details = new Map();
592
+ for (const [name, value] of Object.entries(config.common)) {
593
+ details.set(name, {
594
+ name,
595
+ value,
596
+ sourceKind: 'common',
597
+ inherited: true
598
+ });
599
+ }
600
+ for (const [name, detail] of resolveInheritedVariableDetails(envName, config)) {
601
+ details.set(name, detail);
602
+ }
603
+ const env = config.environments.find(e => e.name === envName);
604
+ if (env) {
605
+ for (const [name, value] of Object.entries(env.variables)) {
606
+ details.set(name, {
607
+ name,
608
+ value,
609
+ sourceKind: 'env',
610
+ sourceName: env.name,
611
+ inherited: false
612
+ });
613
+ }
614
+ }
615
+ return details;
616
+ }
617
+ /**
618
+ * Returns the set of variable names contributed by the entire ancestor chain of `envName`,
619
+ * NOT including `common` and NOT including the env's own self-declared variables.
620
+ *
621
+ * Used to relax the scope-rule diagnostic so an env-section value may reference any
622
+ * variable defined in an ancestor template/env (not just `common`).
623
+ */
624
+ function collectAncestorVariableNames(envName, config) {
625
+ return new Set(resolveInheritedVariableDetails(envName, config).keys());
626
+ }
627
+ /**
628
+ * Detects cycles in the template `extends` graph.
629
+ * Returns one entry per cycle-back edge encountered.
630
+ */
631
+ function detectExtendsCycles(config) {
632
+ const cycles = [];
633
+ const visitedGlobally = new Set();
634
+ const visit = (name, kind, stack) => {
635
+ const stackIndex = stack.indexOf(name);
636
+ if (stackIndex >= 0) {
637
+ cycles.push({
638
+ name,
639
+ kind,
640
+ path: [...stack.slice(stackIndex), name]
641
+ });
642
+ return;
643
+ }
644
+ if (visitedGlobally.has(name)) {
645
+ return;
646
+ }
647
+ const found = findExtendsNode(name, config);
648
+ if (!found) {
649
+ return;
650
+ }
651
+ const nextStack = [...stack, name];
652
+ for (const parent of found.node.parents) {
653
+ const parentFound = findExtendsTemplate(parent, config);
654
+ if (!parentFound) {
655
+ continue;
656
+ }
657
+ visit(parent, parentFound.kind, nextStack);
658
+ }
659
+ visitedGlobally.add(name);
660
+ };
661
+ for (const env of config.environments) {
662
+ visit(env.name, 'env', []);
663
+ }
664
+ for (const template of config.templates) {
665
+ visit(template.name, 'template', []);
666
+ }
667
+ return cycles;
668
+ }
669
+ function resolveEncryptedSecretValues(config, entryFilePath) {
670
+ const errors = [];
671
+ for (const declaration of config.secretDeclarations) {
672
+ if (!(0, crypto_1.isEncryptedSecretValue)(declaration.value)) {
673
+ continue;
674
+ }
675
+ const parsed = (0, crypto_1.parseEncryptedSecretValue)(declaration.value);
676
+ const filePath = declaration.filePath ?? entryFilePath;
677
+ if (!parsed.ok) {
678
+ errors.push({
679
+ code: 'invalid-format',
680
+ message: parsed.error,
681
+ filePath,
682
+ line: declaration.lineNumber,
683
+ variableName: declaration.name
684
+ });
685
+ continue;
686
+ }
687
+ const { kid } = parsed.parsed;
688
+ const key = (0, keyStore_1.resolveSecretKey)(kid, filePath);
689
+ if (!key) {
690
+ errors.push({
691
+ code: 'missing-key',
692
+ message: `Missing secret key for kid '${kid}'.`,
693
+ filePath,
694
+ line: declaration.lineNumber,
695
+ variableName: declaration.name,
696
+ kid
697
+ });
698
+ continue;
699
+ }
700
+ let plaintext;
701
+ try {
702
+ plaintext = (0, crypto_1.decryptSecretValue)(declaration.value, key);
703
+ }
704
+ catch (error) {
705
+ const message = error instanceof Error ? error.message : String(error);
706
+ errors.push({
707
+ code: 'decrypt-failed',
708
+ message: `Failed to decrypt secret '${declaration.name}' with kid '${kid}': ${message}`,
709
+ filePath,
710
+ line: declaration.lineNumber,
711
+ variableName: declaration.name,
712
+ kid
713
+ });
714
+ continue;
715
+ }
716
+ declaration.value = plaintext;
717
+ if (declaration.envName) {
718
+ const env = config.environments.find(e => e.name === declaration.envName);
719
+ if (env) {
720
+ env.variables[declaration.name] = plaintext;
721
+ }
722
+ }
723
+ else if (declaration.templateName) {
724
+ const template = config.templates.find(t => t.name === declaration.templateName);
725
+ if (template) {
726
+ template.variables[declaration.name] = plaintext;
727
+ }
728
+ }
729
+ else {
730
+ config.common[declaration.name] = plaintext;
731
+ }
732
+ config.secretValues.set(declaration.name, plaintext);
733
+ }
734
+ return errors;
735
+ }
736
+ //# sourceMappingURL=environmentParser.js.map