UNPKG

storyblok

Version:
1,719 lines (1,692 loc) 326 kB
#!/usr/bin/env node import 'dotenv/config'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { resolve, dirname, join, parse, extname, relative, isAbsolute, basename } from 'pathe'; import { existsSync, mkdirSync, appendFileSync, writeFileSync, readdirSync, unlinkSync, readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { loadConfig as loadConfig$1, SUPPORTED_EXTENSIONS } from 'c12'; import chalk from 'chalk'; import { readPackageUp } from 'read-package-up'; import { Command } from 'commander'; import { MultiBar, Presets } from 'cli-progress'; import { Spinner } from '@topcli/spinner'; import fs, { mkdir, writeFile, readdir, readFile as readFile$1, appendFile, access, constants, unlink } from 'node:fs/promises'; import filenamify from 'filenamify'; import { createManagementApiClient, normalizeAssetUrl } from '@storyblok/management-api-client'; import { select, password, input, confirm } from '@inquirer/prompts'; import { exec, spawn } from 'node:child_process'; import { promisify } from 'node:util'; import { minimatch } from 'minimatch'; import { Readable, pipeline, Transform, Writable } from 'node:stream'; import { Sema } from 'async-sema'; import { hash } from 'ohash'; import { compile } from 'json-schema-to-typescript'; import open from 'open'; import { Octokit } from 'octokit'; import { pipeline as pipeline$1 } from 'node:stream/promises'; import { Buffer } from 'node:buffer'; import Storyblok from 'storyblok-js-client'; const commands = { LOGIN: "login", LOGOUT: "logout", SIGNUP: "signup", USER: "user", COMPONENTS: "components", LANGUAGES: "languages", MIGRATIONS: "migrations", TYPES: "types", DATASOURCES: "datasources", CREATE: "create", LOGS: "logs", REPORTS: "reports", ASSETS: "assets", STORIES: "stories" }; const colorPalette = { PRIMARY: "#8d60ff", LOGIN: "#dad4ff", LOGOUT: "#6d6d6d", SIGNUP: "#b6ff6d", USER: "#71d300", COMPONENTS: "#a185ff", LANGUAGES: "#f5c003", MIGRATIONS: "#8CE2FF", TYPES: "#3178C6", CREATE: "#ffb3ba", GROUPS: "#4ade80", TAGS: "#fbbf24", PRESETS: "#a855f7", DATASOURCES: "#4ade80", LOGS: "#4ade80", REPORTS: "#4ade80", ASSETS: "#f97316", STORIES: "#a185ff" }; const regions = { EU: "eu", US: "us", CN: "cn", CA: "ca", AP: "ap" }; const regionsDomain = { eu: "api.storyblok.com", us: "api-us.storyblok.com", cn: "app.storyblokchina.cn", ca: "api-ca.storyblok.com", ap: "api-ap.storyblok.com" }; const managementApiRegions = { eu: "mapi.storyblok.com", us: "api-us.storyblok.com", cn: "app.storyblokchina.cn", ca: "api-ca.storyblok.com", ap: "api-ap.storyblok.com" }; const appDomains = { eu: "app.storyblok.com", us: "app-us.storyblok.com", cn: "app.storyblokchina.cn", ca: "app-ca.storyblok.com", ap: "app-ap.storyblok.com" }; const regionNames = { eu: "Europe", us: "United States", cn: "China", ca: "Canada", ap: "Australia" }; ({ SB_Agent_Version: process.env.npm_package_version || "4.x" }); const SUPPORTED_ASSET_EXTENSIONS = /* @__PURE__ */ new Set([ // Images: image/png, image/x-png, image/gif, image/jpeg, image/avif, image/svg+xml, image/webp ".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif", ".svg", // Video: video/*, application/mp4, application/x-mpegurl, application/vnd.apple.mpegurl ".mp4", ".mov", ".avi", ".webm", ".wmv", ".mkv", ".flv", ".ogv", ".3gp", ".m4v", ".mpg", ".mpeg", ".m3u8", // Audio: audio/* ".mp3", ".wav", ".ogg", ".aac", ".flac", ".wma", ".m4a", ".opus", // Documents: application/msword, text/plain, application/pdf, application/vnd.openxmlformats-officedocument.wordprocessingml.document ".pdf", ".doc", ".docx", ".txt" ]); const directories = { assets: "assets", components: "components", datasources: "datasources", logs: "logs", reports: "reports", stories: "stories" }; const chunk = (items, size) => { const all = Array.from(items); if (all.length === 0) { return []; } const chunks = []; for (let i = 0; i < all.length; i += size) { chunks.push(all.slice(i, i + size)); } return chunks; }; function isPlainObject(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function mergeDeep(target, source) { if (!isPlainObject(source)) { return target; } const targetRecord = target; for (const [key, value] of Object.entries(source)) { if (isPlainObject(value)) { const existing = targetRecord[key]; const base = isPlainObject(existing) ? existing : {}; targetRecord[key] = mergeDeep(base, value); } else { targetRecord[key] = value; } } return target; } const BASE_GLOBAL_CONFIG = { region: void 0, space: void 0, path: void 0, api: { maxRetries: 3, maxConcurrency: 6 }, log: { console: { enabled: false, level: "info" }, file: { enabled: true, level: "info", maxFiles: 10 } }, report: { enabled: true, maxFiles: 10 }, ui: { enabled: true }, verbose: false }; const DEFAULT_GLOBAL_CONFIG = Object.freeze( structuredClone(BASE_GLOBAL_CONFIG) ); function createDefaultResolvedConfig() { return structuredClone(BASE_GLOBAL_CONFIG); } async function loadConfig(options) { return loadConfig$1({ name: options.name, cwd: options.cwd, configFile: options.configFile, defaults: options.defaults || {}, rcFile: false, globalRc: false, dotenv: false, packageJson: false }); } const CONFIG_FILE_NAME = "storyblok.config"; const HIDDEN_CONFIG_DIR = ".storyblok"; const HIDDEN_CONFIG_FILE_NAME = "config"; function setValueAtPath(target, path, value) { if (!path.length) { return; } let current = target; path.forEach((key, index) => { if (index === path.length - 1) { current[key] = value; return; } if (!isPlainObject(current[key])) { current[key] = {}; } current = current[key]; }); } function getValueAtPath(source, path) { return path.reduce((accumulator, key) => { if (accumulator === null || typeof accumulator !== "object") { return void 0; } return accumulator[key]; }, source); } function extractDirectValues(input) { const direct = {}; for (const [key, value] of Object.entries(input)) { if (isPlainObject(value)) { continue; } direct[key] = value; } return direct; } function getCommandAncestry(command) { const chain = []; let current = command; while (current) { chain.unshift(current); current = current.parent; } return chain; } function getOptionPath(option) { const longFlag = option.long || option.flags.split(",").pop()?.trim(); if (!longFlag) { return [option.attributeName()]; } let normalized = longFlag.replace(/^--/, ""); const isNegated = normalized.startsWith("no-"); if (isNegated) { normalized = normalized.replace(/^no-/, ""); } const segments = normalized.split("-"); const path = []; let currentConfig = DEFAULT_GLOBAL_CONFIG; let i = 0; while (i < segments.length) { const segment = segments[i]; const currentAsRecord = currentConfig; if (currentConfig && isPlainObject(currentAsRecord[segment])) { path.push(segment); currentConfig = currentAsRecord[segment]; i++; } else { const remainingSegments = segments.slice(i); const camelCased = remainingSegments.map((seg, idx) => idx === 0 ? seg : seg.charAt(0).toUpperCase() + seg.slice(1)).join(""); path.push(camelCased); break; } } return path; } function resolveConfigFilePath(cwd, configFile) { for (const ext of SUPPORTED_EXTENSIONS) { const candidate = resolve(cwd, `${configFile}${ext}`); if (existsSync(candidate)) { return candidate; } } return null; } async function loadConfigLayer({ cwd, configFile }) { if (!existsSync(cwd)) { return null; } const filePath = resolveConfigFilePath(cwd, configFile); if (!filePath) { return null; } const { config } = await loadConfig({ name: "storyblok", cwd, configFile }); return config ?? null; } async function loadConfigLayers() { const cwd = process.cwd(); const locations = [ { cwd: resolve(homedir(), HIDDEN_CONFIG_DIR), configFile: HIDDEN_CONFIG_FILE_NAME }, { cwd: resolve(cwd, HIDDEN_CONFIG_DIR), configFile: HIDDEN_CONFIG_FILE_NAME }, { cwd, configFile: CONFIG_FILE_NAME } ]; const layers = []; for (const location of locations) { const layer = await loadConfigLayer(location); if (layer) { layers.push(layer); } } return layers; } function collectGlobalDefaults(root, baseDefaults) { const defaults = baseDefaults; for (const option of root.options) { if (option.defaultValue === void 0) { continue; } setValueAtPath(defaults, getOptionPath(option), option.defaultValue); } return defaults; } function collectLocalDefaults(commands) { const defaults = {}; for (const command of commands) { for (const option of command.options) { if (option.defaultValue === void 0) { continue; } const attrName = option.attributeName(); if (!(attrName in defaults)) { defaults[attrName] = option.defaultValue; } } } return defaults; } function applyCliOverrides(commandChain, globalResolved, localResolved) { const [root] = commandChain; for (const command of commandChain) { const isRoot = command === root; for (const option of command.options) { const attrName = option.attributeName(); const source = command.getOptionValueSource(attrName); if (!source || source === "default" || source === "config") { continue; } const value = command.getOptionValue(attrName); if (isRoot) { setValueAtPath(globalResolved, getOptionPath(option), value); delete localResolved[attrName]; } else { localResolved[attrName] = value; } } } } function applyConfigToCommander(commandChain, resolved) { for (const command of commandChain) { for (const option of command.options) { const attrName = option.attributeName(); const source = command.getOptionValueSource(attrName); if (source && source !== "default" && source !== "config") { continue; } const value = getValueAtPath(resolved, getOptionPath(option)); if (value === void 0) { continue; } command.setOptionValueWithSource(attrName, value, "config"); } } } function parseNumber(value) { const parsed = Number.parseInt(value, 10); if (Number.isNaN(parsed)) { throw new TypeError(`Invalid number value "${value}".`); } return parsed; } const GLOBAL_OPTION_DEFINITIONS = [ { flags: "-p, --path <path>", description: "Base directory for file storage (default: .storyblok)", defaultValue: DEFAULT_GLOBAL_CONFIG.path }, { flags: "--verbose", description: "Enable verbose output", defaultValue: DEFAULT_GLOBAL_CONFIG.verbose }, { flags: "--region <region>", description: "Storyblok region used for API requests", defaultValue: DEFAULT_GLOBAL_CONFIG.region }, { flags: "--api-max-retries <number>", description: "Maximum retry attempts for HTTP requests", defaultValue: DEFAULT_GLOBAL_CONFIG.api.maxRetries, parser: parseNumber }, { flags: "--api-max-concurrency <number>", description: "Maximum concurrent API requests executed by the CLI", defaultValue: DEFAULT_GLOBAL_CONFIG.api.maxConcurrency, parser: parseNumber }, // Boolean flags that default to true need both positive and negative forms { flags: "--log-console-enabled", description: "Enable console logging output", defaultValue: DEFAULT_GLOBAL_CONFIG.log.console.enabled }, { flags: "--no-log-console-enabled", description: "Disable console logging output", defaultValue: DEFAULT_GLOBAL_CONFIG.log.console.enabled }, { flags: "--log-console-level <level>", description: "Console log level threshold", defaultValue: DEFAULT_GLOBAL_CONFIG.log.console.level }, { flags: "--log-file-enabled", description: "Enable file logging output", defaultValue: DEFAULT_GLOBAL_CONFIG.log.file.enabled }, { flags: "--no-log-file-enabled", description: "Disable file logging output", defaultValue: DEFAULT_GLOBAL_CONFIG.log.file.enabled }, { flags: "--log-file-level <level>", description: "File log level threshold", defaultValue: DEFAULT_GLOBAL_CONFIG.log.file.level }, { flags: "--log-file-max-files <number>", description: "Maximum amount of log files to keep on disk", defaultValue: DEFAULT_GLOBAL_CONFIG.log.file.maxFiles, parser: parseNumber }, { flags: "--ui-enabled", description: "Enable UI output", defaultValue: DEFAULT_GLOBAL_CONFIG.ui.enabled }, { flags: "--no-ui-enabled", description: "Disable UI output", defaultValue: DEFAULT_GLOBAL_CONFIG.ui.enabled }, { flags: "--report-enabled", description: "Enable report generation after command execution", defaultValue: DEFAULT_GLOBAL_CONFIG.report.enabled }, { flags: "--no-report-enabled", description: "Disable report generation after command execution", defaultValue: DEFAULT_GLOBAL_CONFIG.report.enabled }, { flags: "--report-max-files <number>", description: "Maximum number of report files to keep", defaultValue: DEFAULT_GLOBAL_CONFIG.report.maxFiles, parser: parseNumber } ]; function getModuleNames(root) { return new Set( root.commands.filter((cmd) => cmd.commands.length > 0).map((cmd) => cmd.name()) ); } function warnUnknownModuleKeys(modules, knownKeys) { for (const key of Object.keys(modules)) { if (!knownKeys.has(key)) { console.warn(`[storyblok] Unknown module "${key}" in config file. Known modules: ${[...knownKeys].join(", ")}`); } } } function mergeModuleConfig(target, modulesConfig, commands) { let currentLevel = modulesConfig; for (const command of commands) { if (!isPlainObject(currentLevel)) { return; } const segment = currentLevel[command.name()]; if (segment === void 0) { return; } if (isPlainObject(segment)) { Object.assign(target, extractDirectValues(segment)); currentLevel = segment; } else { Object.assign(target, { [command.name()]: segment }); return; } } } async function resolveConfig(thisCommand, ancestry) { let commandChain; if (Array.isArray(ancestry)) { commandChain = ancestry; } else if (ancestry) { commandChain = getCommandAncestry(ancestry); } else { commandChain = getCommandAncestry(thisCommand); } const [root, ...rest] = commandChain; const defaultConfig = createDefaultResolvedConfig(); const globalResolved = collectGlobalDefaults(root, defaultConfig); const localResolved = collectLocalDefaults(rest); const layers = await loadConfigLayers(); const knownModuleKeys = getModuleNames(root); for (const layer of layers) { const { modules, ...globalLayer } = layer; mergeDeep(globalResolved, globalLayer); if (modules && isPlainObject(modules)) { warnUnknownModuleKeys(modules, knownModuleKeys); mergeModuleConfig(localResolved, modules, rest); } } applyCliOverrides(commandChain, globalResolved, localResolved); const resolved = structuredClone(defaultConfig); mergeDeep(resolved, globalResolved); Object.assign(resolved, localResolved); if (resolved.space != null) { resolved.space = String(resolved.space); } return resolved; } let activeConfig = createDefaultResolvedConfig(); function getActiveConfig() { return activeConfig; } function setActiveConfig(config) { activeConfig = structuredClone(config); } class FetchError extends Error { response; request; constructor(message, response, request = {}) { super(message); this.name = "FetchError"; this.response = response; this.request = request; } } const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function customFetch(url, options = {}) { const { api } = getActiveConfig(); const maxRetries = options.maxRetries ?? api.maxRetries; const baseDelay = options.baseDelay ?? 500; const requestContext = { url, method: options.method ?? "GET" }; let attempt = 0; while (attempt <= maxRetries) { try { const headers = { "Content-Type": "application/json", ...options.headers }; const fetchOptions = { ...options, headers }; if (options.body) { fetchOptions.body = typeof options.body === "string" ? options.body : JSON.stringify(options.body); } const response = await fetch(url, fetchOptions); let data; try { data = await response.json(); } catch { throw new FetchError(`Non-JSON response`, { status: response.status, statusText: response.statusText, data: null }, requestContext); } if (!response.ok) { if (response.status === 429 && attempt < maxRetries) { const waitTime = baseDelay * 2 ** attempt; await delay(waitTime); attempt++; continue; } throw new FetchError(`HTTP error! status: ${response.status}`, { status: response.status, statusText: response.statusText, data }, requestContext); } return { ...data, perPage: Number(response.headers.get("Per-Page")), total: Number(response.headers.get("Total")) }; } catch (error) { if (error instanceof FetchError) { throw error; } throw new FetchError(error instanceof Error ? error.message : String(error), { status: 0, statusText: "Network Error", data: null }, requestContext); } } throw new FetchError("Max retries exceeded", { status: 429, statusText: "Rate Limit Exceeded", data: null }, requestContext); } const API_ACTIONS = { login: "login", login_with_token: "Failed to log in with token", login_with_otp: "Failed to log in with email, password and otp", login_email_password: "Failed to log in with email and password", get_user: "Failed to get user", pull_languages: "Failed to pull languages", pull_components: "Failed to pull components", pull_component_groups: "Failed to pull component groups", pull_component_presets: "Failed to pull component presets", pull_component_internal_tags: "Failed to pull component internal tags", push_component: "Failed to push component", push_component_group: "Failed to push component group", push_component_preset: "Failed to push component preset", push_component_internal_tag: "Failed to push component internal tag", update_component: "Failed to update component", update_component_internal_tag: "Failed to update component internal tag", update_component_group: "Failed to update component group", update_component_preset: "Failed to update component preset", delete_component_preset: "Failed to delete component preset", pull_stories: "Failed to pull stories", pull_story: "Failed to pull story", create_story: "Failed to create story", update_story: "Failed to update story", pull_asset: "Failed to pull asset", pull_assets: "Failed to pull assets", pull_asset_folder: "Failed to pull asset folder", pull_asset_folders: "Failed to pull asset folders", push_asset_folder: "Failed to push asset folder", push_asset_create: "Failed to create asset", push_asset_update: "Failed to update asset", pull_datasources: "Failed to pull datasources", push_datasource: "Failed to push datasource", update_datasource: "Failed to update datasource", delete_datasource: "Failed to delete datasource", delete_datasource_entry: "Failed to delete datasource entry", create_space: "Failed to create space", pull_spaces: "Failed to pull spaces", fetch_blueprints: "Failed to fetch blueprints from GitHub" }; const API_ERRORS = { unauthorized: "The user is not authorized to access the API", network_error: "No response from server, please check if you are correctly connected to internet", server_error: "The server returned an error", invalid_credentials: "The provided credentials are invalid", timeout: "The API request timed out", generic: "Error fetching data from the API", not_found: "The requested resource was not found", unprocessable_entity: "The request was well-formed but was unable to be followed due to semantic errors" }; function getErrorId(status) { switch (status) { case 401: return "unauthorized"; case 404: return "not_found"; case 422: return "unprocessable_entity"; default: return status >= 500 ? "server_error" : "generic"; } } function handleAPIError(action, error, customMessage) { if (error instanceof FetchError) { const errorId = getErrorId(error.response.status); throw new APIError(errorId, action, error, customMessage); } const response = error?.response; if (response?.status) { const reqCandidate = error?.request; const wrappedError = new FetchError( response.statusText ?? error.message, { status: response.status, statusText: response.statusText ?? "", data: response.data }, { url: typeof reqCandidate?.url === "string" ? reqCandidate.url : void 0, method: typeof reqCandidate?.method === "string" ? reqCandidate.method : void 0 } ); const errorId = getErrorId(response.status); throw new APIError(errorId, action, wrappedError, customMessage); } throw new APIError("generic", action, error, customMessage); } class APIError extends Error { errorId; cause; code; messageStack; error; response; constructor(errorId, action, error, customMessage) { super(customMessage || API_ERRORS[errorId]); this.name = "API Error"; this.errorId = errorId; this.cause = API_ERRORS[errorId]; this.code = error?.response?.status || 0; this.messageStack = []; this.error = error; this.response = error?.response; if (!customMessage) { this.messageStack.push(API_ACTIONS[action]); } this.messageStack.push(customMessage || API_ERRORS[errorId]); if (this.code === 422) { const responseData = this.response?.data; if (responseData?.name?.[0] === "has already been taken") { this.message = "A component with this name already exists"; } Object.entries(responseData || {}).forEach(([key, errors]) => { if (Array.isArray(errors)) { errors.forEach((e) => { this.messageStack.push(`${key}: ${e}`); }); } }); } } getInfo() { const request = this.error?.request; const hasRequestContext = Boolean(request && (request.url || request.method)); return { name: this.name, message: this.message, httpCode: this.code, cause: this.cause, errorId: this.errorId, stack: this.stack, responseData: this.response?.data, ...hasRequestContext ? { request: { url: request.url, method: request.method } } : {} }; } } class CommandError extends Error { constructor(message) { super(message); this.name = "Command Error"; } getInfo() { return { name: this.name, message: this.message, stack: this.stack }; } } class Logger { transports = []; context = {}; constructor(options) { if (options?.transports) { this.transports = options.transports; } if (options?.context) { this.context = options.context; } } log(level, message, context) { const timestamp = /* @__PURE__ */ new Date(); const mergedContext = context ? { ...this.context, ...context } : this.context; const record = { timestamp, level, message, context: Object.keys(mergedContext).length ? mergedContext : void 0 }; for (const transport of this.transports) { transport.log(record); } } error(message, context) { this.log("error", message, context); } warn(message, context) { this.log("warn", message, context); } info(message, context) { this.log("info", message, context); } debug(message, context) { this.log("debug", message, context); } } let loggerInstance = null; function getLogger(options) { if (!loggerInstance) { loggerInstance = new Logger(options); } return loggerInstance; } function setLoggerTransports(transports) { if (loggerInstance) { loggerInstance.transports = transports; } } const FS_ERRORS = { file_not_found: "The file requested was not found", permission_denied: "Permission denied while accessing the file", operation_on_directory: "The operation is not allowed on a directory", not_a_directory: "The path provided is not a directory", file_already_exists: "The file already exists", directory_not_empty: "The directory is not empty", too_many_open_files: "Too many open files", no_space_left: "No space left on the device", invalid_argument: "An invalid argument was provided", unknown_error: "An unknown error occurred" }; const FS_ACTIONS = { read: "Failed to read/parse file:", write: "Writing file", delete: "Deleting file", mkdir: "Creating directory", rmdir: "Removing directory", authorization_check: "Failed to check authorization in .netrc file:" }; function handleFileSystemError(action, error) { if (error.code) { switch (error.code) { case "ENOENT": throw new FileSystemError("file_not_found", action, error); case "EACCES": case "EPERM": throw new FileSystemError("permission_denied", action, error); case "EISDIR": throw new FileSystemError("operation_on_directory", action, error); case "ENOTDIR": throw new FileSystemError("not_a_directory", action, error); case "EEXIST": throw new FileSystemError("file_already_exists", action, error); case "ENOTEMPTY": throw new FileSystemError("directory_not_empty", action, error); case "EMFILE": throw new FileSystemError("too_many_open_files", action, error); case "ENOSPC": throw new FileSystemError("no_space_left", action, error); case "EINVAL": throw new FileSystemError("invalid_argument", action, error); default: throw new FileSystemError("unknown_error", action, error); } } else { throw new FileSystemError("unknown_error", action, error); } } class FileSystemError extends Error { errorId; cause; code; messageStack; error; constructor(errorId, action, error, customMessage) { super(customMessage || FS_ERRORS[errorId]); this.name = "File System Error"; this.errorId = errorId; this.cause = FS_ERRORS[errorId]; this.code = error.code; this.messageStack = []; this.error = error; if (!customMessage) { this.messageStack.push(FS_ACTIONS[action]); } this.messageStack.push(customMessage || FS_ERRORS[errorId]); } getInfo() { return { name: this.name, message: this.message, code: this.code, cause: this.cause, errorId: this.errorId, stack: this.stack }; } } function hasMessage(error) { return typeof error === "object" && error !== null && "message" in error && typeof error.message === "string"; } function toError(maybeError) { if (maybeError instanceof Error) { return maybeError; } if (typeof maybeError === "string") { return new Error(maybeError); } if (hasMessage(maybeError)) { return new Error(maybeError.message); } try { return new Error(JSON.stringify(maybeError)); } catch { return new Error(String(maybeError)); } } function handleVerboseError(error) { if (error instanceof CommandError || error instanceof APIError || error instanceof FileSystemError) { const errorDetails = "getInfo" in error ? error.getInfo() : {}; if (error instanceof CommandError) { konsola.error(`Command Error: ${error.getInfo().message}`, errorDetails); } else if (error instanceof APIError) { konsola.error(`API Error: ${error.getInfo().cause}`, errorDetails); } else if (error instanceof FileSystemError) { konsola.error(`File System Error: ${error.getInfo().cause}`, errorDetails); } else { konsola.error(`Unexpected Error: ${error}`, errorDetails); } } else { konsola.error(`Unexpected Error`, error); } } function handleError(error, verbose = false, context) { if (error instanceof APIError || error instanceof FileSystemError) { const messageStack = error.messageStack; messageStack.forEach((message, index) => { konsola.error(message, null, { header: index === 0, margin: false }); }); } else { konsola.error(error.message, null, { header: true }); } if (verbose) { handleVerboseError(error); } else { konsola.br(); konsola.info("For more information about the error, run the command with the `--verbose` flag"); } if (!process.env.VITEST) { console.log(""); } getLogger().error(error.message, { error, errorCode: "code" in error ? String(error.code) : "UNKNOWN_ERROR", context }); } function logOnlyError(error, context) { getLogger().error(error.message, { error, errorCode: "code" in error ? String(error.code) : "UNKNOWN_ERROR", context }); } function requireAuthentication(state, verbose = false) { if (!state.isLoggedIn || !state.password || !state.region) { handleError( new CommandError(`You are currently not logged in. Please run ${chalk.hex(colorPalette.PRIMARY)("storyblok login")} to authenticate, or ${chalk.hex(colorPalette.PRIMARY)("storyblok signup")} to sign up.`), verbose ); return false; } return true; } const toCamelCase = (str) => { return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/_/g, "").replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/[^a-z0-9]([a-z])/gi, (_, letter) => letter.toUpperCase()).replace(/[^a-z0-9]/gi, ""); }; const toPascalCase = (str) => { const camelCase = toCamelCase(str); return camelCase ? camelCase[0].toUpperCase() + camelCase.slice(1) : camelCase; }; const capitalize = (str) => { return str.charAt(0).toUpperCase() + str.slice(1); }; const toHumanReadable = (str) => { return str.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ").replace(/\s+/g, " ").trim(); }; function maskToken(token) { if (token.length <= 4) { return token; } const visiblePart = token.slice(0, 4); const maskedPart = "*".repeat(token.length - 4); return `${visiblePart}${maskedPart}`; } function createRegexFromGlob(pattern) { return new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*")}$`); } function formatHeader(title) { return `${title}`; } const konsola = { title: (message, color, subtitle) => { if (subtitle) { console.log(`${formatHeader(chalk.bgHex(color).bold(` ${capitalize(message)} `))} ${subtitle}`); } else { console.log(formatHeader(chalk.bgHex(color).bold(` ${capitalize(message)} `))); } console.log(""); console.log(""); }, br: () => { console.log(""); }, ok: (message, header = false) => { if (header) { console.log(""); const successHeader = chalk.bgGreen.bold.white(` Success `); console.log(formatHeader(successHeader)); console.log(""); } console.log(message ? `${chalk.green("\u2714")} ${message}` : ""); }, info: (message, options = { header: false, margin: true }) => { if (options.header) { console.log(""); const infoHeader = chalk.bgBlue.bold.white(` Info `); console.log(formatHeader(infoHeader)); } console.log(message ? `${chalk.blue("\u2139")} ${message}` : ""); if (options.margin) { console.error(""); } }, warn: (message, header = false) => { if (header) { console.log(""); const warnHeader = chalk.bgYellow.bold.black(` Warning `); console.warn(formatHeader(warnHeader)); } console.warn(message ? `${chalk.yellow("\u26A0\uFE0F ")} ${message}` : ""); }, error: (message, info, options) => { if (options?.header) { const errorHeader = chalk.bgRed.bold.white(` Error `); console.error(formatHeader(errorHeader)); console.log(""); } console.error(`${chalk.red.bold("\u25B2 error")} ${message}`, info || ""); if (options?.margin) { console.error(""); } } }; const __filename$2 = fileURLToPath(import.meta.url); const __dirname$2 = dirname(__filename$2); const result = await readPackageUp({ cwd: __dirname$2 }); const packageJson$1 = result ? result.packageJson : { name: "storyblok", description: "Storyblok CLI", version: "0.0.0" }; if (!result) { console.debug("Metadata not found"); } function getPackageJson() { return packageJson$1; } const __filename$1 = fileURLToPath(import.meta.url); const __dirname$1 = dirname(__filename$1); function isRegion(value) { return Object.values(regions).includes(value); } const isVitest = process.env.VITEST === "true"; const noopProgressBar = { increment: () => { }, setTotal: () => { }, stop: () => { } }; const noopSpinner = { failed: (_title) => { }, succeed: (_title) => { }, elapsedTime: 0 }; class UI { console; enabled; multiBar; constructor({ enabled }) { this.console = enabled ? console : null; this.enabled = enabled; this.multiBar = enabled ? new MultiBar({ clearOnComplete: false, format: `${chalk.bold(" {title} ")} ${chalk.hex(colorPalette.PRIMARY)("[{bar}]")} {percentage}% | {eta_formatted} | {value}/{total} processed`, etaBuffer: 60 }, Presets.rect) : null; } title(message, color, subtitle) { if (subtitle) { this.console?.log(`${chalk.bgHex(color).bold(` ${capitalize(message)} `)} ${subtitle}`); } else { this.console?.log(chalk.bgHex(color).bold(` ${capitalize(message)} `)); } this.br(); this.br(); } br() { this.console?.log(""); } ok(message, header = false) { if (header) { this.br(); const successHeader = chalk.bgGreen.bold.white(` Success `); this.console?.log(successHeader); this.br(); } this.console?.log(message ? `${chalk.green("\u2714")} ${message}` : ""); } info(message, options = {}) { const { header = false, margin = true } = options; if (header) { this.br(); const infoHeader = chalk.bgBlue.bold.white(` Info `); this.console?.info(infoHeader); } this.console?.info(message ? `${chalk.blue("\u2139")} ${message}` : ""); if (margin) { this.br(); } } warn(message, header = false) { if (header) { this.br(); const warnHeader = chalk.bgYellow.bold.black(` Warning `); this.console?.warn(warnHeader); } this.console?.warn(message ? `${chalk.yellow("\u26A0\uFE0F ")} ${message}` : ""); } error(message, info, options = {}) { const { header = false, margin = false } = options; if (header) { const errorHeader = chalk.bgRed.bold.white(` Error `); this.console?.error(errorHeader); this.br(); } this.console?.error(`${chalk.red.bold("\u25B2 error")} ${message}`, info || ""); if (margin) { this.br(); } } list(items) { for (const item of items) { this.console?.log(` ${item}`); } } createProgressBar(options) { const bar = this.multiBar?.create(0, 0, options); if (!bar) { return noopProgressBar; } return { increment: (count = 1) => bar.increment(count), // cli-progress renders `{eta_formatted}` as "LLs" when total is 0. // Floor at 1 so an empty phase stays a clean 0/1 instead. setTotal: (total) => bar.setTotal(Math.max(total, 1)), stop: () => bar.stop() }; } stopAllProgressBars() { this.multiBar?.stop(); } createSpinner(title) { return this.enabled ? new Spinner({ verbose: !isVitest }).start(title) : noopSpinner; } } let uiInstance = null; function getUI(options = { enabled: false }) { if (!uiInstance) { uiInstance = new UI(options); } return uiInstance; } const DEFAULT_STORAGE_DIR = ".storyblok"; const getStoryblokGlobalPath = () => { const homeDirectory = process.env[process.platform.startsWith("win") ? "USERPROFILE" : "HOME"] || process.cwd(); return join(homeDirectory, ".storyblok"); }; const saveToFile = async (filePath, data, options) => { const resolvedPath = parse(filePath).dir; try { await mkdir(resolvedPath, { recursive: true }); } catch (mkdirError) { handleFileSystemError("mkdir", mkdirError); return; } try { await writeFile(filePath, data, options); } catch (writeError) { handleFileSystemError("write", writeError); } }; const saveToFileSync = (filePath, data, options) => { const resolvedPath = parse(filePath).dir; if (resolvedPath) { try { mkdirSync(resolvedPath, { recursive: true }); } catch (mkdirError) { handleFileSystemError("mkdir", mkdirError); return; } } try { writeFileSync(filePath, data, options); } catch (writeError) { handleFileSystemError("write", writeError); } }; const appendToFile = async (filePath, data, options) => { const dataWithNewline = data.endsWith("\n") ? data : `${data} `; try { await appendFile(filePath, dataWithNewline, options); } catch (maybeError) { const error = toError(maybeError); if ("code" in error && error.code === "ENOENT") { const dir = parse(filePath).dir; await mkdir(dir, { recursive: true }); await appendFile(filePath, dataWithNewline, options); } else { handleFileSystemError( "syscall" in error && error.syscall === "mkdir" ? "mkdir" : "write", error ); } } }; const appendToFileSync = (filePath, data, options) => { const resolvedPath = parse(filePath).dir; try { mkdirSync(resolvedPath, { recursive: true }); } catch (mkdirError) { handleFileSystemError("mkdir", mkdirError); return; } try { const dataWithNewline = data.endsWith("\n") ? data : `${data} `; appendFileSync(filePath, dataWithNewline, options); } catch (writeError) { handleFileSystemError("write", writeError); } }; const readFile = async (filePath) => { try { return await readFile$1(filePath, "utf8"); } catch (error) { handleFileSystemError("read", error); return ""; } }; const loadManifest = async (manifestFile) => { return readFile$1(manifestFile, "utf8").then((manifest) => manifest.split("\n").filter(Boolean).map((entry) => JSON.parse(entry))).catch((error) => { if (error && error.code === "ENOENT") { return []; } throw error; }); }; const saveManifest = async (manifestFile, entries) => { const content = entries.map((entry) => JSON.stringify(entry)).join("\n"); await saveToFile(manifestFile, content ? `${content} ` : ""); }; const deduplicateManifest = async (manifestFile) => { const entries = await loadManifest(manifestFile); if (entries.length === 0) { return; } const uniqueEntries = /* @__PURE__ */ new Map(); for (const entry of entries) { uniqueEntries.set(entry.old_id, entry); } await saveManifest(manifestFile, Array.from(uniqueEntries.values())); }; const resolvePath = (path, folder) => { const basePath = path ?? DEFAULT_STORAGE_DIR; return resolve(process.cwd(), basePath, folder); }; function resolveCommandPath(commandPath, space, baseDir) { if (space) { return resolvePath(baseDir, join(commandPath, space)); } return resolvePath(baseDir, commandPath); } const getComponentNameFromFilename = (filename) => { return filename.replace(/\.js$/, ""); }; const sanitizeFilename = (filename) => { return filenamify(filename, { replacement: "_" }); }; async function readDirectory(directoryPath) { try { const files = await readdir(directoryPath); return files; } catch (maybeError) { handleFileSystemError("read", toError(maybeError)); return []; } } async function readJsonFile(filePath) { try { const content = (await readFile(filePath)).toString(); if (!content) { return { data: [] }; } const parsed = JSON.parse(content); return { data: Array.isArray(parsed) ? parsed : [parsed] }; } catch (error) { return { data: [], error }; } } function importModule(filePath) { return import(pathToFileURL(filePath).href); } async function fileExists(path) { try { await access(path, constants.F_OK); return true; } catch { return false; } } function filterJsonBySuffix(files, suffix) { return files.filter((file) => { if (!file.endsWith(".json")) { return false; } if (suffix) { return file.endsWith(`.${suffix}.json`); } return true; }); } const REPORT_STATUS = { unfinished: "UNFINISHED", success: "SUCCESS", partialSuccess: "PARTIAL_SUCCESS", failure: "FAILURE" }; class Reporter { filePath; enabled; startedAt = /* @__PURE__ */ new Date(); maxFiles; report = { status: REPORT_STATUS.unfinished, meta: { startedAt: this.startedAt.toISOString() }, summary: {} }; constructor(options) { this.enabled = options?.enabled || false; this.filePath = options?.filePath ?? `./${Date.now()}.json`; this.maxFiles = options?.maxFiles; } addMeta(key, value) { this.report.meta[key] = value; return this; } addSummary(key, value) { this.report.summary[key] = value; return this; } finalize() { if (!this.enabled) { return; } const endedAt = /* @__PURE__ */ new Date(); this.report.meta.endedAt = endedAt.toISOString(); this.report.meta.durationMs = endedAt.getTime() - this.startedAt.getTime(); this.updateStatus(); saveToFileSync(this.filePath, JSON.stringify(this.report, null, 2)); if (this.maxFiles !== void 0) { this.pruneOldFiles(); } } updateStatus() { let succeededTotal = 0; let failedTotal = 0; for (const item of Object.values(this.report.summary)) { succeededTotal += item.succeeded; failedTotal += item.failed; } if (failedTotal === 0) { this.report.status = REPORT_STATUS.success; } else if (succeededTotal > 0) { this.report.status = REPORT_STATUS.partialSuccess; } else { this.report.status = REPORT_STATUS.failure; } } pruneOldFiles() { if (this.maxFiles === void 0) { return; } const dir = dirname(this.filePath); const ext = extname(this.filePath); Reporter.pruneReportFiles(dir, this.maxFiles, ext); } static pruneReportFiles(directory, keep, extension = ".json") { if (!existsSync(directory)) { return 0; } const files = readdirSync(directory).filter((file) => extname(file) === extension).sort(); const filesToDelete = files.length - keep; if (filesToDelete <= 0) { return 0; } for (const file of files.slice(0, filesToDelete)) { unlinkSync(join(directory, file)); } return filesToDelete; } static listReportFiles(directory, extension = ".json") { if (!existsSync(directory)) { return []; } return readdirSync(directory).filter((file) => extname(file) === extension).map((f) => relative(process.cwd(), join(directory, f))).sort(); } } let reporterInstance = null; function getReporter(options) { if (!reporterInstance) { reporterInstance = new Reporter(options); } return reporterInstance; } class FileTransport { filePath; level; maxFiles; hasPruned = false; constructor(options) { this.filePath = options?.filePath ?? `./${Date.now()}.jsonl`; this.level = options?.level ?? "info"; this.maxFiles = options?.maxFiles; } log(record) { if (!this.shouldLog(record.level)) { return; } const line = this.format(record); appendToFileSync(this.filePath, line); if (!this.hasPruned && this.maxFiles !== void 0) { this.hasPruned = true; this.pruneOldFiles(); } } pruneOldFiles() { if (this.maxFiles === void 0) { return; } const dir = dirname(this.filePath); const ext = extname(this.filePath); FileTransport.pruneLogFiles(dir, this.maxFiles, ext); } static pruneLogFiles(directory, keep, extension = ".jsonl") { if (!existsSync(directory)) { return 0; } const files = readdirSync(directory).filter((file) => extname(file) === extension).sort(); const filesToDelete = files.length - keep; if (filesToDelete <= 0) { return 0; } for (const file of files.slice(0, filesToDelete)) { unlinkSync(join(directory, file)); } return filesToDelete; } static listLogFiles(directory, extension = ".jsonl") { if (!existsSync(directory)) { return []; } const files = readdirSync(directory).filter((file) => extname(file) === extension).sort(); return files.map((f) => join(directory, f).replace(process.cwd(), ".")); } levelRank(level) { switch (level) { case "error": return 0; case "warn": return 1; case "info": return 2; case "debug": return 3; default: return 3; } } shouldLog(level) { return this.levelRank(level) <= this.levelRank(this.level); } format(record) { const timestamp = (record.timestamp ?? /* @__PURE__ */ new Date()).toISOString(); const level = record.level.toUpperCase(); const message = record.message.replaceAll("\n", "\\n"); const contextNormalized = record.context && this.formatContext(record.context); return JSON.stringify({ timestamp, level, message, context: contextNormalized }); } formatContext(context) { const contextNormalized = {}; for (const [key, value] of Object.entries(context)) { if (value instanceof APIError) { contextNormalized[key] = { name: value.name, message: value.message, httpCode: value.code, httpStatusText: value.error?.response?.statusText, stack: value.stack }; continue; } if (value instanceof Error) { contextNormalized[key] = { name: value.name, message: value.message, stack: value.stack }; continue; } contextNormalized[key] = value; } return contextNormalized; } } class ConsoleTransport { level; constructor(options) { this.level = options?.level ?? "info"; } log(record) { if (!this.shouldLog(record.level)) { return; } const line = this.format(record); switch (record.level) { case "error": (console.error ?? console.log).call(console, line); break; case "warn": (console.warn ?? console.log).call(console, line); break; case "info": (console.info ?? console.log).call(console, line); break; case "debug": (console.debug ?? console.log).call(console, line); break; } } levelRank(level) { switch (level) { case "error": return 0; case "warn": return 1; case "info": return 2; case "debug": return 3; default: return 3; } } shouldLog(level) { return this.levelRank(level) <= this.levelRank(this.level); } format(record) { const timestamp = this.formatTimestamp(record.timestamp ?? /* @__PURE__ */ new Date()); const level = record.level.toUpperCase().padEnd(5, " "); const message = record.message.replaceAll("\n", "\\n"); const context = record.context ? this.formatContext(record.context) : ""; return `[${timestamp}] ${level} ${message}${context}`; } formatTimestamp(date) { const pad2 = (n) => String(n).padStart(2, "0"); const pad3 = (n) => String(n).padStart(3, "0"); const h = pad2(date.getHours()); const m = pad2(date.getMinutes()); const s = pad2(date.getSeconds()); const ms = pad3(date.getMilliseconds()); return `${h}:${m}:${s}.${ms}`; } formatContext(context) { const entries = Object.entries(context); if (entries.length === 0) { return ""; } const parts = entries.map(([k, v]) => `${k}: ${this.stringify(v)}`); return ` (${parts.join(", ")})`; } stringify(value) { try { if (value instanceof APIError) { return JSON.stringify({ name: value.name, message: value.message, httpCode: value.code, httpStatusText: value.error?.response?.statusText, stack: value.stack }); } if (value instanceof Error) { return JSON.stringify({ name: value.name, message: value.message, stack: value.stack }); } if (value && typeof value === "object") { return JSON.stringify(value); } return String(value); } catch { return "[unserializable]"; } } } const getCredentials = async (filePath = join(getStoryblokGlobalPath(), "credentials.json")) => { try { await access(filePath); const content = await readFile(filePath); const parsedContent = JSON.parse(content); if (Object.keys(parsedContent).length === 0) { return null; } return parsedContent; } catch (error) { if (error.code === "ENOENT") { await saveToFile(filePath, JSON.stringify({}, null, 2), { mode: 384 }); return null; } handleFileSystemError("read", error); return null; } }; const addCredentials = async ({ filePath = join(g