@controlplane/cli
Version:
Control Plane Corporation CLI
381 lines • 14.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.computeHash = exports.sortIHasKindByPriority = exports.sortByKindPriority = exports.toVolumes = exports.toEnv = exports.cartesian = exports.removeUndefinedValues = exports.pick = exports.toArray = exports.merge = exports.kindPriorityOrder = exports.gvcRelatedKinds = void 0;
const _ = require("lodash");
const crypto = require("crypto");
const VALID_KEY_REGEX = /^(?:[A-Za-z_][A-Za-z0-9_]*)$/;
exports.gvcRelatedKinds = ['identity', 'volumeset', 'workload'];
exports.kindPriorityOrder = ['agent', 'secret', 'cloudaccount', 'gvc', 'identity', 'volumeset', 'policy', 'workload'];
function merge(...objs) {
let result = {};
for (let obj of objs) {
Object.assign(result, _.pickBy(obj, (x) => x !== undefined && x !== null && x !== ''));
}
return result;
}
exports.merge = merge;
function toArray(obj) {
if (obj === undefined) {
return [];
}
if (Array.isArray(obj)) {
return obj;
}
return [obj];
}
exports.toArray = toArray;
function pick(obj, ...props) {
let result = {};
for (let prop of props) {
const val = obj[prop];
if (val != null) {
result[prop] = val;
}
}
return result;
}
exports.pick = pick;
function removeUndefinedValues(obj) {
// Iterate over all the keys of the object
Object.keys(obj).forEach((key) => {
// If the value is undefined, delete the key
if (obj[key] === undefined) {
delete obj[key];
}
// If the value is an object, recurse into it
else if (typeof obj[key] === 'object' && obj[key] !== null) {
removeUndefinedValues(obj[key]);
// If the nested object becomes empty after the removal of undefined values, delete the key
if (Object.keys(obj[key]).length === 0) {
delete obj[key];
}
}
});
}
exports.removeUndefinedValues = removeUndefinedValues;
function cartesian(t1, t2, join) {
const res = [];
if (t1 === undefined || t2 === undefined) {
return undefined;
}
for (let s1 of t1) {
for (let s2 of t2) {
res.push(join(s1, s2));
}
}
return res;
}
exports.cartesian = cartesian;
/**
* Converts raw .env lines into {name, value} entries.
*
* @param {string[]} kvs - Raw lines read from a .env file (already split by newline).
* @returns {EnvEntry[]} - Parsed environment entries with stable ordering by final occurrence.
* @throws {Error} - If kvs is not an array of strings.
*/
function toEnv(kvs) {
// Create a mutable mapping so the last occurrence of a key overwrites earlier ones
const map = {};
// Use a numeric for-loop to keep variable types explicit
for (let i = 0; i < kvs.length; i++) {
// Ensure the raw item is a string and trim outer whitespace
const line = typeof kvs[i] === 'string' ? kvs[i].trim() : '';
// Continue to the next line when current line is empty
if (line.length === 0) {
continue;
}
// Skip full-line comments that start with '#'
if (line.startsWith('#')) {
continue;
}
// Remove leading "export " if present (common in shell-style env files)
const withoutExport = line.startsWith('export ') ? line.slice(7).trimStart() : line;
// Remove inline comments that appear outside of quotes only (retain those inside quotes)
const noInlineComment = stripInlineCommentSafe(withoutExport);
// Skip the line if it becomes empty after stripping the inline comment
if (noInlineComment.length === 0) {
continue;
}
// Split the logical line into a key/value pair at the first unquoted '=' or ':'
const pair = splitKeyValue(noInlineComment);
// Skip non key/value forms
if (pair === null) {
// Trim the remaining line to see if it is a bare key
const bareKey = noInlineComment.trim();
// Validate the bare key against the allowed key regex
const isBareValid = VALID_KEY_REGEX.test(bareKey);
// Treat a valid bare key (e.g., "WITH_NO_EQUAL_SIGN") as an empty assignment
if (isBareValid) {
// Assign an empty string for the bare key
map[bareKey] = '';
// Continue to the next input line after recording the empty assignment
continue;
}
// Continue to the next input line if not a valid bare key
continue;
}
// Extract the key name from the parsed pair
const name = pair.key;
// Validate the key against the allowed regex and skip if it is invalid
if (!VALID_KEY_REGEX.test(name)) {
continue;
}
// Extract the raw value for post-processing
const rawValue = pair.value;
// Clean the value by unquoting and unescaping if necessary
const value = unquoteValue(rawValue);
// Record the value in the map so later duplicates overwrite earlier ones
map[name] = value !== null && value !== void 0 ? value : '';
}
// Convert the map into a stable array of EnvEntry items
const entries = Object.entries(map).map(([k, v]) => {
// Define the entry name explicitly
const name = k;
// Define the entry value explicitly, ensuring it is a string
const value = String(v !== null && v !== void 0 ? v : '');
// Return the structured environment entry
return { name, value };
});
// Return the final parsed entries
return entries;
}
exports.toEnv = toEnv;
function toVolumes(kvs) {
if (!kvs) {
return [];
}
const list = [];
for (let e of kvs) {
const parts = e.split('@');
let uri = parts[0];
if (!uri) {
continue;
}
let path = parts.slice(1).join('@');
list.push({
uri,
path,
});
}
return list;
}
exports.toVolumes = toVolumes;
function sortByKindPriority(resources, reverse = false) {
const sortedResources = [];
for (const resource of resources) {
if (resource.kind === 'list' || resource.kind === 'queryresult') {
for (let item of resource.items) {
sortedResources.push(item);
}
}
else {
sortedResources.push(resource);
}
}
sortIHasKindByPriority(sortedResources, reverse);
return sortedResources;
}
exports.sortByKindPriority = sortByKindPriority;
function sortIHasKindByPriority(resources, reverse) {
if (resources.length === 0) {
return;
}
resources.sort((a, b) => {
const indexA = exports.kindPriorityOrder.indexOf(a.kind);
const indexB = exports.kindPriorityOrder.indexOf(b.kind);
// If a's kind isn't found, set its index to Infinity so it's placed at the end
if (indexA === -1)
return 1;
// If b's kind isn't found, set its index to Infinity so it's placed at the end
if (indexB === -1)
return -1;
return indexA - indexB;
});
if (reverse) {
resources.reverse();
}
}
exports.sortIHasKindByPriority = sortIHasKindByPriority;
function computeHash(data) {
// Create an SHA-256 hash instance
const hash = crypto.createHash('sha256');
// Update the hash with the serialized data
hash.update(JSON.stringify(data));
// Return the hash in hexadecimal format
return hash.digest('hex');
}
exports.computeHash = computeHash;
// Internal File Use Functions //
/**
* Removes an inline comment that starts with '#' when the '#' is outside of quotes.
* - Preserves '#' characters that appear within single or double quoted strings.
*
* @param {string} line - A single logical line that may contain an inline comment.
* @returns {string} - The line with any trailing comment removed and whitespace trimmed on the right.
*/
function stripInlineCommentSafe(line) {
// Track whether the scan is currently inside a single or double quote
let inQuote = null;
// Track whether the current character should be treated as escaped
let escaped = false;
// Loop index for the character scan
let i = 0;
// Iterate over the input characters
for (i = 0; i < line.length; i++) {
// Current character under inspection
const ch = line[i];
// Handle previously escaped character
if (escaped) {
// Reset the escaped flag after consuming the escaped character
escaped = false;
// Continue scanning the next character
continue;
}
// If a backslash is found, mark the next character as escaped
if (ch === '\\') {
// Mark the next character as escaped
escaped = true;
// Continue scanning after the backslash
continue;
}
// If inside a quoted region, only check for the closing quote
if (inQuote !== null) {
// Close the quote if we see the matching quote character
if (ch === inQuote) {
// Exit quoted mode
inQuote = null;
}
// Continue scanning inside the quoted region
continue;
}
// If encountering an opening quote, enter quoted mode
if (ch === "'" || ch === '"') {
// Enter quoted mode using the detected quote character
inQuote = ch;
// Continue scanning after entering quoted mode
continue;
}
// If encountering a '#', treat the rest of the line as an inline comment
if (ch === '#') {
// Slice everything before the '#' and trim trailing whitespace
const result = line.slice(0, i).trimEnd();
// Return the stripped line
return result;
}
}
// If no inline comment boundary is found, return the original line trimmed on the right
return line.trimEnd();
}
/**
* Splits a logical "KEY=VALUE" or "KEY: VALUE" line into a key and value,
* ignoring separators found inside quotes.
*
* @param {string} line - A single logical line with export/comment already stripped.
* @returns {KeyValuePair | null} - Parsed key/value pair or null if no separator found.
*/
function splitKeyValue(line) {
// Track quote state to ignore separators inside quotes
let inQuote = null;
// Track escape state for the current character
let escaped = false;
// Current loop index while scanning characters
let i = 0;
// Iterate to find the first unquoted '=' or ':'
for (i = 0; i < line.length; i++) {
// Current character
const ch = line[i];
// If the previous character escaped this one, continue
if (escaped) {
// Reset escaped flag after consuming the escaped character
escaped = false;
// Continue to next character
continue;
}
// If a backslash is seen, mark the next character as escaped
if (ch === '\\') {
// Mark escape for the next character
escaped = true;
// Continue to next character
continue;
}
// If in a quoted region, only check for closing the quote
if (inQuote !== null) {
// Close the quote when encountering the matching quote character
if (ch === inQuote) {
// Exit quoted mode
inQuote = null;
}
// Continue scanning inside quotes
continue;
}
// If encountering an opening quote, enter quoted mode
if (ch === "'" || ch === '"') {
// Set the current quote character
inQuote = ch;
// Continue to next character after entering quoted mode
continue;
}
// Accept '=' or ':' as the first unquoted separator
if (ch === '=' || ch === ':') {
// Slice the key to the left of the separator and trim
const key = line.slice(0, i).trim();
// Slice the value to the right of the separator and trim
const value = line.slice(i + 1).trim();
// Return the structured pair as a KeyValuePair
const pair = { key, value };
// Return the parsed pair to the caller
return pair;
}
}
// Return null if no valid separator was found
const noSep = null;
// Return the null sentinel to indicate failure to split
return noSep;
}
/**
* Unquotes and unescapes a value if wrapped in single or double quotes.
* - Single quotes: literal except for handling of \\' and \\\\.
* - Double quotes: supports \\n, \\r, \\t, \\\", \\\', \\\\ sequences.
*
* @param {string} value - Raw value that may be quoted and escaped.
* @returns {string} - Clean, ready-to-use value string.
*/
function unquoteValue(value) {
// Return an empty string when value is empty or undefined
if (!value) {
return '';
}
// Determine whether the value is enclosed in matching quotes
const hasDouble = value.startsWith('"') && value.endsWith('"');
const hasSingle = value.startsWith("'") && value.endsWith("'");
const isQuoted = hasDouble || hasSingle;
// If the value is not quoted, return a trimmed version
if (!isQuoted) {
// Trim surrounding whitespace for unquoted values
const trimmed = value.trim();
// Return the trimmed unquoted value
return trimmed;
}
// Determine the quote character used
const q = value[0];
// Extract the inner content (without the surrounding quotes)
const inner = value.slice(1, -1);
// Handle single-quoted strings (mostly literal)
if (q === "'") {
// Replace escaped single quote and backslash
const singleProcessed = inner.replace(/\\'/g, "'").replace(/\\\\/g, '\\');
// Return the processed single-quoted value
return singleProcessed;
}
// Handle double-quoted strings with common escape sequences
const doubleProcessed = inner
.replace(/\\n/g, '\n')
.replace(/\\r/g, '\r')
.replace(/\\t/g, '\t')
.replace(/\\"/g, '"')
.replace(/\\'/g, "'")
.replace(/\\\\/g, '\\');
// Return the processed double-quoted content
return doubleProcessed;
}
//# sourceMappingURL=objects.js.map