storyblok
Version:
Storyblok CLI
1,719 lines (1,692 loc) • 326 kB
JavaScript
#!/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