norn-cli 2.3.0 → 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.
- package/.claude/skills/norn-social-campaign/SKILL.md +70 -0
- package/CHANGELOG.md +6 -0
- package/demos/nornenv-region-refactor/README.md +64 -0
- package/dist/cli.js +360 -1
- package/out/apiResponseIntellisenseCache.js +394 -0
- package/out/assertionRunner.js +567 -0
- package/out/cacheDir.js +136 -0
- package/out/chatParticipant.js +763 -0
- package/out/cli/colors.js +127 -0
- package/out/cli/formatters/assertion.js +102 -0
- package/out/cli/formatters/index.js +23 -0
- package/out/cli/formatters/response.js +106 -0
- package/out/cli/formatters/summary.js +246 -0
- package/out/cli/redaction.js +237 -0
- package/out/cli/reporters/html.js +689 -0
- package/out/cli/reporters/index.js +22 -0
- package/out/cli/reporters/junit.js +226 -0
- package/out/codeLensProvider.js +351 -0
- package/out/compareContentProvider.js +85 -0
- package/out/completionProvider.js +3739 -0
- package/out/contractAssertionSummary.js +225 -0
- package/out/contractDecorationProvider.js +243 -0
- package/out/coverageCalculator.js +879 -0
- package/out/coveragePanel.js +597 -0
- package/out/debug/breakpointResolver.js +84 -0
- package/out/debug/breakpoints.js +52 -0
- package/out/debug/nornDebugAdapter.js +166 -0
- package/out/debug/nornDebugSession.js +613 -0
- package/out/debug/sequenceLocationIndex.js +77 -0
- package/out/debug/types.js +3 -0
- package/out/deepClone.js +21 -0
- package/out/diagnosticProvider.js +2554 -0
- package/out/environmentParser.js +736 -0
- package/out/environmentProvider.js +544 -0
- package/out/environmentTemplates.js +146 -0
- package/out/errors/formatError.js +113 -0
- package/out/errors/nornError.js +29 -0
- package/out/formUrlEncoded.js +89 -0
- package/out/httpClient.js +348 -0
- package/out/httpRuntimeOptions.js +16 -0
- package/out/importErrors.js +31 -0
- package/out/inlayHintResolver.js +70 -0
- package/out/jsonFileReader.js +323 -0
- package/out/mcpClient.js +193 -0
- package/out/mcpConfig.js +184 -0
- package/out/mcpToolIntellisenseCache.js +96 -0
- package/out/mcpToolSchema.js +50 -0
- package/out/nornConfig.js +132 -0
- package/out/nornHoverProvider.js +124 -0
- package/out/nornInlayHintsProvider.js +191 -0
- package/out/nornPrompt.js +755 -0
- package/out/nornSqlParser.js +286 -0
- package/out/nornapiHoverProvider.js +135 -0
- package/out/nornapiInlayHintsProvider.js +94 -0
- package/out/nornapiParser.js +324 -0
- package/out/nornenvCodeActionProvider.js +101 -0
- package/out/nornenvDecorationProvider.js +239 -0
- package/out/nornenvFoldingProvider.js +63 -0
- package/out/nornenvHoverProvider.js +114 -0
- package/out/nornenvInlayHintsProvider.js +99 -0
- package/out/nornenvLanguageModel.js +187 -0
- package/out/nornenvRegionRefactor.js +267 -0
- package/out/nornsqlHoverProvider.js +95 -0
- package/out/nornsqlInlayHintsProvider.js +114 -0
- package/out/parser.js +839 -0
- package/out/pathAccess.js +28 -0
- package/out/postmanImportPanel.js +732 -0
- package/out/postmanImportPlanner.js +1155 -0
- package/out/postmanImportSidebarView.js +532 -0
- package/out/quotedString.js +35 -0
- package/out/requestPreparation.js +179 -0
- package/out/requestValidation.js +146 -0
- package/out/responsePanel.js +7754 -0
- package/out/schemaGenerator.js +562 -0
- package/out/scriptRunner.js +419 -0
- package/out/secrets/cliSecrets.js +415 -0
- package/out/secrets/crypto.js +105 -0
- package/out/secrets/envFileSecrets.js +177 -0
- package/out/secrets/keyStore.js +259 -0
- package/out/sequenceDeclaration.js +15 -0
- package/out/sequenceRunner.js +3590 -0
- package/out/sqlAdapterRunner.js +122 -0
- package/out/sqlBuiltInAdapters.js +604 -0
- package/out/sqlConfig.js +184 -0
- package/out/starterCatalog.js +554 -0
- package/out/stringUtils.js +25 -0
- package/out/swaggerBodyIntellisenseCache.js +114 -0
- package/out/swaggerParser.js +464 -0
- package/out/testProvider.js +767 -0
- package/out/theoryCaseLoader.js +113 -0
- package/out/validationCache.js +211 -0
- package/package.json +6 -1
|
@@ -0,0 +1,1155 @@
|
|
|
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.buildPostmanImportPlan = buildPostmanImportPlan;
|
|
37
|
+
exports.writePostmanImportPlan = writePostmanImportPlan;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const SUPPORTED_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']);
|
|
41
|
+
const SECRET_NAME_REGEX = /(token|secret|password|passwd|pwd|api[_-]?key|access[_-]?key|client[_-]?secret|authorization)/i;
|
|
42
|
+
function buildPostmanImportPlan(kind, sourceFilePaths, destinationFolder) {
|
|
43
|
+
const warnings = [];
|
|
44
|
+
const blockers = [];
|
|
45
|
+
const files = [];
|
|
46
|
+
if (sourceFilePaths.length === 0) {
|
|
47
|
+
blockers.push({ code: 'no-source', message: 'No Postman JSON file selected.' });
|
|
48
|
+
return {
|
|
49
|
+
kind,
|
|
50
|
+
summary: createEmptySummary(kind, destinationFolder, sourceFilePaths.length),
|
|
51
|
+
files,
|
|
52
|
+
warnings,
|
|
53
|
+
blockers,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (kind === 'collection') {
|
|
57
|
+
const collectionPath = sourceFilePaths[0];
|
|
58
|
+
const build = buildCollectionImport(collectionPath, destinationFolder, warnings, blockers);
|
|
59
|
+
if (build) {
|
|
60
|
+
files.push(...build.files);
|
|
61
|
+
blockers.push(...collectCollisionBlockers(build.files));
|
|
62
|
+
return {
|
|
63
|
+
kind,
|
|
64
|
+
summary: {
|
|
65
|
+
title: build.title,
|
|
66
|
+
kind,
|
|
67
|
+
sourceFileCount: 1,
|
|
68
|
+
folderCount: build.folderCount,
|
|
69
|
+
requestCount: build.requestCount,
|
|
70
|
+
environmentCount: 0,
|
|
71
|
+
variableCount: build.variableCount,
|
|
72
|
+
secretCount: 0,
|
|
73
|
+
fileCount: build.files.length,
|
|
74
|
+
destinationFolder,
|
|
75
|
+
},
|
|
76
|
+
files: build.files,
|
|
77
|
+
warnings,
|
|
78
|
+
blockers,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const build = buildEnvironmentImport(sourceFilePaths, destinationFolder, warnings, blockers);
|
|
84
|
+
if (build) {
|
|
85
|
+
files.push(build.file);
|
|
86
|
+
blockers.push(...collectCollisionBlockers([build.file]));
|
|
87
|
+
return {
|
|
88
|
+
kind,
|
|
89
|
+
summary: {
|
|
90
|
+
title: build.title,
|
|
91
|
+
kind,
|
|
92
|
+
sourceFileCount: sourceFilePaths.length,
|
|
93
|
+
folderCount: 0,
|
|
94
|
+
requestCount: 0,
|
|
95
|
+
environmentCount: build.environmentCount,
|
|
96
|
+
variableCount: build.variableCount,
|
|
97
|
+
secretCount: build.secretCount,
|
|
98
|
+
fileCount: 1,
|
|
99
|
+
destinationFolder,
|
|
100
|
+
},
|
|
101
|
+
files,
|
|
102
|
+
warnings,
|
|
103
|
+
blockers,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
kind,
|
|
109
|
+
summary: createEmptySummary(kind, destinationFolder, sourceFilePaths.length),
|
|
110
|
+
files,
|
|
111
|
+
warnings,
|
|
112
|
+
blockers,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function writePostmanImportPlan(plan) {
|
|
116
|
+
const result = {
|
|
117
|
+
createdFiles: [],
|
|
118
|
+
skippedFiles: [],
|
|
119
|
+
};
|
|
120
|
+
if (plan.blockers.length > 0) {
|
|
121
|
+
return {
|
|
122
|
+
createdFiles: result.createdFiles,
|
|
123
|
+
skippedFiles: result.skippedFiles,
|
|
124
|
+
warningCount: plan.warnings.length,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
for (const file of plan.files) {
|
|
128
|
+
if (file.existsCollision && file.writeMode !== 'append') {
|
|
129
|
+
result.skippedFiles.push(file.relativePath);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
fs.mkdirSync(path.dirname(file.absolutePath), { recursive: true });
|
|
133
|
+
if (file.writeMode === 'append') {
|
|
134
|
+
const existingText = fs.existsSync(file.absolutePath) ? fs.readFileSync(file.absolutePath, 'utf8') : '';
|
|
135
|
+
const contentToAppend = existingText.length > 0 && !existingText.endsWith('\n')
|
|
136
|
+
? `\n${file.content}`
|
|
137
|
+
: file.content;
|
|
138
|
+
fs.appendFileSync(file.absolutePath, contentToAppend, 'utf8');
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
fs.writeFileSync(file.absolutePath, file.content, 'utf8');
|
|
142
|
+
}
|
|
143
|
+
result.createdFiles.push(file.relativePath);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
createdFiles: result.createdFiles,
|
|
147
|
+
skippedFiles: result.skippedFiles,
|
|
148
|
+
warningCount: plan.warnings.length,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function createEmptySummary(kind, destinationFolder, sourceFileCount) {
|
|
152
|
+
return {
|
|
153
|
+
title: kind === 'collection' ? 'Postman Collection Import' : 'Postman Environment Import',
|
|
154
|
+
kind,
|
|
155
|
+
sourceFileCount,
|
|
156
|
+
folderCount: 0,
|
|
157
|
+
requestCount: 0,
|
|
158
|
+
environmentCount: 0,
|
|
159
|
+
variableCount: 0,
|
|
160
|
+
secretCount: 0,
|
|
161
|
+
fileCount: 0,
|
|
162
|
+
destinationFolder,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function buildCollectionImport(sourceFilePath, destinationFolder, warnings, blockers) {
|
|
166
|
+
const root = readJsonObject(sourceFilePath, blockers);
|
|
167
|
+
if (!root) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
const info = asRecord(root.info);
|
|
171
|
+
const collectionTitle = asString(info?.name) || path.basename(sourceFilePath, '.json');
|
|
172
|
+
const schema = asString(info?.schema);
|
|
173
|
+
if (!schema || !schema.includes('collection')) {
|
|
174
|
+
blockers.push({
|
|
175
|
+
code: 'unsupported-schema',
|
|
176
|
+
message: 'Selected JSON does not look like a Postman Collection export.',
|
|
177
|
+
path: sourceFilePath,
|
|
178
|
+
});
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
if (!schema.includes('v2.1')) {
|
|
182
|
+
warnings.push({
|
|
183
|
+
code: 'schema-version',
|
|
184
|
+
message: `Collection schema is "${schema}". v1 targets Postman Collection v2.1 and may import partially.`,
|
|
185
|
+
sourcePath: sourceFilePath,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const itemList = asArray(root.item);
|
|
189
|
+
if (!itemList || itemList.length === 0) {
|
|
190
|
+
blockers.push({
|
|
191
|
+
code: 'empty-collection',
|
|
192
|
+
message: 'Collection has no items to import.',
|
|
193
|
+
path: sourceFilePath,
|
|
194
|
+
});
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
const collectionVariables = readPostmanVariables(root.variable);
|
|
198
|
+
const build = collectCollectionRequests(itemList, collectionTitle, warnings, sourceFilePath, asRecord(root.auth));
|
|
199
|
+
if (build.requests.length === 0) {
|
|
200
|
+
blockers.push({
|
|
201
|
+
code: 'no-requests',
|
|
202
|
+
message: 'No importable requests were found in the collection.',
|
|
203
|
+
path: sourceFilePath,
|
|
204
|
+
});
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
build.collectionVariables = collectionVariables;
|
|
208
|
+
const apiFileBase = `${toKebab(collectionTitle) || 'postman-collection'}.nornapi`;
|
|
209
|
+
const apiFileRelative = apiFileBase;
|
|
210
|
+
const apiFileAbsolute = path.join(destinationFolder, apiFileRelative);
|
|
211
|
+
const headerGroups = assignReusableHeaderGroups(build.requests);
|
|
212
|
+
const activeCollectionVariables = build.collectionVariables.filter(v => !v.disabled);
|
|
213
|
+
const files = [];
|
|
214
|
+
if (activeCollectionVariables.length > 0) {
|
|
215
|
+
const envRelative = '.nornenv';
|
|
216
|
+
const envAbsolute = path.join(destinationFolder, envRelative);
|
|
217
|
+
files.push({
|
|
218
|
+
relativePath: envRelative,
|
|
219
|
+
absolutePath: envAbsolute,
|
|
220
|
+
language: 'nornenv',
|
|
221
|
+
content: generateNornEnvContent([], activeCollectionVariables.map(variable => ({
|
|
222
|
+
key: variable.key,
|
|
223
|
+
value: variable.value,
|
|
224
|
+
secret: false,
|
|
225
|
+
})), 'Postman collection variables'),
|
|
226
|
+
existsCollision: fs.existsSync(envAbsolute),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
files.push({
|
|
230
|
+
relativePath: normalizeRelativePath(apiFileRelative),
|
|
231
|
+
absolutePath: apiFileAbsolute,
|
|
232
|
+
language: 'nornapi',
|
|
233
|
+
content: generateNornApiContent(collectionTitle, build.requests, headerGroups),
|
|
234
|
+
existsCollision: fs.existsSync(apiFileAbsolute),
|
|
235
|
+
});
|
|
236
|
+
const requestsByFile = new Map();
|
|
237
|
+
for (const request of build.requests) {
|
|
238
|
+
const key = getFolderFileRelativePath(request.folderPath);
|
|
239
|
+
const list = requestsByFile.get(key) ?? [];
|
|
240
|
+
list.push(request);
|
|
241
|
+
requestsByFile.set(key, list);
|
|
242
|
+
}
|
|
243
|
+
const folderFiles = new Set();
|
|
244
|
+
for (const folder of build.folders) {
|
|
245
|
+
folderFiles.add(folder.fileRelativePath);
|
|
246
|
+
}
|
|
247
|
+
for (const fileRelative of requestsByFile.keys()) {
|
|
248
|
+
folderFiles.add(fileRelative);
|
|
249
|
+
}
|
|
250
|
+
const sortedNornFiles = Array.from(folderFiles).sort((a, b) => a.localeCompare(b));
|
|
251
|
+
for (const fileRelative of sortedNornFiles) {
|
|
252
|
+
const nornRequests = requestsByFile.get(fileRelative) ?? [];
|
|
253
|
+
const absolutePath = path.join(destinationFolder, fileRelative);
|
|
254
|
+
files.push({
|
|
255
|
+
relativePath: normalizeRelativePath(fileRelative),
|
|
256
|
+
absolutePath,
|
|
257
|
+
language: 'norn',
|
|
258
|
+
content: generateNornFileContent(fileRelative, apiFileRelative, nornRequests, headerGroups, warnings),
|
|
259
|
+
existsCollision: fs.existsSync(absolutePath),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
title: collectionTitle,
|
|
264
|
+
files,
|
|
265
|
+
folderCount: build.folders.length,
|
|
266
|
+
requestCount: build.requests.length,
|
|
267
|
+
variableCount: build.collectionVariables.filter(v => !v.disabled).length,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function buildEnvironmentImport(sourceFilePaths, destinationFolder, warnings, blockers) {
|
|
271
|
+
const sections = [];
|
|
272
|
+
const usedNames = new Set();
|
|
273
|
+
let variableCount = 0;
|
|
274
|
+
let secretCount = 0;
|
|
275
|
+
for (const sourceFilePath of sourceFilePaths) {
|
|
276
|
+
const root = readJsonObject(sourceFilePath, blockers);
|
|
277
|
+
if (!root) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const schema = asString(root._postman_variable_scope) || asString(root._postman_exported_using);
|
|
281
|
+
const envNameRaw = asString(root.name) || path.basename(sourceFilePath, '.json');
|
|
282
|
+
const envName = sanitizeEnvName(envNameRaw);
|
|
283
|
+
if (!envName) {
|
|
284
|
+
blockers.push({
|
|
285
|
+
code: 'invalid-env-name',
|
|
286
|
+
message: `Could not derive a valid environment name from "${envNameRaw}".`,
|
|
287
|
+
path: sourceFilePath,
|
|
288
|
+
});
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (usedNames.has(envName)) {
|
|
292
|
+
blockers.push({
|
|
293
|
+
code: 'duplicate-env-name',
|
|
294
|
+
message: `Duplicate environment name after normalization: "${envName}".`,
|
|
295
|
+
path: sourceFilePath,
|
|
296
|
+
});
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
usedNames.add(envName);
|
|
300
|
+
if (schema && !String(schema).toLowerCase().includes('environment')) {
|
|
301
|
+
warnings.push({
|
|
302
|
+
code: 'environment-shape',
|
|
303
|
+
message: `File may not be a standard Postman Environment export (${schema}). Attempting import.`,
|
|
304
|
+
sourcePath: sourceFilePath,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const valuesArray = asArray(root.values);
|
|
308
|
+
if (!valuesArray) {
|
|
309
|
+
warnings.push({
|
|
310
|
+
code: 'env-values-missing',
|
|
311
|
+
message: `Environment "${envName}" has no values array.`,
|
|
312
|
+
sourcePath: sourceFilePath,
|
|
313
|
+
});
|
|
314
|
+
sections.push({ name: envName, variables: [] });
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const variables = [];
|
|
318
|
+
for (const valueEntry of valuesArray) {
|
|
319
|
+
const entry = asRecord(valueEntry);
|
|
320
|
+
if (!entry) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const key = asString(entry.key);
|
|
324
|
+
if (!key) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const disabled = entry.enabled === false || entry.disabled === true;
|
|
328
|
+
if (disabled) {
|
|
329
|
+
warnings.push({
|
|
330
|
+
code: 'disabled-env-var',
|
|
331
|
+
message: `Skipped disabled environment value "${key}".`,
|
|
332
|
+
sourcePath: sourceFilePath,
|
|
333
|
+
});
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const value = stringifyScalar(entry.value);
|
|
337
|
+
const secret = SECRET_NAME_REGEX.test(key);
|
|
338
|
+
if (secret) {
|
|
339
|
+
secretCount += 1;
|
|
340
|
+
}
|
|
341
|
+
variableCount += 1;
|
|
342
|
+
variables.push({ key, value, secret });
|
|
343
|
+
}
|
|
344
|
+
sections.push({ name: envName, variables });
|
|
345
|
+
}
|
|
346
|
+
if (sections.length === 0) {
|
|
347
|
+
blockers.push({
|
|
348
|
+
code: 'no-env-sections',
|
|
349
|
+
message: 'No importable Postman environments were found in the selected JSON file(s).',
|
|
350
|
+
});
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
const envRelative = '.nornenv';
|
|
354
|
+
const envAbsolute = path.join(destinationFolder, envRelative);
|
|
355
|
+
const envFileExists = fs.existsSync(envAbsolute);
|
|
356
|
+
const existingEnvNames = envFileExists ? readExistingNornenvEnvironmentNames(envAbsolute) : new Set();
|
|
357
|
+
if (envFileExists) {
|
|
358
|
+
warnings.push({
|
|
359
|
+
code: 'nornenv-append-merge',
|
|
360
|
+
message: 'Destination already contains a .nornenv file. Imported environments will be appended; resolve conflicts manually if needed.',
|
|
361
|
+
sourcePath: envAbsolute,
|
|
362
|
+
});
|
|
363
|
+
for (const section of sections) {
|
|
364
|
+
if (existingEnvNames.has(section.name)) {
|
|
365
|
+
warnings.push({
|
|
366
|
+
code: 'existing-env-name-conflict',
|
|
367
|
+
message: `Environment "${section.name}" already exists in .nornenv and will be appended again.`,
|
|
368
|
+
sourcePath: envAbsolute,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const file = {
|
|
374
|
+
relativePath: envRelative,
|
|
375
|
+
absolutePath: envAbsolute,
|
|
376
|
+
language: 'nornenv',
|
|
377
|
+
content: envFileExists ? generateNornEnvAppendContent(sections) : generateNornEnvContent(sections),
|
|
378
|
+
existsCollision: envFileExists,
|
|
379
|
+
writeMode: envFileExists ? 'append' : 'write',
|
|
380
|
+
};
|
|
381
|
+
const title = sections.length === 1 ? sections[0].name : `Postman Environments (${sections.length})`;
|
|
382
|
+
return {
|
|
383
|
+
title,
|
|
384
|
+
file,
|
|
385
|
+
environmentCount: sections.length,
|
|
386
|
+
variableCount,
|
|
387
|
+
secretCount,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function collectCollisionBlockers(files) {
|
|
391
|
+
return files
|
|
392
|
+
.filter(file => file.existsCollision && file.writeMode !== 'append')
|
|
393
|
+
.map(file => ({
|
|
394
|
+
code: 'file-overlap',
|
|
395
|
+
message: `Target file already exists: ${file.relativePath}`,
|
|
396
|
+
path: file.absolutePath,
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
function readJsonObject(sourceFilePath, blockers) {
|
|
400
|
+
try {
|
|
401
|
+
const text = fs.readFileSync(sourceFilePath, 'utf8');
|
|
402
|
+
const parsed = JSON.parse(text);
|
|
403
|
+
const obj = asRecord(parsed);
|
|
404
|
+
if (!obj) {
|
|
405
|
+
blockers.push({
|
|
406
|
+
code: 'json-root-not-object',
|
|
407
|
+
message: 'JSON root is not an object.',
|
|
408
|
+
path: sourceFilePath,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return obj ?? undefined;
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
blockers.push({
|
|
415
|
+
code: 'json-read-failed',
|
|
416
|
+
message: `Failed to read or parse JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
417
|
+
path: sourceFilePath,
|
|
418
|
+
});
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function collectCollectionRequests(itemList, collectionTitle, warnings, sourcePath, collectionAuth) {
|
|
423
|
+
const folders = [];
|
|
424
|
+
const requests = [];
|
|
425
|
+
const endpointNames = new Set();
|
|
426
|
+
const requestNames = new Set();
|
|
427
|
+
const folderNameCounters = new Map();
|
|
428
|
+
const rootFolderAuth = collectionAuth;
|
|
429
|
+
function walk(items, folderPath, inheritedAuth) {
|
|
430
|
+
const siblingFolderCounts = new Map();
|
|
431
|
+
for (const itemUnknown of items) {
|
|
432
|
+
const item = asRecord(itemUnknown);
|
|
433
|
+
if (!item) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const itemNameRaw = asString(item.name) || 'item';
|
|
437
|
+
const itemAuth = asRecord(item.auth);
|
|
438
|
+
const effectiveAuth = itemAuth ?? inheritedAuth;
|
|
439
|
+
const childItems = asArray(item.item);
|
|
440
|
+
if (childItems) {
|
|
441
|
+
const baseSegment = toKebab(itemNameRaw) || 'folder';
|
|
442
|
+
const dedupedSegment = dedupeSlug(baseSegment, siblingFolderCounts);
|
|
443
|
+
const nextPath = [...folderPath, dedupedSegment];
|
|
444
|
+
const fileRelativePath = getFolderFileRelativePath(nextPath);
|
|
445
|
+
folders.push({ pathSegments: nextPath, fileRelativePath });
|
|
446
|
+
walk(childItems, nextPath, effectiveAuth);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const requestValue = item.request;
|
|
450
|
+
const requestRecord = typeof requestValue === 'string' ? undefined : asRecord(requestValue);
|
|
451
|
+
const methodRaw = asString(requestRecord?.method) || 'GET';
|
|
452
|
+
const method = methodRaw.toUpperCase();
|
|
453
|
+
if (!SUPPORTED_METHODS.has(method)) {
|
|
454
|
+
warnings.push({
|
|
455
|
+
code: 'unsupported-method',
|
|
456
|
+
message: `Request "${itemNameRaw}" uses unsupported method "${methodRaw}". Skipped.`,
|
|
457
|
+
sourcePath,
|
|
458
|
+
requestName: itemNameRaw,
|
|
459
|
+
});
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const urlRaw = buildRawUrl(requestRecord?.url);
|
|
463
|
+
if (!urlRaw) {
|
|
464
|
+
warnings.push({
|
|
465
|
+
code: 'missing-url',
|
|
466
|
+
message: `Request "${itemNameRaw}" has no URL. Skipped.`,
|
|
467
|
+
sourcePath,
|
|
468
|
+
requestName: itemNameRaw,
|
|
469
|
+
});
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const requestWarnings = [];
|
|
473
|
+
const authHeadersAndUrl = resolveAuthToHeadersAndUrl(method, urlRaw, requestRecord?.auth, effectiveAuth, sourcePath, itemNameRaw);
|
|
474
|
+
requestWarnings.push(...authHeadersAndUrl.warnings);
|
|
475
|
+
const explicitHeaders = readHeaders(requestRecord?.header);
|
|
476
|
+
const mergedHeaders = mergeHeaders(explicitHeaders, authHeadersAndUrl.headers);
|
|
477
|
+
const body = renderRequestBody(requestRecord?.body, sourcePath, itemNameRaw);
|
|
478
|
+
requestWarnings.push(...body.warnings);
|
|
479
|
+
if (asArray(item.event)?.length) {
|
|
480
|
+
requestWarnings.push({
|
|
481
|
+
code: 'postman-events-skipped',
|
|
482
|
+
message: `Postman scripts/events on request "${itemNameRaw}" are not converted.`,
|
|
483
|
+
sourcePath,
|
|
484
|
+
requestName: itemNameRaw,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
warnings.push(...requestWarnings);
|
|
488
|
+
const endpointName = dedupeIdentifier(toPascalIdentifier(itemNameRaw) || `${method}${toPascalIdentifier(collectionTitle) || 'Request'}`, endpointNames);
|
|
489
|
+
const namedRequestName = dedupeIdentifier(toPascalIdentifier(itemNameRaw) || `${endpointName}Request`, requestNames);
|
|
490
|
+
const endpointUrl = convertPostmanUrlToNornEndpoint(authHeadersAndUrl.url);
|
|
491
|
+
const endpointParamNames = extractEndpointParamNames(endpointUrl);
|
|
492
|
+
const endpointCall = endpointParamNames.length > 0
|
|
493
|
+
? `${endpointName}(${endpointParamNames.map(name => `{{${name}}}`).join(', ')})`
|
|
494
|
+
: endpointName;
|
|
495
|
+
requests.push({
|
|
496
|
+
name: itemNameRaw,
|
|
497
|
+
endpointName,
|
|
498
|
+
namedRequestName,
|
|
499
|
+
folderPath,
|
|
500
|
+
method,
|
|
501
|
+
endpointUrl,
|
|
502
|
+
endpointCall,
|
|
503
|
+
inlineHeaders: mergedHeaders,
|
|
504
|
+
headerGroupSignature: mergedHeaders.length > 0 ? headerSignature(mergedHeaders) : undefined,
|
|
505
|
+
bodyText: body.bodyText,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Fold top-level duplicate folder names deterministically across multiple passes if needed
|
|
510
|
+
void folderNameCounters;
|
|
511
|
+
walk(itemList, [], rootFolderAuth);
|
|
512
|
+
return {
|
|
513
|
+
title: collectionTitle,
|
|
514
|
+
folders,
|
|
515
|
+
requests,
|
|
516
|
+
collectionVariables: [],
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function readPostmanVariables(value) {
|
|
520
|
+
const arr = asArray(value);
|
|
521
|
+
if (!arr) {
|
|
522
|
+
return [];
|
|
523
|
+
}
|
|
524
|
+
const result = [];
|
|
525
|
+
for (const entryUnknown of arr) {
|
|
526
|
+
const entry = asRecord(entryUnknown);
|
|
527
|
+
if (!entry) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const key = asString(entry.key);
|
|
531
|
+
if (!key) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const disabled = entry.enabled === false || entry.disabled === true;
|
|
535
|
+
result.push({
|
|
536
|
+
key,
|
|
537
|
+
value: stringifyScalar(entry.value),
|
|
538
|
+
disabled,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
return result;
|
|
542
|
+
}
|
|
543
|
+
function buildRawUrl(urlValue) {
|
|
544
|
+
if (typeof urlValue === 'string') {
|
|
545
|
+
return urlValue.trim();
|
|
546
|
+
}
|
|
547
|
+
const url = asRecord(urlValue);
|
|
548
|
+
if (!url) {
|
|
549
|
+
return undefined;
|
|
550
|
+
}
|
|
551
|
+
const raw = asString(url.raw);
|
|
552
|
+
if (raw) {
|
|
553
|
+
return raw.trim();
|
|
554
|
+
}
|
|
555
|
+
const protocol = asString(url.protocol);
|
|
556
|
+
const host = buildHost(url.host);
|
|
557
|
+
const pathText = buildPath(url.path);
|
|
558
|
+
const queryText = buildQuery(url.query);
|
|
559
|
+
if (!host && !pathText) {
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
562
|
+
const protocolPrefix = protocol ? `${protocol}://` : '';
|
|
563
|
+
const base = `${protocolPrefix}${host || ''}${pathText}`;
|
|
564
|
+
return queryText ? `${base}${queryText}` : base;
|
|
565
|
+
}
|
|
566
|
+
function buildHost(hostValue) {
|
|
567
|
+
if (typeof hostValue === 'string') {
|
|
568
|
+
return hostValue;
|
|
569
|
+
}
|
|
570
|
+
const hostArr = asArray(hostValue);
|
|
571
|
+
if (!hostArr) {
|
|
572
|
+
return '';
|
|
573
|
+
}
|
|
574
|
+
const parts = hostArr.map(part => stringifyScalar(part)).filter(Boolean);
|
|
575
|
+
return parts.join('.');
|
|
576
|
+
}
|
|
577
|
+
function buildPath(pathValue) {
|
|
578
|
+
if (typeof pathValue === 'string') {
|
|
579
|
+
return pathValue.startsWith('/') ? pathValue : `/${pathValue}`;
|
|
580
|
+
}
|
|
581
|
+
const pathArr = asArray(pathValue);
|
|
582
|
+
if (!pathArr) {
|
|
583
|
+
return '';
|
|
584
|
+
}
|
|
585
|
+
const parts = pathArr.map(part => stringifyScalar(part)).filter(Boolean);
|
|
586
|
+
if (parts.length === 0) {
|
|
587
|
+
return '';
|
|
588
|
+
}
|
|
589
|
+
return `/${parts.join('/')}`;
|
|
590
|
+
}
|
|
591
|
+
function buildQuery(queryValue) {
|
|
592
|
+
const queryArr = asArray(queryValue);
|
|
593
|
+
if (!queryArr) {
|
|
594
|
+
return '';
|
|
595
|
+
}
|
|
596
|
+
const parts = [];
|
|
597
|
+
for (const itemUnknown of queryArr) {
|
|
598
|
+
const item = asRecord(itemUnknown);
|
|
599
|
+
if (!item) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (item.disabled === true) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const key = asString(item.key);
|
|
606
|
+
if (!key) {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const value = asString(item.value);
|
|
610
|
+
parts.push(value !== undefined ? `${key}=${value}` : key);
|
|
611
|
+
}
|
|
612
|
+
return parts.length > 0 ? `?${parts.join('&')}` : '';
|
|
613
|
+
}
|
|
614
|
+
function resolveAuthToHeadersAndUrl(method, rawUrl, requestAuthValue, inheritedAuthValue, sourcePath, requestName) {
|
|
615
|
+
const warnings = [];
|
|
616
|
+
const headers = [];
|
|
617
|
+
let url = rawUrl;
|
|
618
|
+
const requestAuth = asRecord(requestAuthValue);
|
|
619
|
+
const inheritedAuth = asRecord(inheritedAuthValue);
|
|
620
|
+
const auth = requestAuth ?? inheritedAuth;
|
|
621
|
+
if (!auth) {
|
|
622
|
+
return { headers, url, warnings };
|
|
623
|
+
}
|
|
624
|
+
const type = asString(auth.type)?.toLowerCase();
|
|
625
|
+
if (!type || type === 'noauth') {
|
|
626
|
+
return { headers, url, warnings };
|
|
627
|
+
}
|
|
628
|
+
if (type === 'bearer') {
|
|
629
|
+
const attrs = readKeyValueMap(auth.bearer);
|
|
630
|
+
const token = attrs.get('token');
|
|
631
|
+
if (token) {
|
|
632
|
+
headers.push({ name: 'Authorization', value: `Bearer ${token}` });
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
warnings.push({
|
|
636
|
+
code: 'auth-bearer-missing-token',
|
|
637
|
+
message: `Bearer auth on "${requestName}" is missing a token value.`,
|
|
638
|
+
sourcePath,
|
|
639
|
+
requestName,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
return { headers, url, warnings };
|
|
643
|
+
}
|
|
644
|
+
if (type === 'basic') {
|
|
645
|
+
const attrs = readKeyValueMap(auth.basic);
|
|
646
|
+
const username = attrs.get('username') ?? '';
|
|
647
|
+
const password = attrs.get('password') ?? '';
|
|
648
|
+
const encoded = Buffer.from(`${username}:${password}`, 'utf8').toString('base64');
|
|
649
|
+
headers.push({ name: 'Authorization', value: `Basic ${encoded}` });
|
|
650
|
+
return { headers, url, warnings };
|
|
651
|
+
}
|
|
652
|
+
if (type === 'apikey') {
|
|
653
|
+
const attrs = readKeyValueMap(auth.apikey);
|
|
654
|
+
const key = attrs.get('key');
|
|
655
|
+
const value = attrs.get('value') ?? '';
|
|
656
|
+
const where = (attrs.get('in') ?? 'header').toLowerCase();
|
|
657
|
+
if (!key) {
|
|
658
|
+
warnings.push({
|
|
659
|
+
code: 'auth-apikey-missing-key',
|
|
660
|
+
message: `API Key auth on "${requestName}" is missing the key field.`,
|
|
661
|
+
sourcePath,
|
|
662
|
+
requestName,
|
|
663
|
+
});
|
|
664
|
+
return { headers, url, warnings };
|
|
665
|
+
}
|
|
666
|
+
if (where === 'header') {
|
|
667
|
+
headers.push({ name: key, value });
|
|
668
|
+
}
|
|
669
|
+
else if (where === 'query') {
|
|
670
|
+
url = appendQueryParam(url, key, value);
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
warnings.push({
|
|
674
|
+
code: 'auth-apikey-unsupported-location',
|
|
675
|
+
message: `API Key auth location "${where}" on "${requestName}" is not supported. Header/query only in v1.`,
|
|
676
|
+
sourcePath,
|
|
677
|
+
requestName,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
return { headers, url, warnings };
|
|
681
|
+
}
|
|
682
|
+
warnings.push({
|
|
683
|
+
code: 'unsupported-auth',
|
|
684
|
+
message: `Auth type "${type}" on "${requestName}" is not converted in v1.`,
|
|
685
|
+
sourcePath,
|
|
686
|
+
requestName,
|
|
687
|
+
});
|
|
688
|
+
void method;
|
|
689
|
+
return { headers, url, warnings };
|
|
690
|
+
}
|
|
691
|
+
function appendQueryParam(url, key, value) {
|
|
692
|
+
if (!key) {
|
|
693
|
+
return url;
|
|
694
|
+
}
|
|
695
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
696
|
+
return `${url}${separator}${key}=${value}`;
|
|
697
|
+
}
|
|
698
|
+
function readKeyValueMap(value) {
|
|
699
|
+
const result = new Map();
|
|
700
|
+
const arr = asArray(value);
|
|
701
|
+
if (!arr) {
|
|
702
|
+
return result;
|
|
703
|
+
}
|
|
704
|
+
for (const itemUnknown of arr) {
|
|
705
|
+
const item = asRecord(itemUnknown);
|
|
706
|
+
if (!item) {
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
if (item.disabled === true) {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const key = asString(item.key);
|
|
713
|
+
if (!key) {
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
result.set(key.toLowerCase(), stringifyScalar(item.value));
|
|
717
|
+
}
|
|
718
|
+
return result;
|
|
719
|
+
}
|
|
720
|
+
function readHeaders(headerValue) {
|
|
721
|
+
const arr = asArray(headerValue);
|
|
722
|
+
if (!arr) {
|
|
723
|
+
return [];
|
|
724
|
+
}
|
|
725
|
+
const result = [];
|
|
726
|
+
for (const headerUnknown of arr) {
|
|
727
|
+
const header = asRecord(headerUnknown);
|
|
728
|
+
if (!header) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (header.disabled === true) {
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const name = asString(header.key);
|
|
735
|
+
if (!name) {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
result.push({ name, value: stringifyScalar(header.value) });
|
|
739
|
+
}
|
|
740
|
+
return result;
|
|
741
|
+
}
|
|
742
|
+
function mergeHeaders(explicitHeaders, authHeaders) {
|
|
743
|
+
const merged = [...explicitHeaders];
|
|
744
|
+
const lowerMap = new Map();
|
|
745
|
+
explicitHeaders.forEach((header, index) => lowerMap.set(header.name.toLowerCase(), index));
|
|
746
|
+
for (const header of authHeaders) {
|
|
747
|
+
const existingIndex = lowerMap.get(header.name.toLowerCase());
|
|
748
|
+
if (existingIndex !== undefined) {
|
|
749
|
+
merged[existingIndex] = header;
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
lowerMap.set(header.name.toLowerCase(), merged.length);
|
|
753
|
+
merged.push(header);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return merged;
|
|
757
|
+
}
|
|
758
|
+
function renderRequestBody(bodyValue, sourcePath, requestName) {
|
|
759
|
+
const body = asRecord(bodyValue);
|
|
760
|
+
if (!body) {
|
|
761
|
+
return { warnings: [] };
|
|
762
|
+
}
|
|
763
|
+
const mode = asString(body.mode)?.toLowerCase();
|
|
764
|
+
if (!mode) {
|
|
765
|
+
return { warnings: [] };
|
|
766
|
+
}
|
|
767
|
+
if (mode === 'raw') {
|
|
768
|
+
const raw = asString(body.raw) ?? '';
|
|
769
|
+
if (!raw.trim()) {
|
|
770
|
+
return { warnings: [] };
|
|
771
|
+
}
|
|
772
|
+
const options = asRecord(body.options);
|
|
773
|
+
const rawOptions = asRecord(options?.raw);
|
|
774
|
+
const language = asString(rawOptions?.language)?.toLowerCase();
|
|
775
|
+
const looksJson = language === 'json' || isLikelyJson(raw);
|
|
776
|
+
if (!looksJson) {
|
|
777
|
+
return {
|
|
778
|
+
warnings: [{
|
|
779
|
+
code: 'unsupported-body-raw',
|
|
780
|
+
message: `Raw body on "${requestName}" is not JSON. v1 converts JSON and urlencoded only.`,
|
|
781
|
+
sourcePath,
|
|
782
|
+
requestName,
|
|
783
|
+
}]
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
return { bodyText: raw, warnings: [] };
|
|
787
|
+
}
|
|
788
|
+
if (mode === 'urlencoded') {
|
|
789
|
+
const arr = asArray(body.urlencoded);
|
|
790
|
+
if (!arr) {
|
|
791
|
+
return { warnings: [] };
|
|
792
|
+
}
|
|
793
|
+
const lines = [];
|
|
794
|
+
for (const entryUnknown of arr) {
|
|
795
|
+
const entry = asRecord(entryUnknown);
|
|
796
|
+
if (!entry || entry.disabled === true) {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
const key = asString(entry.key);
|
|
800
|
+
if (!key) {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
const value = stringifyScalar(entry.value);
|
|
804
|
+
lines.push(`${key}=${value}`);
|
|
805
|
+
}
|
|
806
|
+
return { bodyText: lines.join('\n'), warnings: [] };
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
warnings: [{
|
|
810
|
+
code: 'unsupported-body-mode',
|
|
811
|
+
message: `Body mode "${mode}" on "${requestName}" is not converted in v1 (JSON + urlencoded only).`,
|
|
812
|
+
sourcePath,
|
|
813
|
+
requestName,
|
|
814
|
+
}]
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
function isLikelyJson(value) {
|
|
818
|
+
const trimmed = value.trim();
|
|
819
|
+
if (!(trimmed.startsWith('{') || trimmed.startsWith('['))) {
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
JSON.parse(trimmed);
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function assignReusableHeaderGroups(requests) {
|
|
831
|
+
const signatureCounts = new Map();
|
|
832
|
+
const signatureHeaders = new Map();
|
|
833
|
+
for (const request of requests) {
|
|
834
|
+
const signature = request.headerGroupSignature;
|
|
835
|
+
if (!signature || request.inlineHeaders.length === 0) {
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
signatureCounts.set(signature, (signatureCounts.get(signature) ?? 0) + 1);
|
|
839
|
+
if (!signatureHeaders.has(signature)) {
|
|
840
|
+
signatureHeaders.set(signature, request.inlineHeaders);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const reusable = Array.from(signatureCounts.entries())
|
|
844
|
+
.filter(([, count]) => count >= 2)
|
|
845
|
+
.map(([signature]) => signature)
|
|
846
|
+
.sort((a, b) => a.localeCompare(b));
|
|
847
|
+
return reusable.map((signature, index) => ({
|
|
848
|
+
signature,
|
|
849
|
+
name: `Headers${String(index + 1).padStart(2, '0')}`,
|
|
850
|
+
headers: signatureHeaders.get(signature) ?? [],
|
|
851
|
+
}));
|
|
852
|
+
}
|
|
853
|
+
function generateNornApiContent(title, requests, headerGroups) {
|
|
854
|
+
const lines = [];
|
|
855
|
+
lines.push(`# Generated from Postman collection: ${title}`);
|
|
856
|
+
lines.push('# v1 import output - review and refine as needed');
|
|
857
|
+
lines.push('');
|
|
858
|
+
lines.push('endpoints');
|
|
859
|
+
for (const request of requests) {
|
|
860
|
+
lines.push(` ${request.endpointName}: ${request.method} ${request.endpointUrl}`);
|
|
861
|
+
}
|
|
862
|
+
lines.push('end endpoints');
|
|
863
|
+
if (headerGroups.length > 0) {
|
|
864
|
+
lines.push('');
|
|
865
|
+
for (const group of headerGroups) {
|
|
866
|
+
lines.push('');
|
|
867
|
+
lines.push(`headers ${group.name}`);
|
|
868
|
+
for (const header of group.headers) {
|
|
869
|
+
lines.push(`${header.name}: ${header.value}`);
|
|
870
|
+
}
|
|
871
|
+
lines.push('end headers');
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
lines.push('');
|
|
875
|
+
return lines.join('\n');
|
|
876
|
+
}
|
|
877
|
+
function generateNornFileContent(fileRelativePath, apiFileRelativePath, requests, headerGroups, warnings) {
|
|
878
|
+
const lines = [];
|
|
879
|
+
const importPath = toRelativeImport(fileRelativePath, apiFileRelativePath);
|
|
880
|
+
lines.push(`import "${importPath}"`);
|
|
881
|
+
if (requests.length === 0) {
|
|
882
|
+
lines.push('');
|
|
883
|
+
lines.push('# Folder imported from Postman has no direct requests in this file (subfolders may exist).');
|
|
884
|
+
lines.push('');
|
|
885
|
+
return lines.join('\n');
|
|
886
|
+
}
|
|
887
|
+
const headerGroupBySignature = new Map(headerGroups.map(group => [group.signature, group.name]));
|
|
888
|
+
for (const request of requests) {
|
|
889
|
+
lines.push('');
|
|
890
|
+
lines.push(`[${request.namedRequestName}]`);
|
|
891
|
+
const headerGroupName = request.headerGroupSignature ? headerGroupBySignature.get(request.headerGroupSignature) : undefined;
|
|
892
|
+
lines.push(`${request.method} ${request.endpointCall}${headerGroupName ? ` ${headerGroupName}` : ''}`);
|
|
893
|
+
if (!headerGroupName) {
|
|
894
|
+
for (const header of request.inlineHeaders) {
|
|
895
|
+
lines.push(`${header.name}: ${header.value}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (request.bodyText && request.bodyText.length > 0) {
|
|
899
|
+
lines.push('');
|
|
900
|
+
lines.push(request.bodyText);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
void warnings;
|
|
904
|
+
lines.push('');
|
|
905
|
+
return lines.join('\n');
|
|
906
|
+
}
|
|
907
|
+
function generateNornEnvContent(sections, commonVariables = [], sourceLabel = 'Postman environment JSON') {
|
|
908
|
+
const lines = [];
|
|
909
|
+
lines.push(`# Generated from ${sourceLabel}`);
|
|
910
|
+
lines.push('# Review imported values and secrets before committing.');
|
|
911
|
+
if (commonVariables.length > 0) {
|
|
912
|
+
lines.push('');
|
|
913
|
+
for (const variable of commonVariables) {
|
|
914
|
+
const keyword = variable.secret ? 'secret' : 'var';
|
|
915
|
+
lines.push(`${keyword} ${sanitizeVariableName(variable.key)} = ${toNornLiteral(variable.value)}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
for (const section of sections) {
|
|
919
|
+
lines.push('');
|
|
920
|
+
lines.push(`[env:${section.name}]`);
|
|
921
|
+
if (section.variables.length === 0) {
|
|
922
|
+
lines.push('# (no variables)');
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
for (const variable of section.variables) {
|
|
926
|
+
const keyword = variable.secret ? 'secret' : 'var';
|
|
927
|
+
lines.push(`${keyword} ${sanitizeVariableName(variable.key)} = ${toNornLiteral(variable.value)}`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
lines.push('');
|
|
931
|
+
return lines.join('\n');
|
|
932
|
+
}
|
|
933
|
+
function generateNornEnvAppendContent(sections) {
|
|
934
|
+
const lines = [];
|
|
935
|
+
lines.push('');
|
|
936
|
+
lines.push('# Appended from Postman environment JSON import');
|
|
937
|
+
for (const section of sections) {
|
|
938
|
+
lines.push('');
|
|
939
|
+
lines.push(`[env:${section.name}]`);
|
|
940
|
+
if (section.variables.length === 0) {
|
|
941
|
+
lines.push('# (no variables)');
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
for (const variable of section.variables) {
|
|
945
|
+
const keyword = variable.secret ? 'secret' : 'var';
|
|
946
|
+
lines.push(`${keyword} ${sanitizeVariableName(variable.key)} = ${toNornLiteral(variable.value)}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
lines.push('');
|
|
950
|
+
return lines.join('\n');
|
|
951
|
+
}
|
|
952
|
+
function readExistingNornenvEnvironmentNames(filePath) {
|
|
953
|
+
const names = new Set();
|
|
954
|
+
try {
|
|
955
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
956
|
+
const regex = /^\[env:([a-zA-Z_][a-zA-Z0-9_-]*)\]$/gm;
|
|
957
|
+
let match;
|
|
958
|
+
while ((match = regex.exec(content)) !== null) {
|
|
959
|
+
names.add(match[1]);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
// Best effort only; import can still proceed in append mode.
|
|
964
|
+
}
|
|
965
|
+
return names;
|
|
966
|
+
}
|
|
967
|
+
function convertPostmanUrlToNornEndpoint(rawUrl) {
|
|
968
|
+
// Convert colon-style path params to Norn endpoint path params.
|
|
969
|
+
const colonConverted = rawUrl.replace(/\/:([A-Za-z_][A-Za-z0-9_-]*)/g, '/{$1}');
|
|
970
|
+
// Preserve a leading base URL variable (e.g. {{url}}, {{baseUrl}}) so it stays sourced from .nornenv,
|
|
971
|
+
// but convert placeholders in the remainder of the path/query into endpoint parameters.
|
|
972
|
+
const leadingBaseVarMatch = colonConverted.match(/^(\{\{[A-Za-z_][A-Za-z0-9_-]*\}\})(.*)$/);
|
|
973
|
+
if (leadingBaseVarMatch) {
|
|
974
|
+
return leadingBaseVarMatch[1] + convertPathAndQueryPlaceholdersToEndpointParams(leadingBaseVarMatch[2]);
|
|
975
|
+
}
|
|
976
|
+
// For full URLs, preserve placeholders inside the authority/host portion and only convert path/query placeholders.
|
|
977
|
+
const split = splitUrlAuthorityAndPath(colonConverted);
|
|
978
|
+
return split.prefix + convertPathAndQueryPlaceholdersToEndpointParams(split.suffix);
|
|
979
|
+
}
|
|
980
|
+
function extractEndpointParamNames(endpointUrl) {
|
|
981
|
+
const names = [];
|
|
982
|
+
const seen = new Set();
|
|
983
|
+
// Match single-brace endpoint params only. This must ignore Norn variable placeholders like {{baseUrl}}.
|
|
984
|
+
const regex = /(?<!\{)\{([A-Za-z_][A-Za-z0-9_-]*)\}(?!\})/g;
|
|
985
|
+
let match;
|
|
986
|
+
while ((match = regex.exec(endpointUrl)) !== null) {
|
|
987
|
+
const name = match[1];
|
|
988
|
+
if (!seen.has(name)) {
|
|
989
|
+
seen.add(name);
|
|
990
|
+
names.push(name);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return names;
|
|
994
|
+
}
|
|
995
|
+
function convertPathAndQueryPlaceholdersToEndpointParams(value) {
|
|
996
|
+
if (!value) {
|
|
997
|
+
return value;
|
|
998
|
+
}
|
|
999
|
+
return value.replace(/\{\{([A-Za-z_][A-Za-z0-9_-]*)\}\}/g, '{$1}');
|
|
1000
|
+
}
|
|
1001
|
+
function splitUrlAuthorityAndPath(url) {
|
|
1002
|
+
const schemeIndex = url.indexOf('://');
|
|
1003
|
+
if (schemeIndex < 0) {
|
|
1004
|
+
return { prefix: '', suffix: url };
|
|
1005
|
+
}
|
|
1006
|
+
const authorityStart = schemeIndex + 3;
|
|
1007
|
+
const slashIndex = url.indexOf('/', authorityStart);
|
|
1008
|
+
const queryIndex = url.indexOf('?', authorityStart);
|
|
1009
|
+
let splitIndex = -1;
|
|
1010
|
+
if (slashIndex >= 0 && queryIndex >= 0) {
|
|
1011
|
+
splitIndex = Math.min(slashIndex, queryIndex);
|
|
1012
|
+
}
|
|
1013
|
+
else if (slashIndex >= 0) {
|
|
1014
|
+
splitIndex = slashIndex;
|
|
1015
|
+
}
|
|
1016
|
+
else if (queryIndex >= 0) {
|
|
1017
|
+
splitIndex = queryIndex;
|
|
1018
|
+
}
|
|
1019
|
+
if (splitIndex < 0) {
|
|
1020
|
+
return { prefix: url, suffix: '' };
|
|
1021
|
+
}
|
|
1022
|
+
return {
|
|
1023
|
+
prefix: url.slice(0, splitIndex),
|
|
1024
|
+
suffix: url.slice(splitIndex),
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
function headerSignature(headers) {
|
|
1028
|
+
return headers
|
|
1029
|
+
.map(header => `${header.name.toLowerCase()}:${header.value}`)
|
|
1030
|
+
.sort((a, b) => a.localeCompare(b))
|
|
1031
|
+
.join('\n');
|
|
1032
|
+
}
|
|
1033
|
+
function getFolderFileRelativePath(folderPath) {
|
|
1034
|
+
if (folderPath.length === 0) {
|
|
1035
|
+
return 'root.norn';
|
|
1036
|
+
}
|
|
1037
|
+
const parent = folderPath.slice(0, -1);
|
|
1038
|
+
const fileName = `${folderPath[folderPath.length - 1]}.norn`;
|
|
1039
|
+
return normalizeRelativePath(path.join(...parent, fileName));
|
|
1040
|
+
}
|
|
1041
|
+
function toRelativeImport(fromFileRelativePath, targetFileRelativePath) {
|
|
1042
|
+
const fromDir = path.posix.dirname(toPosixPath(fromFileRelativePath));
|
|
1043
|
+
const target = toPosixPath(targetFileRelativePath);
|
|
1044
|
+
let relative = path.posix.relative(fromDir, target);
|
|
1045
|
+
if (!relative.startsWith('.')) {
|
|
1046
|
+
relative = `./${relative}`;
|
|
1047
|
+
}
|
|
1048
|
+
return relative;
|
|
1049
|
+
}
|
|
1050
|
+
function normalizeRelativePath(filePath) {
|
|
1051
|
+
return toPosixPath(filePath);
|
|
1052
|
+
}
|
|
1053
|
+
function toPosixPath(filePath) {
|
|
1054
|
+
return filePath.split(path.sep).join(path.posix.sep);
|
|
1055
|
+
}
|
|
1056
|
+
function sanitizeEnvName(name) {
|
|
1057
|
+
const trimmed = name.trim();
|
|
1058
|
+
if (!trimmed) {
|
|
1059
|
+
return '';
|
|
1060
|
+
}
|
|
1061
|
+
let normalized = trimmed.replace(/\s+/g, '-').replace(/[^A-Za-z0-9_-]/g, '-');
|
|
1062
|
+
normalized = normalized.replace(/-+/g, '-');
|
|
1063
|
+
if (!/^[A-Za-z_]/.test(normalized)) {
|
|
1064
|
+
normalized = `env-${normalized}`;
|
|
1065
|
+
}
|
|
1066
|
+
return normalized;
|
|
1067
|
+
}
|
|
1068
|
+
function sanitizeVariableName(name) {
|
|
1069
|
+
let value = name.trim().replace(/[^A-Za-z0-9_]/g, '_');
|
|
1070
|
+
value = value.replace(/_+/g, '_');
|
|
1071
|
+
if (!value) {
|
|
1072
|
+
value = 'value';
|
|
1073
|
+
}
|
|
1074
|
+
if (!/^[A-Za-z_]/.test(value)) {
|
|
1075
|
+
value = `v_${value}`;
|
|
1076
|
+
}
|
|
1077
|
+
return value;
|
|
1078
|
+
}
|
|
1079
|
+
function toNornLiteral(value) {
|
|
1080
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
1081
|
+
return value;
|
|
1082
|
+
}
|
|
1083
|
+
if (/^(true|false|null)$/i.test(value)) {
|
|
1084
|
+
return value.toLowerCase();
|
|
1085
|
+
}
|
|
1086
|
+
if (/^https?:\/\/\S+$/.test(value)) {
|
|
1087
|
+
return value;
|
|
1088
|
+
}
|
|
1089
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
1090
|
+
}
|
|
1091
|
+
function toKebab(value) {
|
|
1092
|
+
return value
|
|
1093
|
+
.trim()
|
|
1094
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
1095
|
+
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
1096
|
+
.replace(/^-+|-+$/g, '')
|
|
1097
|
+
.toLowerCase();
|
|
1098
|
+
}
|
|
1099
|
+
function toPascalIdentifier(value) {
|
|
1100
|
+
const parts = value
|
|
1101
|
+
.trim()
|
|
1102
|
+
.replace(/[^A-Za-z0-9]+/g, ' ')
|
|
1103
|
+
.split(/\s+/)
|
|
1104
|
+
.filter(Boolean);
|
|
1105
|
+
let combined = parts
|
|
1106
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
1107
|
+
.join('');
|
|
1108
|
+
if (!combined) {
|
|
1109
|
+
return '';
|
|
1110
|
+
}
|
|
1111
|
+
if (!/^[A-Za-z_]/.test(combined)) {
|
|
1112
|
+
combined = `N${combined}`;
|
|
1113
|
+
}
|
|
1114
|
+
return combined;
|
|
1115
|
+
}
|
|
1116
|
+
function dedupeIdentifier(base, used) {
|
|
1117
|
+
let candidate = base || 'GeneratedItem';
|
|
1118
|
+
let counter = 2;
|
|
1119
|
+
while (used.has(candidate)) {
|
|
1120
|
+
candidate = `${base}${counter}`;
|
|
1121
|
+
counter += 1;
|
|
1122
|
+
}
|
|
1123
|
+
used.add(candidate);
|
|
1124
|
+
return candidate;
|
|
1125
|
+
}
|
|
1126
|
+
function dedupeSlug(base, siblingCounts) {
|
|
1127
|
+
const count = (siblingCounts.get(base) ?? 0) + 1;
|
|
1128
|
+
siblingCounts.set(base, count);
|
|
1129
|
+
return count === 1 ? base : `${base}-${count}`;
|
|
1130
|
+
}
|
|
1131
|
+
function stringifyScalar(value) {
|
|
1132
|
+
if (value === null || value === undefined) {
|
|
1133
|
+
return '';
|
|
1134
|
+
}
|
|
1135
|
+
if (typeof value === 'string') {
|
|
1136
|
+
return value;
|
|
1137
|
+
}
|
|
1138
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
1139
|
+
return String(value);
|
|
1140
|
+
}
|
|
1141
|
+
return JSON.stringify(value);
|
|
1142
|
+
}
|
|
1143
|
+
function asRecord(value) {
|
|
1144
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1145
|
+
return undefined;
|
|
1146
|
+
}
|
|
1147
|
+
return value;
|
|
1148
|
+
}
|
|
1149
|
+
function asArray(value) {
|
|
1150
|
+
return Array.isArray(value) ? value : undefined;
|
|
1151
|
+
}
|
|
1152
|
+
function asString(value) {
|
|
1153
|
+
return typeof value === 'string' ? value : undefined;
|
|
1154
|
+
}
|
|
1155
|
+
//# sourceMappingURL=postmanImportPlanner.js.map
|