UNPKG

vercel

Version:

The command-line interface for Vercel

1,520 lines (1,512 loc) • 56.3 kB
import { createRequire as __createRequire } from 'node:module'; import { fileURLToPath as __fileURLToPath } from 'node:url'; import { dirname as __dirname_ } from 'node:path'; const require = __createRequire(import.meta.url); const __filename = __fileURLToPath(import.meta.url); const __dirname = __dirname_(__filename); import { formatCondition, formatTransform } from "./chunk-XNUHSM7I.js"; import { output_manager_default } from "./chunk-ZQKJVHXY.js"; import { require_source } from "./chunk-S7KYDPEM.js"; import { __toESM } from "./chunk-TZ2YI2VH.js"; // src/util/routes/env.ts function extractEnvVarNames(value) { const names = /* @__PURE__ */ new Set(); for (const m of value.matchAll(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g)) { names.add(m[1]); } return Array.from(names); } function populateRouteEnv(route) { const routeEnv = /* @__PURE__ */ new Set(); if (route.dest) { for (const name of extractEnvVarNames(route.dest)) { routeEnv.add(name); } } if (route.headers) { for (const value of Object.values(route.headers)) { for (const name of extractEnvVarNames(value)) { routeEnv.add(name); } } } route.env = routeEnv.size > 0 ? Array.from(routeEnv) : void 0; if (route.transforms) { for (const transform of route.transforms) { if (transform.args) { const argsStr = Array.isArray(transform.args) ? transform.args.join(" ") : transform.args; const names = extractEnvVarNames(argsStr); transform.env = names.length > 0 ? names : void 0; } } } } // src/util/routes/generate-route.ts async function generateRoute(client, projectId, input, options = {}) { const { teamId } = options; const query = new URLSearchParams(); if (teamId) query.set("teamId", teamId); const queryString = query.toString(); const url = `/v1/projects/${projectId}/routes/generate${queryString ? `?${queryString}` : ""}`; return await client.fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input) }); } // src/util/routes/parse-transforms.ts function parseTransforms(values, type, op) { return values.map((value) => parseTransform(value, type, op)); } function parseTransform(input, type, op) { if (op === "delete") { const key2 = input.trim(); if (!key2) { throw new Error("Delete operation requires a key"); } return { type, op, target: { key: key2 } }; } const eqIndex = input.indexOf("="); if (eqIndex === -1) { throw new Error(`Invalid format: "${input}". Expected format: key=value`); } const key = input.slice(0, eqIndex).trim(); const args = input.slice(eqIndex + 1); if (!key) { throw new Error("Transform key cannot be empty"); } return { type, op, target: { key }, args }; } function collectTransforms(flags) { const transforms = []; if (flags.setResponseHeader) { transforms.push( ...parseTransforms(flags.setResponseHeader, "response.headers", "set") ); } if (flags.appendResponseHeader) { transforms.push( ...parseTransforms( flags.appendResponseHeader, "response.headers", "append" ) ); } if (flags.deleteResponseHeader) { transforms.push( ...parseTransforms( flags.deleteResponseHeader, "response.headers", "delete" ) ); } if (flags.setRequestHeader) { transforms.push( ...parseTransforms(flags.setRequestHeader, "request.headers", "set") ); } if (flags.appendRequestHeader) { transforms.push( ...parseTransforms(flags.appendRequestHeader, "request.headers", "append") ); } if (flags.deleteRequestHeader) { transforms.push( ...parseTransforms(flags.deleteRequestHeader, "request.headers", "delete") ); } if (flags.setRequestQuery) { transforms.push( ...parseTransforms(flags.setRequestQuery, "request.query", "set") ); } if (flags.appendRequestQuery) { transforms.push( ...parseTransforms(flags.appendRequestQuery, "request.query", "append") ); } if (flags.deleteRequestQuery) { transforms.push( ...parseTransforms(flags.deleteRequestQuery, "request.query", "delete") ); } return transforms; } function collectResponseHeaders(setHeaders) { const headers = {}; for (const input of setHeaders) { const eqIndex = input.indexOf("="); if (eqIndex === -1) { throw new Error( `Invalid header format: "${input}". Expected format: key=value` ); } const key = input.slice(0, eqIndex).trim(); const value = input.slice(eqIndex + 1); if (!key) { throw new Error("Header key cannot be empty"); } headers[key] = value; } return headers; } // src/util/routes/interactive.ts var MAX_NAME_LENGTH = 256; var MAX_DESCRIPTION_LENGTH = 1024; var MAX_CONDITIONS = 16; var VALID_SYNTAXES = [ "regex", "path-to-regexp", "equals" ]; var REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; var VALID_ACTION_TYPES = [ "rewrite", "redirect", "set-status" ]; var ALL_ACTION_CHOICES = [ { name: "Rewrite", value: "rewrite", exclusive: true }, { name: "Redirect", value: "redirect", exclusive: true }, { name: "Set Status Code", value: "set-status", exclusive: true }, { name: "Response Headers", value: "response-headers" }, { name: "Request Headers", value: "request-headers" }, { name: "Request Query", value: "request-query" } ]; function stripQuotes(str) { if (str.startsWith('"') && str.endsWith('"') && str.length >= 2) { return str.slice(1, -1); } if (str.startsWith("'") && str.endsWith("'") && str.length >= 2) { return str.slice(1, -1); } return str; } function extractTransformFlags(flags) { return { setResponseHeader: flags["--set-response-header"], appendResponseHeader: flags["--append-response-header"], deleteResponseHeader: flags["--delete-response-header"], setRequestHeader: flags["--set-request-header"], appendRequestHeader: flags["--append-request-header"], deleteRequestHeader: flags["--delete-request-header"], setRequestQuery: flags["--set-request-query"], appendRequestQuery: flags["--append-request-query"], deleteRequestQuery: flags["--delete-request-query"] }; } function collectHeadersAndTransforms(transformFlags) { const headers = transformFlags.setResponseHeader ? collectResponseHeaders(transformFlags.setResponseHeader) : {}; const transforms = collectTransforms({ ...transformFlags, setResponseHeader: void 0 // Already handled in headers }); return { headers, transforms }; } function hasAnyTransformFlags(flags) { const tf = extractTransformFlags(flags); return !!(tf.setResponseHeader || tf.appendResponseHeader || tf.deleteResponseHeader || tf.setRequestHeader || tf.appendRequestHeader || tf.deleteRequestHeader || tf.setRequestQuery || tf.appendRequestQuery || tf.deleteRequestQuery); } function validateActionFlags(action, dest, status) { if (!action) { if (dest || status !== void 0) { return "--action is required when using --dest or --status. Use --action rewrite, --action redirect, or --action set-status."; } return null; } if (!VALID_ACTION_TYPES.includes(action)) { return `Invalid action type: "${action}". Valid types: ${VALID_ACTION_TYPES.join(", ")}`; } switch (action) { case "rewrite": if (!dest) return "--action rewrite requires --dest."; if (status !== void 0) return "--action rewrite does not accept --status."; break; case "redirect": if (!dest) return "--action redirect requires --dest."; if (status === void 0) return `--action redirect requires --status (${REDIRECT_STATUS_CODES.join(", ")}).`; if (!REDIRECT_STATUS_CODES.includes(status)) return `Invalid redirect status: ${status}. Must be one of: ${REDIRECT_STATUS_CODES.join(", ")}`; break; case "set-status": if (dest) return "--action set-status does not accept --dest."; if (status === void 0) return "--action set-status requires --status."; if (status < 100 || status > 599) return "Status code must be between 100 and 599."; break; } return null; } async function collectActionDetails(client, actionType, flags) { switch (actionType) { case "rewrite": { const dest = await client.input.text({ message: "Destination URL:", validate: (val) => val ? true : "Destination is required" }); Object.assign(flags, { "--dest": dest }); break; } case "redirect": { const dest = await client.input.text({ message: "Destination URL:", validate: (val) => val ? true : "Destination is required" }); const status = await client.input.select({ message: "Status code:", choices: [ { name: "307 - Temporary Redirect", value: 307 }, { name: "308 - Permanent Redirect", value: 308 }, { name: "301 - Moved Permanently", value: 301 }, { name: "302 - Found", value: 302 }, { name: "303 - See Other", value: 303 } ] }); Object.assign(flags, { "--dest": dest, "--status": status }); break; } case "set-status": { const statusCode = await client.input.text({ message: "HTTP status code:", validate: (val) => { const num = parseInt(val, 10); if (isNaN(num) || num < 100 || num > 599) { return "Status code must be between 100 and 599"; } return true; } }); Object.assign(flags, { "--status": parseInt(statusCode, 10) }); break; } case "response-headers": { await collectInteractiveHeaders(client, "response", flags); break; } case "request-headers": { await collectInteractiveHeaders(client, "request-header", flags); break; } case "request-query": { await collectInteractiveHeaders(client, "request-query", flags); break; } } } async function collectInteractiveConditions(client, flags) { let addMore = true; while (addMore) { const currentHas = flags["--has"] || []; const currentMissing = flags["--missing"] || []; if (currentHas.length > 0 || currentMissing.length > 0) { output_manager_default.log("\nCurrent conditions:"); for (const c of currentHas) { output_manager_default.print(` has: ${c} `); } for (const c of currentMissing) { output_manager_default.print(` does not have: ${c} `); } output_manager_default.print("\n"); } const conditionType = await client.input.select({ message: "Condition type:", choices: [ { name: "has - Request must have this", value: "has" }, { name: "does not have - Request must NOT have this", value: "missing" } ] }); const targetType = await client.input.select({ message: "What to check:", choices: [ { name: "Header", value: "header" }, { name: "Cookie", value: "cookie" }, { name: "Query Parameter", value: "query" }, { name: "Host", value: "host" } ] }); let conditionValue; if (targetType === "host") { const operator = await client.input.select({ message: "How to match the host:", choices: [ { name: "Equals", value: "eq" }, { name: "Contains", value: "contains" }, { name: "Matches (regex)", value: "re" } ] }); const hostInput = await client.input.text({ message: operator === "re" ? "Host pattern (regex):" : "Host value:", validate: (val) => { if (!val) return "Host value is required"; if (operator === "re") { try { new RegExp(val); return true; } catch { return "Invalid regex pattern"; } } return true; } }); conditionValue = `host:${operator}=${hostInput}`; } else { const key = await client.input.text({ message: `${targetType.charAt(0).toUpperCase() + targetType.slice(1)} name:`, validate: (val) => val ? true : `${targetType} name is required` }); const operator = await client.input.select({ message: "How to match the value:", choices: [ { name: "Exists (any value)", value: "exists" }, { name: "Equals", value: "eq" }, { name: "Contains", value: "contains" }, { name: "Matches (regex)", value: "re" } ] }); if (operator === "exists") { conditionValue = `${targetType}:${key}:exists`; } else { const valueInput = await client.input.text({ message: operator === "re" ? "Value pattern (regex):" : "Value:", validate: (val) => { if (!val) return "Value is required"; if (operator === "re") { try { new RegExp(val); return true; } catch { return "Invalid regex pattern"; } } return true; } }); conditionValue = `${targetType}:${key}:${operator}=${valueInput}`; } } const flagName = conditionType === "has" ? "--has" : "--missing"; const existing = flags[flagName] || []; flags[flagName] = [...existing, conditionValue]; const totalConditions = (flags["--has"] || []).length + (flags["--missing"] || []).length; if (totalConditions >= MAX_CONDITIONS) { output_manager_default.warn(`Maximum ${MAX_CONDITIONS} conditions reached.`); break; } addMore = await client.input.confirm("Add another condition?", false); } } function formatCollectedItems(flags, type) { const items = []; const prefix = type === "response" ? "response-header" : type === "request-header" ? "request-header" : "request-query"; const setItems = flags[`--set-${prefix}`] || []; const appendItems = flags[`--append-${prefix}`] || []; const deleteItems = flags[`--delete-${prefix}`] || []; for (const item of setItems) { items.push(` set: ${item}`); } for (const item of appendItems) { items.push(` append: ${item}`); } for (const item of deleteItems) { items.push(` delete: ${item}`); } return items; } async function collectInteractiveHeaders(client, type, flags) { const flagName = type === "response" ? "--set-response-header" : type === "request-header" ? "--set-request-header" : "--set-request-query"; const sectionName = type === "response" ? "Response Headers" : type === "request-header" ? "Request Headers" : "Request Query Parameters"; const itemName = type === "response" ? "response header" : type === "request-header" ? "request header" : "query parameter"; output_manager_default.log(` --- ${sectionName} ---`); let addMore = true; while (addMore) { const collected = formatCollectedItems(flags, type); if (collected.length > 0) { output_manager_default.log(` Current ${sectionName.toLowerCase()}:`); for (const item of collected) { output_manager_default.print(`${item} `); } output_manager_default.print("\n"); } const op = await client.input.select({ message: `${sectionName} operation:`, choices: [ { name: "Set", value: "set" }, { name: "Append", value: "append" }, { name: "Delete", value: "delete" } ] }); const key = await client.input.text({ message: `${itemName.charAt(0).toUpperCase() + itemName.slice(1)} name:`, validate: (val) => val ? true : `${itemName} name is required` }); if (op === "delete") { const opFlagName = flagName.replace("--set-", "--delete-"); const existing = flags[opFlagName] || []; flags[opFlagName] = [...existing, key]; } else { const value = await client.input.text({ message: `${itemName.charAt(0).toUpperCase() + itemName.slice(1)} value:` }); const opFlagName = op === "append" ? flagName.replace("--set-", "--append-") : flagName; const existing = flags[opFlagName] || []; flags[opFlagName] = [...existing, `${key}=${value}`]; } addMore = await client.input.confirm(`Add another ${itemName}?`, false); } } // src/util/routes/ai-transform.ts var import_chalk = __toESM(require_source(), 1); // src/util/routes/parse-conditions.ts var CONDITION_OPERATORS = [ "eq", "contains", "re", "exists" ]; function escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function buildConditionValue(operator, value) { if (operator === "exists") return void 0; if (operator === "re") return value; const escapedValue = escapeRegExp(value); if (operator === "contains") return `.*${escapedValue}.*`; return `^${escapedValue}$`; } function validateRegexPattern(pattern, context) { try { new RegExp(pattern); } catch (e) { throw new Error( `Invalid regex in ${context}: "${pattern}". ${e instanceof Error ? e.message : ""}` ); } } function parseOperatorValue(valuePart) { if (valuePart === "exists") { return { operator: "exists", rawValue: "" }; } const eqIdx = valuePart.indexOf("="); if (eqIdx === -1) return null; const maybeOp = valuePart.slice(0, eqIdx); if (CONDITION_OPERATORS.includes(maybeOp)) { return { operator: maybeOp, rawValue: valuePart.slice(eqIdx + 1) }; } return null; } function parseConditions(conditions) { return conditions.map(parseCondition); } function parseCondition(condition) { const parts = condition.split(":"); if (parts.length < 2) { throw new Error( `Invalid condition format: "${condition}". Expected format: type:key or type:key:value` ); } const type = parts[0].toLowerCase(); const validTypes = ["header", "cookie", "query", "host"]; if (!validTypes.includes(type)) { throw new Error( `Invalid condition type: "${type}". Valid types: ${validTypes.join(", ")}` ); } if (type === "host") { const rawValue = parts.slice(1).join(":"); if (!rawValue) { throw new Error("Host condition requires a value"); } const opResult2 = parseOperatorValue(rawValue); if (opResult2) { if (opResult2.operator === "exists") { throw new Error( 'Host condition does not support "exists" operator (host always has a value)' ); } if (opResult2.operator !== "re" && !opResult2.rawValue) { throw new Error( `Host condition with "${opResult2.operator}" operator requires a value` ); } const compiledValue = buildConditionValue( opResult2.operator, opResult2.rawValue ); if (compiledValue !== void 0) { validateRegexPattern(compiledValue, "host condition"); } return { type: "host", value: compiledValue }; } validateRegexPattern(rawValue, "host condition"); return { type: "host", value: rawValue }; } const key = parts[1]; if (!key) { throw new Error(`${type} condition requires a key`); } const valuePart = parts.length > 2 ? parts.slice(2).join(":") : void 0; if (valuePart === void 0) { return { type, key }; } const opResult = parseOperatorValue(valuePart); if (opResult) { if (opResult.operator === "exists") { return { type, key }; } if (opResult.operator !== "re" && !opResult.rawValue) { throw new Error( `Condition "${opResult.operator}" operator requires a value after "="` ); } const compiledValue = buildConditionValue( opResult.operator, opResult.rawValue ); if (compiledValue !== void 0) { validateRegexPattern(compiledValue, `${type} condition value`); } return { type, key, ...compiledValue !== void 0 && { value: compiledValue } }; } validateRegexPattern(valuePart, `${type} condition value`); return { type, key, value: valuePart }; } function formatCondition2(field) { if (field.type === "host") { return `host:${field.value}`; } if (field.value) { return `${field.type}:${field.key}:${field.value}`; } return `${field.type}:${field.key}`; } // src/util/routes/ai-transform.ts function generatedRouteToAddInput(generated) { const hasConditions = []; const missingConditions = []; const headers = {}; const transforms = []; let dest; let status; if (generated.conditions) { for (const c of generated.conditions) { const compiledValue = c.value !== void 0 ? buildConditionValue(c.operator, c.value) : void 0; const field = c.field === "host" ? { type: "host", value: compiledValue ?? c.value ?? "" } : { type: c.field, key: c.key ?? "", ...compiledValue !== void 0 && { value: compiledValue } }; if (c.missing) { missingConditions.push(field); } else { hasConditions.push(field); } } } for (const action of generated.actions) { switch (action.type) { case "rewrite": dest = action.dest; break; case "redirect": dest = action.dest; status = action.status; break; case "set-status": status = action.status; break; case "modify": { if (!action.headers) break; if (action.subType === "response-headers") { for (const h of action.headers) { if (h.op === "set") { headers[h.key] = h.value ?? ""; } else { transforms.push({ type: "response.headers", op: h.op, target: { key: h.key }, ...h.op !== "delete" && h.value && { args: h.value } }); } } } else if (action.subType === "transform-request-header") { for (const h of action.headers) { transforms.push({ type: "request.headers", op: h.op, target: { key: h.key }, ...h.op !== "delete" && h.value && { args: h.value } }); } } else if (action.subType === "transform-request-query") { for (const h of action.headers) { transforms.push({ type: "request.query", op: h.op, target: { key: h.key }, ...h.op !== "delete" && h.value && { args: h.value } }); } } break; } } } return { name: generated.name, description: generated.description || void 0, srcSyntax: generated.pathCondition.syntax, route: { src: generated.pathCondition.value, ...dest !== void 0 && { dest }, ...status !== void 0 && { status }, ...Object.keys(headers).length > 0 && { headers }, ...transforms.length > 0 && { transforms }, ...hasConditions.length > 0 && { has: hasConditions }, ...missingConditions.length > 0 && { missing: missingConditions } } }; } function convertRouteToCurrentRoute(generated) { return { name: generated.name, description: generated.description || void 0, pathCondition: generated.pathCondition, conditions: generated.conditions, actions: generated.actions }; } function routingRuleToCurrentRoute(rule) { const conditions = []; const actions = []; if (rule.route.has) { for (const c of rule.route.has) { conditions.push({ field: c.type, operator: c.value !== void 0 ? "re" : "exists", key: c.key, value: c.value !== void 0 ? typeof c.value === "string" ? c.value : JSON.stringify(c.value) : void 0, missing: false }); } } if (rule.route.missing) { for (const c of rule.route.missing) { conditions.push({ field: c.type, operator: c.value !== void 0 ? "re" : "exists", key: c.key, value: c.value !== void 0 ? typeof c.value === "string" ? c.value : JSON.stringify(c.value) : void 0, missing: true }); } } const isRedirect = rule.route.dest && rule.route.status && REDIRECT_STATUS_CODES.includes(rule.route.status); if (isRedirect) { actions.push({ type: "redirect", dest: rule.route.dest, status: rule.route.status }); } else if (rule.route.dest) { actions.push({ type: "rewrite", dest: rule.route.dest }); } else if (rule.route.status) { actions.push({ type: "set-status", status: rule.route.status }); } const responseHeaders = rule.route.headers ? Object.entries(rule.route.headers).map(([key, value]) => ({ key, value, op: "set" })) : []; const allTransforms = rule.route.transforms ?? []; const responseHeaderTransforms = allTransforms.filter((t) => t.type === "response.headers").map((t) => ({ key: typeof t.target.key === "string" ? t.target.key : String(t.target.key), value: t.args, op: t.op })); const allResponseHeaders = [...responseHeaders, ...responseHeaderTransforms]; if (allResponseHeaders.length > 0) { actions.push({ type: "modify", subType: "response-headers", headers: allResponseHeaders }); } const requestHeaders = allTransforms.filter((t) => t.type === "request.headers").map((t) => ({ key: typeof t.target.key === "string" ? t.target.key : String(t.target.key), value: t.args, op: t.op })); if (requestHeaders.length > 0) { actions.push({ type: "modify", subType: "transform-request-header", headers: requestHeaders }); } const requestQuery = allTransforms.filter((t) => t.type === "request.query").map((t) => ({ key: typeof t.target.key === "string" ? t.target.key : String(t.target.key), value: t.args, op: t.op })); if (requestQuery.length > 0) { actions.push({ type: "modify", subType: "transform-request-query", headers: requestQuery }); } return { name: rule.name, description: rule.description, pathCondition: { value: rule.route.src, syntax: rule.srcSyntax ?? "regex" }, ...conditions.length > 0 && { conditions }, actions }; } function printGeneratedRoutePreview(generated) { output_manager_default.print("\n"); output_manager_default.print(` ${import_chalk.default.bold("Generated Route:")} `); output_manager_default.print(` ${import_chalk.default.cyan("Name:")} ${generated.name} `); if (generated.description) { output_manager_default.print(` ${import_chalk.default.cyan("Description:")} ${generated.description} `); } output_manager_default.print( ` ${import_chalk.default.cyan("Source:")} ${generated.pathCondition.value} ` ); if (generated.conditions && generated.conditions.length > 0) { output_manager_default.print(` ${import_chalk.default.cyan("Conditions:")} `); for (const c of generated.conditions) { const prefix = c.missing ? "does not have" : "has"; const operatorLabel = c.operator === "eq" ? "equal to" : c.operator === "contains" ? "containing" : c.operator === "re" ? "matching" : ""; const key = c.key ? ` ${import_chalk.default.cyan(`"${c.key}"`)}` : ""; const value = c.operator === "exists" || !c.value ? "" : ` ${operatorLabel} ${import_chalk.default.cyan(`"${c.value}"`)}`; output_manager_default.print(` ${import_chalk.default.gray(prefix)} ${c.field}${key}${value} `); } } for (const action of generated.actions) { if (action.type === "rewrite" && action.dest) { output_manager_default.print( ` ${import_chalk.default.cyan("Action:")} Rewrite \u2192 ${action.dest} ` ); } else if (action.type === "redirect" && action.dest) { output_manager_default.print( ` ${import_chalk.default.cyan("Action:")} Redirect \u2192 ${action.dest} (${action.status}) ` ); } else if (action.type === "set-status" && action.status) { output_manager_default.print( ` ${import_chalk.default.cyan("Action:")} Set Status ${action.status} ` ); } } for (const action of generated.actions) { if (action.type === "modify" && action.headers) { const label = action.subType === "response-headers" ? "Response Headers" : action.subType === "transform-request-header" ? "Request Headers" : "Request Query"; output_manager_default.print(` ${import_chalk.default.cyan(`${label}:`)} `); for (const h of action.headers) { if (h.op === "delete") { output_manager_default.print(` ${import_chalk.default.yellow(h.op)} ${import_chalk.default.cyan(h.key)} `); } else { output_manager_default.print( ` ${import_chalk.default.yellow(h.op)} ${import_chalk.default.cyan(h.key)} = ${h.value} ` ); } } } } output_manager_default.print("\n"); } // src/commands/routes/edit-interactive.ts var import_chalk2 = __toESM(require_source(), 1); function getPrimaryActionType(route) { const { dest, status } = route.route; if (dest && status && REDIRECT_STATUS_CODES.includes(status)) { return "redirect"; } if (dest) return "rewrite"; if (status) return "set-status"; return null; } function getPrimaryActionLabel(route) { const actionType = getPrimaryActionType(route); switch (actionType) { case "rewrite": return `Rewrite \u2192 ${route.route.dest}`; case "redirect": return `Redirect \u2192 ${route.route.dest} (${route.route.status})`; case "set-status": return `Set Status ${route.route.status}`; default: return "(none)"; } } function getResponseHeaders(route) { const headers = route.route.headers ?? {}; return Object.entries(headers).map(([key, value]) => ({ key, value })); } function getTransformsByType(route, type) { const transforms = route.route.transforms ?? []; return transforms.filter((t) => t.type === type); } function printRouteConfig(route) { output_manager_default.print("\n"); output_manager_default.print(` ${import_chalk2.default.cyan("Name:")} ${route.name} `); if (route.description) { output_manager_default.print(` ${import_chalk2.default.cyan("Description:")} ${route.description} `); } output_manager_default.print( ` ${import_chalk2.default.cyan("Source:")} ${route.route.src} ${import_chalk2.default.gray(`(${route.srcSyntax ?? "regex"})`)} ` ); output_manager_default.print( ` ${import_chalk2.default.cyan("Status:")} ${route.enabled === false ? import_chalk2.default.red("Disabled") : import_chalk2.default.green("Enabled")} ` ); const actionLabel = getPrimaryActionLabel(route); output_manager_default.print(` ${import_chalk2.default.cyan("Action:")} ${actionLabel} `); const hasConds = route.route.has ?? []; if (hasConds.length > 0) { output_manager_default.print(` ${import_chalk2.default.cyan("Has conditions:")} `); for (const c of hasConds) { output_manager_default.print(` ${formatCondition(c)} `); } } const missingConds = route.route.missing ?? []; if (missingConds.length > 0) { output_manager_default.print(` ${import_chalk2.default.cyan("Does not have conditions:")} `); for (const c of missingConds) { output_manager_default.print(` ${formatCondition(c)} `); } } const responseHeaders = getResponseHeaders(route); if (responseHeaders.length > 0) { output_manager_default.print(` ${import_chalk2.default.cyan("Response Headers:")} `); for (const h of responseHeaders) { output_manager_default.print(` ${import_chalk2.default.cyan(h.key)} = ${h.value} `); } } const requestHeaders = getTransformsByType(route, "request.headers"); if (requestHeaders.length > 0) { output_manager_default.print(` ${import_chalk2.default.cyan("Request Headers:")} `); for (const t of requestHeaders) { output_manager_default.print(` ${formatTransform(t)} `); } } const requestQuery = getTransformsByType(route, "request.query"); if (requestQuery.length > 0) { output_manager_default.print(` ${import_chalk2.default.cyan("Request Query:")} `); for (const t of requestQuery) { output_manager_default.print(` ${formatTransform(t)} `); } } output_manager_default.print("\n"); } function cloneRoute(route) { return JSON.parse(JSON.stringify(route)); } function applyFlagMutations(route, flags) { if (flags["--name"] !== void 0) { const name = flags["--name"]; if (name.length > MAX_NAME_LENGTH) { return `Name must be ${MAX_NAME_LENGTH} characters or less.`; } route.name = name; } if (flags["--description"] !== void 0) { const desc = flags["--description"]; if (desc.length > MAX_DESCRIPTION_LENGTH) { return `Description must be ${MAX_DESCRIPTION_LENGTH} characters or less.`; } route.description = desc || void 0; } if (flags["--src"] !== void 0) { route.route.src = stripQuotes(flags["--src"]); } if (flags["--src-syntax"] !== void 0) { const syntax = flags["--src-syntax"]; if (!VALID_SYNTAXES.includes(syntax)) { return `Invalid syntax: "${syntax}". Valid options: ${VALID_SYNTAXES.join(", ")}`; } route.srcSyntax = syntax; } const actionFlag = flags["--action"]; const destFlag = flags["--dest"]; const statusFlag = flags["--status"]; const noDest = flags["--no-dest"]; const noStatus = flags["--no-status"]; if (actionFlag) { if (!VALID_ACTION_TYPES.includes(actionFlag)) { return `Invalid action type: "${actionFlag}". Valid types: ${VALID_ACTION_TYPES.join(", ")}`; } switch (actionFlag) { case "rewrite": { const dest = destFlag ? stripQuotes(destFlag) : void 0; if (!dest) return "--action rewrite requires --dest."; route.route.dest = dest; delete route.route.status; break; } case "redirect": { const dest = destFlag ? stripQuotes(destFlag) : void 0; if (!dest) return "--action redirect requires --dest."; if (statusFlag === void 0) return `--action redirect requires --status (${REDIRECT_STATUS_CODES.join(", ")}).`; if (!REDIRECT_STATUS_CODES.includes(statusFlag)) return `Invalid redirect status: ${statusFlag}. Must be one of: ${REDIRECT_STATUS_CODES.join(", ")}`; route.route.dest = dest; route.route.status = statusFlag; break; } case "set-status": { if (statusFlag === void 0) return "--action set-status requires --status."; if (statusFlag < 100 || statusFlag > 599) return "Status code must be between 100 and 599."; delete route.route.dest; route.route.status = statusFlag; break; } } } else { if (destFlag !== void 0) { route.route.dest = stripQuotes(destFlag); } if (statusFlag !== void 0) { route.route.status = statusFlag; } if (noDest) { delete route.route.dest; } if (noStatus) { delete route.route.status; } } if (flags["--clear-conditions"]) { route.route.has = []; route.route.missing = []; } if (flags["--clear-headers"]) { route.route.headers = {}; } if (flags["--clear-transforms"]) { route.route.transforms = []; } const transformFlags = extractTransformFlags(flags); try { const { headers, transforms } = collectHeadersAndTransforms(transformFlags); if (Object.keys(headers).length > 0) { route.route.headers = { ...route.route.headers ?? {}, ...headers }; } if (transforms.length > 0) { const existing = route.route.transforms ?? []; route.route.transforms = [...existing, ...transforms]; } } catch (e) { return `Invalid transform format. ${e instanceof Error ? e.message : ""}`; } const hasFlags = flags["--has"]; const missingFlags = flags["--missing"]; try { if (hasFlags) { const newHas = parseConditions(hasFlags); const existingHas = route.route.has ?? []; route.route.has = [...existingHas, ...newHas]; } if (missingFlags) { const newMissing = parseConditions(missingFlags); const existingMissing = route.route.missing ?? []; route.route.missing = [...existingMissing, ...newMissing]; } } catch (e) { return e instanceof Error ? e.message : "Invalid condition format"; } const totalConditions = (route.route.has ?? []).length + (route.route.missing ?? []).length; if (totalConditions > MAX_CONDITIONS) { return `Too many conditions: ${totalConditions}. Maximum is ${MAX_CONDITIONS}.`; } const hasDest = !!route.route.dest; const hasStatus = !!route.route.status; const hasHeaders = Object.keys(route.route.headers ?? {}).length > 0; const hasTransforms = (route.route.transforms ?? []).length > 0; if (!hasDest && !hasStatus && !hasHeaders && !hasTransforms) { return "This edit would leave the route with no action. Add --action, headers, or transforms."; } return null; } async function runInteractiveEditLoop(client, route) { for (; ; ) { const hasConds = (route.route.has ?? []).length; const missingConds = (route.route.missing ?? []).length; const responseHeaders = getAllResponseHeaders(route).length; const requestHeaders = getTransformsByType(route, "request.headers").length; const requestQuery = getTransformsByType(route, "request.query").length; const syntaxLabel = route.srcSyntax === "path-to-regexp" ? "Pattern" : route.srcSyntax === "equals" ? "Exact" : "Regex"; const descriptionPreview = route.description ? route.description.length > 40 ? route.description.slice(0, 40) + "..." : route.description : ""; const editChoices = [ { name: `Name (${route.name})`, value: "name" }, { name: descriptionPreview ? `Description (${descriptionPreview})` : "Description", value: "description" }, { name: `Source (${syntaxLabel}: ${route.route.src})`, value: "source" }, { name: `Primary action (${getPrimaryActionLabel(route)})`, value: "action" }, { name: `Conditions (${hasConds} has, ${missingConds} missing)`, value: "conditions" }, { name: `Response Headers (${responseHeaders})`, value: "response-headers" }, { name: `Request Headers (${requestHeaders})`, value: "request-headers" }, { name: `Request Query (${requestQuery})`, value: "request-query" }, { name: "Done - save changes", value: "done" } ]; const choice = await client.input.select({ message: "What would you like to edit?", choices: editChoices, pageSize: editChoices.length, loop: false }); switch (choice) { case "name": await editName(client, route); break; case "description": await editDescription(client, route); break; case "source": await editSource(client, route); break; case "action": await editPrimaryAction(client, route); break; case "conditions": await editConditions(client, route); break; case "response-headers": await editResponseHeaders(client, route); break; case "request-headers": await editTransformsByType( client, route, "request.headers", "request-header" ); break; case "request-query": await editTransformsByType( client, route, "request.query", "request-query" ); break; case "done": break; } if (choice === "done") { const hasDest = !!route.route.dest; const hasStatus = !!route.route.status; const hasHeaders = Object.keys(route.route.headers ?? {}).length > 0; const hasTransforms = (route.route.transforms ?? []).length > 0; if (!hasDest && !hasStatus && !hasHeaders && !hasTransforms) { output_manager_default.warn( "Route has no action (no destination, status, or headers). Add an action before saving." ); continue; } break; } } } async function editName(client, route) { const name = await client.input.text({ message: `Name (current: ${route.name}):`, validate: (val) => { if (!val) return "Route name is required"; if (val.length > MAX_NAME_LENGTH) return `Name must be ${MAX_NAME_LENGTH} characters or less`; return true; } }); route.name = name; } async function editDescription(client, route) { const desc = await client.input.text({ message: `Description${route.description ? ` (current: ${route.description})` : ""}:`, validate: (val) => val && val.length > MAX_DESCRIPTION_LENGTH ? `Description must be ${MAX_DESCRIPTION_LENGTH} characters or less` : true }); route.description = desc || void 0; } async function editSource(client, route) { const syntaxChoice = await client.input.select({ message: `Path syntax (current: ${route.srcSyntax ?? "regex"}):`, choices: [ { name: "Path pattern (e.g., /api/:version/users/:id)", value: "path-to-regexp" }, { name: "Exact match (e.g., /about)", value: "equals" }, { name: "Regular expression (e.g., ^/api/(.*)$)", value: "regex" } ] }); route.srcSyntax = syntaxChoice; const src = await client.input.text({ message: `Path pattern (current: ${route.route.src}):`, validate: (val) => { if (!val) return "Path pattern is required"; return true; } }); route.route.src = src; } async function editPrimaryAction(client, route) { const currentType = getPrimaryActionType(route); const choices = []; if (currentType === "rewrite" || currentType === "redirect") { choices.push({ name: "Change destination", value: "change-dest" }); } if (currentType === "redirect" || currentType === "set-status") { choices.push({ name: "Change status code", value: "change-status" }); } if (currentType !== "rewrite") { choices.push({ name: "Switch to Rewrite", value: "switch-rewrite" }); } if (currentType !== "redirect") { choices.push({ name: "Switch to Redirect", value: "switch-redirect" }); } if (currentType !== "set-status") { choices.push({ name: "Switch to Set Status Code", value: "switch-set-status" }); } if (currentType) { choices.push({ name: "Remove primary action", value: "remove" }); } else { choices.push({ name: "Add Rewrite", value: "switch-rewrite" }); choices.push({ name: "Add Redirect", value: "switch-redirect" }); choices.push({ name: "Add Set Status Code", value: "switch-set-status" }); } choices.push({ name: "Back", value: "back" }); const action = await client.input.select({ message: `Primary action (current: ${getPrimaryActionLabel(route)}):`, choices }); const flags = {}; switch (action) { case "change-dest": { const dest = await client.input.text({ message: `Destination (current: ${route.route.dest}):`, validate: (val) => val ? true : "Destination is required" }); route.route.dest = dest; break; } case "change-status": { if (currentType === "redirect") { const status = await client.input.select({ message: `Status code (current: ${route.route.status}):`, choices: [ { name: "307 - Temporary Redirect", value: 307 }, { name: "308 - Permanent Redirect", value: 308 }, { name: "301 - Moved Permanently", value: 301 }, { name: "302 - Found", value: 302 }, { name: "303 - See Other", value: 303 } ] }); route.route.status = status; } else { const statusCode = await client.input.text({ message: `Status code (current: ${route.route.status}):`, validate: (val) => { const num = parseInt(val, 10); if (isNaN(num) || num < 100 || num > 599) { return "Status code must be between 100 and 599"; } return true; } }); route.route.status = parseInt(statusCode, 10); } break; } case "switch-rewrite": { await collectActionDetails(client, "rewrite", flags); route.route.dest = flags["--dest"]; delete route.route.status; break; } case "switch-redirect": { await collectActionDetails(client, "redirect", flags); route.route.dest = flags["--dest"]; route.route.status = flags["--status"]; break; } case "switch-set-status": { await collectActionDetails(client, "set-status", flags); delete route.route.dest; route.route.status = flags["--status"]; break; } case "remove": { delete route.route.dest; delete route.route.status; break; } } } async function editConditions(client, route) { for (; ; ) { const hasConds = route.route.has ?? []; const missingConds = route.route.missing ?? []; if (hasConds.length > 0 || missingConds.length > 0) { output_manager_default.print("\n"); if (hasConds.length > 0) { output_manager_default.print(` ${import_chalk2.default.cyan("Has conditions:")} `); hasConds.forEach((c, i) => { output_manager_default.print( ` ${import_chalk2.default.gray(`${i + 1}.`)} ${formatCondition(c)} ` ); }); } if (missingConds.length > 0) { output_manager_default.print(` ${import_chalk2.default.cyan("Does not have conditions:")} `); missingConds.forEach((c, i) => { output_manager_default.print( ` ${import_chalk2.default.gray(`${hasConds.length + i + 1}.`)} ${formatCondition(c)} ` ); }); } output_manager_default.print("\n"); } else { output_manager_default.print("\n No conditions set.\n\n"); } const choices = []; if (hasConds.length > 0 || missingConds.length > 0) { choices.push({ name: "Remove a condition", value: "remove" }); } choices.push({ name: "Add a new condition", value: "add" }); choices.push({ name: "Back", value: "back" }); const action = await client.input.select({ message: "Conditions:", choices }); if (action === "back") break; if (action === "remove") { const allConds = [ ...hasConds.map((c, i) => ({ label: `[has] ${formatCondition(c)}`, idx: i, kind: "has" })), ...missingConds.map((c, i) => ({ label: `[does not have] ${formatCondition(c)}`, idx: i, kind: "missing" })) ]; const toRemove = await client.input.select({ message: "Select condition to remove:", choices: [ ...allConds.map((c, i) => ({ name: c.label, value: i })), { name: "Cancel", value: -1 } ] }); if (toRemove !== -1) { const selected = allConds[toRemove]; if (selected.kind === "has") { hasConds.splice(selected.idx, 1); route.route.has = hasConds; } else { missingConds.splice(selected.idx, 1); route.route.missing = missingConds; } } } if (action === "add") { const existingHasStrings = hasConds.map( (c) => formatCondition2(c) ); const existingMissingStrings = missingConds.map( (c) => formatCondition2(c) ); const tempFlags = { "--has": existingHasStrings.length > 0 ? existingHasStrings : void 0, "--missing": existingMissingStrings.length > 0 ? existingMissingStrings : void 0 }; const hasBefore = existingHasStrings.length; const missingBefore = existingMissingStrings.length; await collectInteractiveConditions(client, tempFlags); const allHas = tempFlags["--has"] || []; const allMissing = tempFlags["--missing"] || []; const newHas = allHas.slice(hasBefore); const newMissing = allMissing.slice(missingBefore); if (newHas.length > 0) { const parsed = parseConditions(newHas); const existing = route.route.has ?? []; route.route.has = [...existing, ...parsed]; } if (newMissing.length > 0) { const parsed = parseConditions(newMissing); const existing = route.route.missing ?? []; route.route.missing = [...existing, ...parsed]; } break; } } } function getAllResponseHeaders(route) { const items = []; for (const [key, value] of Object.entries(route.route.headers ?? {})) { items.push({ op: "set", key, value, source: "headers" }); } const transforms = route.route.transforms ?? []; for (const t of transforms) { if (t.type === "response.headers") { items.push({ op: t.op, key: typeof t.target.key === "string" ? t.target.key : JSON.stringify(t.target.key), value: typeof t.args === "string" ? t.args : void 0, source: "transform" }); } } return items; } function formatResponseHeaderItem(item) { if (item.op === "delete") { return `${import_chalk2.default.yellow(item.op)} ${import_chalk2.default.cyan(item.key)}`; } return `${import_chalk2.default.yellow(item.op)} ${import_chalk2.default.cyan(item.key)} = ${item.value}`; } async function editResponseHeaders(client, route) { for (; ; ) { const allHeaders = getAllResponseHeaders(route); if (allHeaders.length > 0) { output_manager_default.print("\n"); output_manager_default.print(` ${import_c