@getbrevo/cli 2.2.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/index.js CHANGED
@@ -3,7 +3,8 @@
3
3
  `),strippedUrlSuffix=void 0)}function isLocalHttpAllowed(parsed){return parsed.protocol==="http:"&&(parsed.hostname==="localhost"||parsed.hostname==="127.0.0.1"||parsed.hostname==="::1")}function resolveApiBase(){let raw=process.env.BREVO_API_URL||"https://api.brevo.com",parsed;try{parsed=new URL(raw)}catch{throw new CliError(`Invalid BREVO_API_URL: "${raw}" is not a valid URL.`)}if(parsed.protocol==="https:"||isLocalHttpAllowed(parsed))return stripPath(parsed);throw new CliError(`BREVO_API_URL must use HTTPS. Got: ${raw}
4
4
  HTTP is only allowed for localhost/127.0.0.1.`)}var API_BASE=resolveApiBase();function resolveOauthProxyUrl(){let raw=process.env.BREVO_OAUTH_PROXY_URL||"https://oauth-cli.brevo.com",parsed;try{parsed=new URL(raw)}catch{throw new CliError(`Invalid BREVO_OAUTH_PROXY_URL: "${raw}" is not a valid URL.`)}if(parsed.protocol!=="https:"&&!isLocalHttpAllowed(parsed))throw new CliError(`BREVO_OAUTH_PROXY_URL must use HTTPS. Got: ${raw}
5
5
  HTTP is only allowed for localhost/127.0.0.1.`);return parsed.origin}var OAUTH_PROXY_URL=resolveOauthProxyUrl();function resolveAppStoreUrl(){let raw=process.env.BREVO_APP_STORE_URL||"https://app-store-bo-be.brevo.com",parsed;try{parsed=new URL(raw)}catch{throw new CliError(`Invalid BREVO_APP_STORE_URL: "${raw}" is not a valid URL.`)}if(parsed.protocol!=="https:"&&!isLocalHttpAllowed(parsed))throw new CliError(`BREVO_APP_STORE_URL must use HTTPS. Got: ${raw}
6
- HTTP is only allowed for localhost/127.0.0.1.`);return parsed.origin}var APP_STORE_BASE=resolveAppStoreUrl(),USER_AGENT_HEADER="User-Agent",CLI_AUTH_METHODS={API_KEY:"api_key",OAUTH:"oauth"},coreEndpoints={ACCOUNT:"/v3/account/info",CORPORATE_SUB_ACCOUNTS:"/v3/corporate/subAccount",APP_STORE_APPS:"/v3/app-store/apps",APP_STORE_APP:appId=>`/v3/app-store/apps/${encodeURIComponent(appId)}`,CLI_INFO:"/cli/info",APP_STORE_APP_UPLOAD:appId=>`/v3/app-store/apps/${encodeURIComponent(appId)}/upload`,APP_STORE_APP_INSTALLS:appId=>`/v3/app-store/apps/${encodeURIComponent(appId)}/installs`,APP_STORE_SURFACE_POINTS:"/v3/app-store/surface-points",APP_STORE_SURFACE_POINT_LOCATIONS:"/v3/app-store/surface-points/locations",OAUTH_AUTHORIZE:"/oauth/authorize",OAUTH_TOKEN:"/oauth/token"},ENDPOINTS={...coreEndpoints},EXAMPLE_APP_ID="3f8c1a2e-5b47-4d9c-8e10-6a2b7d4f0c93",coreCli={LOGIN:"brevo login",INIT:"brevo app init",HELP:"brevo --help",APP_CREATE:"brevo app create",APP_LIST:"brevo app list",APP_SCAFFOLD:"brevo app scaffold",APP_SCAFFOLD_APP_ID:appId=>appId?`brevo app scaffold --app-id ${appId}`:"brevo app scaffold --app-id <id>",APP_CREDENTIALS:appId=>appId?`brevo app credentials --app-id ${appId}`:"brevo app credentials --app-id <id>",APP_DELETE_APP_ID:appId=>appId?`brevo app delete --app-id ${appId}`:"brevo app delete --app-id <id>",APP_CREDENTIALS_REVEAL:appId=>appId?`brevo app credentials --reveal-secret --app-id ${appId}`:"brevo app credentials --reveal-secret",APP_UPLOAD:"brevo app upload",APP_INSTALL:accountId=>accountId?`brevo app install ${accountId}`:"brevo app install",APP_UNINSTALL:accountId=>accountId?`brevo app uninstall ${accountId}`:"brevo app uninstall",APP_INSTALL_APP_ID:appId=>appId?`brevo app install --app-id ${appId}`:"brevo app install --app-id <id>",APP_UNINSTALL_APP_ID:appId=>appId?`brevo app uninstall --app-id ${appId}`:"brevo app uninstall --app-id <id>",APP_DELETE:"brevo app delete",APP_START:feature=>feature?`brevo app start ${feature}`:"brevo app start <feature>",APP_SCOPES:"brevo app available-scopes",SKILL_INSTALL:"brevo skill:cli install",SKILL_UNINSTALL:"brevo skill:cli uninstall"},CLI={...coreCli};var DEFAULT_PORT=3009,DEFAULT_REDIRECT_URI=`http://localhost:${DEFAULT_PORT}/auth/callback`,PLACEHOLDER_CLIENT_ID="YOUR_CLIENT_ID",OAUTH_BASE="https://oauth.brevo.com",OAUTH_REALM="partner",OAUTH_SCOPES_URL=`${OAUTH_BASE}/realms/${OAUTH_REALM}/scopes`,LEGACY_ALL_SCOPE="all",DEFAULT_SCOPES=["contacts:read","contacts:write","crm:read","crm:write"],EXTENSION_TYPE_ACTION_LINK="actionLink",EXTENSION_TYPE_IFRAME="iframeExtension";var DEFAULT_LINK_TARGET="_blank",UPLOADABLE_LINK_TARGETS=[DEFAULT_LINK_TARGET],BREVO_DASHBOARD_API_KEYS_URL="https://app.brevo.com/settings/keys/api",BREVO_API_KEY_DOCS_URL="https://developers.brevo.com/docs/api-key-authentication";var BREVO_CLI_REFERENCE_URL="https://developers.brevo.com/docs/cli-reference",BREVO_OAUTH_SCOPES_DOCS_URL="https://developers.brevo.com/docs/oauth-scopes#scope-catalog";var APP_NAME_MAX_LENGTH=48,APP_NAME_REGEX=/^[a-zA-Z0-9 ._\-\u00C0-\u024F]+$/;function validateAppName(name){let trimmed=name.trim();return trimmed.length===0?"App name cannot be empty.":trimmed.length>APP_NAME_MAX_LENGTH?`App name must be at most ${APP_NAME_MAX_LENGTH} characters (got ${trimmed.length}).`:APP_NAME_REGEX.test(trimmed)?!0:"App name can only contain letters, numbers, spaces, hyphens, dots, underscores, and accented characters."}function validateYesNo(input){let val=String(input).toLowerCase().trim();return val==="y"||val==="yes"||val==="n"||val==="no"||val===""?!0:"Please enter y or n"}function validateEnum(value,allowed,flagName){if(value&&!allowed.includes(value))throw new CliError(`Invalid ${flagName} "${value}". Must be one of: ${allowed.join(", ")}.`)}function validateUrl(value,fieldName){if(value){if(/[\s,]/.test(value))throw new CliError(`Invalid ${fieldName}: "${value}" contains whitespace or a comma. Pass each URL with a separate --redirect-uri flag.`);try{let parsed=new URL(value);if(parsed.protocol!=="http:"&&parsed.protocol!=="https:")throw new Error("bad protocol")}catch{throw new CliError(`Invalid ${fieldName}: "${value}" is not a valid HTTP/HTTPS URL.`)}}}function collectUrls(value,previous=[]){return validateUrl(value,"redirect URL"),[...previous,value]}var SCOPE_TOKEN_REGEX=/^[A-Za-z0-9][A-Za-z0-9:_.-]*$/,SCOPE_SPLIT_REGEX=/[\s,]+/;function splitScopes(input){if(input==null)return[];let values=Array.isArray(input)?input:[input],out=[],seen=new Set;for(let v of values)if(typeof v=="string")for(let token of v.split(SCOPE_SPLIT_REGEX))token&&(seen.has(token)||(seen.add(token),out.push(token)));return out}function validateScopes(scopes){for(let scope of scopes)if(!SCOPE_TOKEN_REGEX.test(scope))throw new CliError(`Invalid scope: "${scope}" \u2014 scopes can only contain letters, numbers, ':', '_', '.', '-'.`)}function containsLegacyAllScope(scopes){return scopes?.includes(LEGACY_ALL_SCOPE)??!1}function isSafeUiAppUrl(parsed){return parsed.protocol==="https:"?!0:parsed.protocol==="http:"&&(parsed.hostname==="localhost"||parsed.hostname==="127.0.0.1"||parsed.hostname==="::1")}function validateUiAppUrl(value){let trimmed=value.trim();if(!trimmed)return"URL cannot be empty.";let parsed;try{parsed=new URL(trimmed)}catch{return`Invalid URL: "${trimmed}" is not a valid URL.`}return isSafeUiAppUrl(parsed)?!0:`Invalid URL: "${trimmed}" must use https:// (http:// is allowed only for localhost).`}var UI_APP_LABEL_MAX_LENGTH=48,UI_APP_MORE_INFO_MAX_LENGTH=255;function validateUiAppLabel(value){let trimmed=value.trim();return trimmed?trimmed.length>UI_APP_LABEL_MAX_LENGTH?`Label must be at most ${UI_APP_LABEL_MAX_LENGTH} characters (got ${trimmed.length}).`:!0:"Label cannot be empty."}function validateUiAppMoreInfo(value){let trimmed=value.trim();return trimmed.length>UI_APP_MORE_INFO_MAX_LENGTH?`More info must be at most ${UI_APP_MORE_INFO_MAX_LENGTH} characters (got ${trimmed.length}).`:!0}function validateSurfacePoint(point){return String(point??"").trim()?!0:"Surface point cannot be empty."}var AUTHORABLE_EXTENSION_TYPES=[EXTENSION_TYPE_ACTION_LINK,EXTENSION_TYPE_IFRAME];function validateUiAppContext(fields){let seen=new Set;for(let field of fields){let trimmed=String(field??"").trim();if(!trimmed)return"Context field names cannot be empty.";if(seen.has(trimmed))return`Duplicate context field "${trimmed}".`;seen.add(trimmed)}return!0}function asText(value){return typeof value=="string"?value:typeof value=="number"||typeof value=="boolean"||typeof value=="bigint"?String(value):""}function isPresentField(value){return value===void 0?!1:typeof value!="string"||value.trim()!==""}function validateUiApp(uiApp){if(!uiApp||typeof uiApp!="object")throw new CliError('app-config.json has an invalid "ui_app" block \u2014 expected an object. Fix the file, or recreate the app with `brevo app create` and choose "UI app".');let block=uiApp,extensionType=asText(block.extension_type);if(!AUTHORABLE_EXTENSION_TYPES.includes(extensionType))throw new CliError(`Unsupported ui_app.extension_type "${extensionType}". Must be one of: ${AUTHORABLE_EXTENSION_TYPES.join(", ")}.`);rejectPreBex290Fields(block),rejectRootCtaFields(block),validateSurfacePointList(block.surface_point_list,extensionType)}function rejectPreBex290Fields(block){if(block.heading!==void 0)throw new CliError("ui_app.heading was renamed to ui_app.label (it is the menu entry's text and the card's CTA). Rename the field in app-config.json.");if(block.subheading!==void 0)throw new CliError("ui_app.subheading was renamed to ui_app.more_info (it is the menu entry's second line and the card's description). Rename the field in app-config.json.");if(block.context!==void 0)throw new CliError('ui_app.context is no longer a top-level field \u2014 record context is now per placement. Move each field list into the matching `surface_point_list` entry, e.g. [{ "surface_point_name": "contactDetails.header.menu", "context": ["recordId"] }].')}function rejectRootCtaFields(block){let moved=[["label",'"label": "Open in Acme"'],["more_info",'"more_info": "See this record in Acme"'],["redirect_link",'"redirect_link": "https://example.com/open"'],["modal_iframe_url",'"modal_iframe_url": "https://example.com/embed"']];for(let[key,hint]of moved)if(block[key]!==void 0)throw new CliError(`ui_app.${key} moved into each surface_point_list entry (each placement carries its own) \u2014 e.g. [{ "surface_point_name": "contactDetails.header.menu", ${hint} }]. Move it in app-config.json.`);if(block.link_target!==void 0)throw new CliError("ui_app.link_target moved onto each surface_point_list entry (BEX-426), and is not authored in app-config.json at all \u2014 `brevo app upload` injects it per placement. Remove it from the file.")}function validateSurfacePointList(entries,extensionType){if(!Array.isArray(entries)||entries.length===0)throw new CliError('ui_app.surface_point_list must list at least one placement (e.g. [{ "surface_point_name": "contactDetails.header.menu", "context": ["recordId"] }]). An empty list makes the platform fall back to its default widget slots, which is unlikely to be where you want the app.');let names=[];for(let entry of entries){if(!entry||typeof entry!="object"||Array.isArray(entry))throw new CliError('ui_app.surface_point_list entries must be objects, e.g. { "surface_point_name": "contactDetails.header.menu", "context": ["recordId"] }. A bare string is the pre-BEX-290 shape.');let row=entry;if(row.surface_point_name===void 0)throw new CliError('ui_app.surface_point_list entries must carry "surface_point_name" (e.g. { "surface_point_name": "contactDetails.header.menu" }).'+(row.surface_point!==void 0?' "surface_point" is not a field \u2014 rename it to "surface_point_name".':""));let check=validateSurfacePoint(asText(row.surface_point_name));if(check!==!0)throw new CliError(`ui_app.surface_point_list: ${check}`);let name=asText(row.surface_point_name).trim();names.push(name),validateEntryContext(row,name),validateEntrySize(row,name),validateEntryCtaFields(row,name,extensionType)}if(new Set(names).size!==names.length)throw new CliError("ui_app.surface_point_list contains duplicate extension points.")}var SIZE_AXIS_PATTERN=/^([1-9]\d*)(px|%)$/;function validateSurfacePointSize(size){if(!size||typeof size!="object"||Array.isArray(size))return'must be an object, e.g. { "width": "280px", "height": "160px" }.';let{width,height}=size;for(let[axis,value]of Object.entries({width,height})){if(value===void 0)continue;let match=typeof value=="string"?SIZE_AXIS_PATTERN.exec(value):null;if(!match)return`${axis} must be a positive integer with a px or % unit, e.g. "280px" or "50%".`;if(match[2]==="%"&&Number(match[1])>100)return`${axis} "${match[0]}" is out of range \u2014 a % axis must be between 1% and 100%.`}return!0}function validateEntryContext(row,name){if(row.context===void 0)return;if(!Array.isArray(row.context))throw new CliError(`ui_app.surface_point_list["${name}"].context must be an array of field names, e.g. ["recordId"].`);let contextCheck=validateUiAppContext(row.context.map(asText));if(contextCheck!==!0)throw new CliError(`ui_app.surface_point_list["${name}"].context: ${contextCheck}`)}function validateEntrySize(row,name){if(row.size===void 0)return;let sizeCheck=validateSurfacePointSize(row.size);if(sizeCheck!==!0)throw new CliError(`ui_app.surface_point_list["${name}"].size: ${sizeCheck}`)}function validateEntryCtaFields(row,name,extensionType){let at=field=>`ui_app.surface_point_list["${name}"].${field}`,labelCheck=validateUiAppLabel(asText(row.label));if(labelCheck!==!0)throw new CliError(`${at("label")}: ${labelCheck}`);let moreInfoCheck=validateUiAppMoreInfo(asText(row.more_info));if(moreInfoCheck!==!0)throw new CliError(`${at("more_info")}: ${moreInfoCheck}`);if(extensionType===EXTENSION_TYPE_IFRAME){let urlCheck2=validateUiAppUrl(asText(row.modal_iframe_url));if(urlCheck2!==!0)throw new CliError(`${at("modal_iframe_url")}: ${urlCheck2}`);if(isPresentField(row.link_target))throw new CliError(`${at("link_target")} has no effect on "${EXTENSION_TYPE_IFRAME}" extensions, which embed their URL in a modal rather than navigating to it. Remove it.`);if(isPresentField(row.redirect_link))throw new CliError(`${at("redirect_link")} cannot be combined with "${EXTENSION_TYPE_IFRAME}": a menu entry would follow the redirect instead of opening the modal, while a card would open the modal. Remove it, or use "${EXTENSION_TYPE_ACTION_LINK}" instead.`);return}let urlCheck=validateUiAppUrl(asText(row.redirect_link));if(urlCheck!==!0)throw new CliError(`${at("redirect_link")}: ${urlCheck}`);if(row.link_target!==void 0&&!UPLOADABLE_LINK_TARGETS.includes(asText(row.link_target)))throw new CliError(`Invalid ${at("link_target")} "${asText(row.link_target)}". Must be one of: ${UPLOADABLE_LINK_TARGETS.join(", ")}.`);if(isPresentField(row.modal_iframe_url))throw new CliError(`${at("modal_iframe_url")} is only used by "${EXTENSION_TYPE_IFRAME}" extensions and is ignored for "${EXTENSION_TYPE_ACTION_LINK}". Remove it, or use redirect_link instead.`)}function parseAccountId(value){let trimmed=String(value??"").trim();if(!trimmed)throw new CliError("Invalid account ID: value cannot be empty.");if(!/^\d+$/.test(trimmed))throw new CliError(`Invalid account ID: "${trimmed}" is not a numeric Brevo account ID.`);return trimmed}function parsePositiveInt(value,flagName){let n=Number.parseInt(value,10);if(!Number.isFinite(n)||n<=0)throw new CliError(`Invalid ${flagName}: "${value}" is not a positive integer.`);return n}function parseAppId(value){let trimmed=value.trim();if(trimmed.length===0)throw new CliError("Invalid --app-id: value cannot be empty.");return trimmed}function isUiAppConfigShape(config){return!!config?.ui_app}function isUiAppRecordShape(app){return app?app.ui_app?!0:!app.client_id&&!app.redirect_uris?.length:!1}function getConfigDir(){return process.env.BREVO_CONFIG_HOME||path.join(os.homedir(),".brevo")}function getCredentialsPath(){return path.join(getConfigDir(),"credentials.json")}function ensureDir(){fs.mkdirSync(getConfigDir(),{recursive:!0,mode:448})}var APP_NAME_CACHE_TTL_MS=600*1e3;function sanitizeAppNames(value){if(!value||typeof value!="object")return;let out={};for(let[key,raw]of Object.entries(value))if(typeof raw=="string"&&raw.trim())out[key]={name:raw,savedAt:0};else if(raw&&typeof raw=="object"){let entry=raw;typeof entry.name=="string"&&entry.name.trim()&&typeof entry.savedAt=="number"&&Number.isFinite(entry.savedAt)&&(out[key]={name:entry.name,savedAt:entry.savedAt})}return Object.keys(out).length>0?out:void 0}function sanitizeApps(apps){let sanitized={};for(let[key,value]of Object.entries(apps))if(value&&typeof value=="object"){let entry=value;typeof entry.clientId=="string"&&typeof entry.clientSecret=="string"&&(sanitized[key]={clientId:entry.clientId,clientSecret:entry.clientSecret})}return sanitized}function readCredentials(){try{let parsed=JSON.parse(fs.readFileSync(getCredentialsPath(),"utf-8"));if(parsed.profiles){let profileName=typeof parsed.activeProfile=="string"&&parsed.activeProfile||"default",firstKey=Object.keys(parsed.profiles)[0],profile=parsed.profiles[profileName]??(firstKey?parsed.profiles[firstKey]:void 0),migrated={auth:typeof profile?.apiKey=="string"&&profile.apiKey?{kind:"api-key",apiKey:profile.apiKey}:void 0,accountEmail:profile?.accountEmail,organizationId:profile?.organizationId,userId:profile?.userId,apps:sanitizeApps(parsed.apps??{})};try{writeCredentials(migrated)}catch{}return migrated}if(!parsed.auth&&typeof parsed.apiKey=="string"&&parsed.apiKey){let migrated={auth:{kind:"api-key",apiKey:parsed.apiKey},accountEmail:parsed.accountEmail,organizationId:parsed.organizationId,userId:parsed.userId,apps:sanitizeApps(parsed.apps??{})};try{writeCredentials(migrated)}catch{}return migrated}return{auth:sanitizeAuth(parsed.auth),accountEmail:parsed.accountEmail,organizationId:parsed.organizationId,userId:parsed.userId,apps:sanitizeApps(parsed.apps??{}),appNames:sanitizeAppNames(parsed.appNames)}}catch{return{apps:{}}}}function sanitizeAuth(raw){if(!raw||typeof raw!="object")return;let v=raw;if(v.kind==="api-key"&&typeof v.apiKey=="string"&&v.apiKey)return{kind:"api-key",apiKey:v.apiKey};if(v.kind==="oauth"&&typeof v.accessToken=="string"&&v.accessToken&&typeof v.refreshToken=="string"&&v.refreshToken&&typeof v.tokenType=="string"&&v.tokenType&&typeof v.expiresAt=="number"&&Number.isFinite(v.expiresAt))return{kind:"oauth",accessToken:v.accessToken,refreshToken:v.refreshToken,expiresAt:v.expiresAt,tokenType:v.tokenType,scope:typeof v.scope=="string"?v.scope:void 0}}function writeCredentials(creds){ensureDir();let filePath=getCredentialsPath();fs.writeFileSync(filePath,JSON.stringify(creds,null,2),{mode:384});try{fs.chmodSync(filePath,384)}catch{}}function getAuthCred(){return process.env.BREVO_API_KEY?{kind:"api-key",apiKey:process.env.BREVO_API_KEY}:readCredentials().auth}function getEmail(){return readCredentials().accountEmail}function getOrganizationId(){return readCredentials().organizationId}function getUserId(){return readCredentials().userId}function saveCredentials(apiKey,account){let creds=readCredentials();creds.auth={kind:"api-key",apiKey},creds.accountEmail=account.email,creds.organizationId=account.organizationId,creds.userId=account.userId,writeCredentials(creds)}function saveOauthCredentials(tokens,account){let creds=readCredentials();creds.auth={kind:"oauth",accessToken:tokens.accessToken,refreshToken:tokens.refreshToken,expiresAt:Date.now()+tokens.expiresIn*1e3,tokenType:tokens.tokenType,scope:tokens.scope},account?(creds.accountEmail=account.email,creds.organizationId=account.organizationId,creds.userId=account.userId):(delete creds.accountEmail,delete creds.organizationId,delete creds.userId),writeCredentials(creds)}function updateOauthTokens(tokens){let creds=readCredentials();creds.auth={kind:"oauth",accessToken:tokens.accessToken,refreshToken:tokens.refreshToken,expiresAt:Date.now()+tokens.expiresIn*1e3,tokenType:tokens.tokenType,scope:tokens.scope},writeCredentials(creds)}function clearCredentials(){let creds=readCredentials();delete creds.auth,delete creds.accountEmail,delete creds.organizationId,delete creds.userId,writeCredentials(creds)}function deleteCredentialsFile(){try{fs.unlinkSync(getCredentialsPath())}catch(error){if(typeof error=="object"&&error!==null&&"code"in error&&error.code==="ENOENT")return;throw error}}function hasAppCredentials(){return Object.keys(readCredentials().apps).length>0}function countAppCredentials(){return Object.keys(readCredentials().apps).length}function isAuthenticated(){return!!getAuthCred()}function saveAppCredentials(appId,cred){let creds=readCredentials();creds.apps[appId]=cred,writeCredentials(creds)}function clearAppsCache(){let creds=readCredentials();creds.apps={},delete creds.appNames,writeCredentials(creds)}function getAppCredentials(appId){return readCredentials().apps[appId]}function deleteAppCredentials(appId){if(!appId)return;let creds=readCredentials();appId in creds.apps&&(delete creds.apps[appId],writeCredentials(creds))}function saveAppName(appId,name){if(!appId||!name)return;let creds=readCredentials();creds.appNames={...creds.appNames,[appId]:{name,savedAt:Date.now()}},writeCredentials(creds)}function getAppNames(){let creds=readCredentials(),cache=creds.appNames??{},now=Date.now(),fresh={},result={},pruned=!1;for(let[id,entry]of Object.entries(cache))now-entry.savedAt<APP_NAME_CACHE_TTL_MS?(fresh[id]=entry,result[id]=entry.name):pruned=!0;if(pruned){creds.appNames=Object.keys(fresh).length>0?fresh:void 0;try{writeCredentials(creds)}catch{}}return result}function deleteAppName(appId){if(!appId)return;let creds=readCredentials();if(!creds.appNames||!(appId in creds.appNames))return;let{[appId]:_removed,...rest}=creds.appNames;creds.appNames=Object.keys(rest).length>0?rest:void 0,writeCredentials(creds)}var PROJECT_CONFIG_FILE="app-config.json";function readProjectConfig(){return readProjectConfigAt(process.cwd())}function readNormalizedAppId(raw){let rawAppId=raw.appId;if(typeof rawAppId=="string")return rawAppId.trim()||void 0;if(typeof rawAppId=="number"&&Number.isFinite(rawAppId))return String(rawAppId)}function buildAuthOverride(rawAuth){if(!rawAuth||typeof rawAuth!="object")return;let auth=rawAuth,override,scopes=auth.scopes;if((Array.isArray(scopes)||typeof scopes=="string")&&(override={...auth,scopes:splitScopes(scopes)}),"redirectUrls"in auth){override=override??{...auth};let legacyRedirects=auth.redirectUrls;!Array.isArray(override.redirectUris)&&Array.isArray(legacyRedirects)&&(override.redirectUris=legacyRedirects),delete override.redirectUrls}return override&&"type"in override?delete override.type:"type"in auth&&(override={...auth},delete override.type),override}function readDistributionType(rawRecord,rawAuth){let newDistributionType=rawRecord.distribution_type;if(typeof newDistributionType=="string"&&newDistributionType.trim())return newDistributionType.trim();let legacyAuthType=rawAuth&&typeof rawAuth=="object"?rawAuth.type:void 0;if(typeof legacyAuthType=="string"&&legacyAuthType.trim()&&legacyAuthType!=="none")return legacyAuthType.trim();let legacyDistribution=rawRecord.distribution;return typeof legacyDistribution=="string"&&legacyDistribution.trim()?legacyDistribution.trim():"private"}function readProjectConfigAt(dir){try{let raw=JSON.parse(fs.readFileSync(path.resolve(dir,PROJECT_CONFIG_FILE),"utf-8"));if(!raw||typeof raw!="object")return null;let rawRecord=raw,appId=readNormalizedAppId(rawRecord);if(!appId)return null;let rawAuth=rawRecord.auth,authOverride=buildAuthOverride(rawAuth),distributionType=readDistributionType(rawRecord,rawAuth),{distribution:_legacyDistribution,permittedUrls:_permittedUrls,support:_support,...rawWithoutLegacyDistribution}=rawRecord,rawUiApp=rawWithoutLegacyDistribution.ui_app;return"ui_app"in rawWithoutLegacyDistribution&&(!rawUiApp||typeof rawUiApp!="object")&&delete rawWithoutLegacyDistribution.ui_app,{...rawWithoutLegacyDistribution,appId,distribution_type:distributionType,...authOverride?{auth:authOverride}:{}}}catch{return null}}function hasLocalApp(){let cfg=readProjectConfig();return cfg?.appId!=null&&cfg.appId!==""}function findEnclosingProjectDir(){let dir=path.dirname(process.cwd());for(;;){if(readProjectConfigAt(dir))return dir;let parent=path.dirname(dir);if(parent===dir)return null;dir=parent}}function isUiAppConfig(config){return isUiAppConfigShape(config)}function writeProjectConfig(config){let configPath=path.resolve(process.cwd(),PROJECT_CONFIG_FILE);fs.writeFileSync(configPath,JSON.stringify(config,null,2)+`
6
+ HTTP is only allowed for localhost/127.0.0.1.`);return parsed.origin}var APP_STORE_BASE=resolveAppStoreUrl(),USER_AGENT_HEADER="User-Agent",CLI_AUTH_METHODS={API_KEY:"api_key",OAUTH:"oauth"},coreEndpoints={ACCOUNT:"/v3/account/info",CORPORATE_SUB_ACCOUNTS:"/v3/corporate/subAccount",APP_STORE_APPS:"/v3/app-store/apps",APP_STORE_APP:appId=>`/v3/app-store/apps/${encodeURIComponent(appId)}`,CLI_INFO:"/cli/info",APP_STORE_APP_UPLOAD:appId=>`/v3/app-store/apps/${encodeURIComponent(appId)}/upload`,APP_STORE_APP_INSTALLS:appId=>`/v3/app-store/apps/${encodeURIComponent(appId)}/installs`,APP_STORE_SURFACE_POINTS:"/v3/app-store/surface-points",APP_STORE_SURFACE_POINT_LOCATIONS:"/v3/app-store/surface-points/locations",OAUTH_AUTHORIZE:"/oauth/authorize",OAUTH_TOKEN:"/oauth/token"},ENDPOINTS={...coreEndpoints},EXAMPLE_APP_ID="3f8c1a2e-5b47-4d9c-8e10-6a2b7d4f0c93",coreCli={LOGIN:"brevo login",INIT:"brevo app init",HELP:"brevo --help",APP_CREATE:"brevo app create",APP_LIST:"brevo app list",APP_SCAFFOLD:"brevo app scaffold",APP_SCAFFOLD_APP_ID:appId=>appId?`brevo app scaffold --app-id ${appId}`:"brevo app scaffold --app-id <id>",APP_CREDENTIALS:appId=>appId?`brevo app credentials --app-id ${appId}`:"brevo app credentials --app-id <id>",APP_DELETE_APP_ID:appId=>appId?`brevo app delete --app-id ${appId}`:"brevo app delete --app-id <id>",APP_CREDENTIALS_REVEAL:appId=>appId?`brevo app credentials --reveal-secret --app-id ${appId}`:"brevo app credentials --reveal-secret",APP_UPLOAD:"brevo app upload",APP_INSTALL:accountId=>accountId?`brevo app install ${accountId}`:"brevo app install",APP_UNINSTALL:accountId=>accountId?`brevo app uninstall ${accountId}`:"brevo app uninstall",APP_INSTALL_APP_ID:appId=>appId?`brevo app install --app-id ${appId}`:"brevo app install --app-id <id>",APP_UNINSTALL_APP_ID:appId=>appId?`brevo app uninstall --app-id ${appId}`:"brevo app uninstall --app-id <id>",APP_DELETE:"brevo app delete",APP_START:feature=>feature?`brevo app start ${feature}`:"brevo app start <feature>",APP_SCOPES:"brevo app available-scopes",SKILL_INSTALL:"brevo skill:cli install",SKILL_UNINSTALL:"brevo skill:cli uninstall"},CLI={...coreCli};var DEFAULT_PORT=3009,DEFAULT_REDIRECT_URI=`http://localhost:${DEFAULT_PORT}/auth/callback`,PLACEHOLDER_CLIENT_ID="YOUR_CLIENT_ID";function resolveOauthBaseUrl(){let raw=process.env.BREVO_OAUTH_BASE_URL||"https://oauth.brevo.com",parsed;try{parsed=new URL(raw)}catch{throw new CliError(`Invalid BREVO_OAUTH_BASE_URL: "${raw}" is not a valid URL.`)}if(parsed.protocol!=="https:"&&!isLocalHttpAllowed(parsed))throw new CliError(`BREVO_OAUTH_BASE_URL must use HTTPS. Got: ${raw}
7
+ HTTP is only allowed for localhost/127.0.0.1.`);return parsed.origin}var OAUTH_BASE=resolveOauthBaseUrl(),OAUTH_REALM="partner",OAUTH_SCOPES_URL=`${OAUTH_BASE}/realms/${OAUTH_REALM}/scopes`,LEGACY_ALL_SCOPE="all",DEFAULT_SCOPES=["contacts:read","contacts:write","crm:read","crm:write"],EXTENSION_TYPE_ACTION_LINK="actionLink",EXTENSION_TYPE_IFRAME="iframeExtension";var DEFAULT_LINK_TARGET="_blank",UPLOADABLE_LINK_TARGETS=[DEFAULT_LINK_TARGET],BREVO_DASHBOARD_API_KEYS_URL="https://app.brevo.com/settings/keys/api",BREVO_API_KEY_DOCS_URL="https://developers.brevo.com/docs/api-key-authentication";var BREVO_CLI_REFERENCE_URL="https://developers.brevo.com/docs/cli-reference",BREVO_OAUTH_SCOPES_DOCS_URL="https://developers.brevo.com/docs/oauth-scopes#scope-catalog";var APP_NAME_MAX_LENGTH=48,APP_NAME_REGEX=/^[a-zA-Z0-9 ._\-\u00C0-\u024F]+$/;function validateAppName(name){let trimmed=name.trim();return trimmed.length===0?"App name cannot be empty.":trimmed.length>APP_NAME_MAX_LENGTH?`App name must be at most ${APP_NAME_MAX_LENGTH} characters (got ${trimmed.length}).`:APP_NAME_REGEX.test(trimmed)?!0:"App name can only contain letters, numbers, spaces, hyphens, dots, underscores, and accented characters."}function validateYesNo(input){let val=String(input).toLowerCase().trim();return val==="y"||val==="yes"||val==="n"||val==="no"||val===""?!0:"Please enter y or n"}function validateEnum(value,allowed,flagName){if(value&&!allowed.includes(value))throw new CliError(`Invalid ${flagName} "${value}". Must be one of: ${allowed.join(", ")}.`)}function validateUrl(value,fieldName){if(value){if(/[\s,]/.test(value))throw new CliError(`Invalid ${fieldName}: "${value}" contains whitespace or a comma. Pass each URL with a separate --redirect-uri flag.`);try{let parsed=new URL(value);if(parsed.protocol!=="http:"&&parsed.protocol!=="https:")throw new Error("bad protocol")}catch{throw new CliError(`Invalid ${fieldName}: "${value}" is not a valid HTTP/HTTPS URL.`)}}}function collectUrls(value,previous=[]){return validateUrl(value,"redirect URL"),[...previous,value]}var SCOPE_TOKEN_REGEX=/^[A-Za-z0-9][A-Za-z0-9:_.-]*$/,SCOPE_SPLIT_REGEX=/[\s,]+/;function splitScopes(input){if(input==null)return[];let values=Array.isArray(input)?input:[input],out=[],seen=new Set;for(let v of values)if(typeof v=="string")for(let token of v.split(SCOPE_SPLIT_REGEX))token&&(seen.has(token)||(seen.add(token),out.push(token)));return out}function validateScopes(scopes){for(let scope of scopes)if(!SCOPE_TOKEN_REGEX.test(scope))throw new CliError(`Invalid scope: "${scope}" \u2014 scopes can only contain letters, numbers, ':', '_', '.', '-'.`)}function containsLegacyAllScope(scopes){return scopes?.includes(LEGACY_ALL_SCOPE)??!1}function isSafeUiAppUrl(parsed){return parsed.protocol==="https:"?!0:parsed.protocol==="http:"&&(parsed.hostname==="localhost"||parsed.hostname==="127.0.0.1"||parsed.hostname==="::1")}function validateUiAppUrl(value){let trimmed=value.trim();if(!trimmed)return"URL cannot be empty.";let parsed;try{parsed=new URL(trimmed)}catch{return`Invalid URL: "${trimmed}" is not a valid URL.`}return isSafeUiAppUrl(parsed)?!0:`Invalid URL: "${trimmed}" must use https:// (http:// is allowed only for localhost).`}var UI_APP_LABEL_MAX_LENGTH=48,UI_APP_MORE_INFO_MAX_LENGTH=255;function validateUiAppLabel(value){let trimmed=value.trim();return trimmed?trimmed.length>UI_APP_LABEL_MAX_LENGTH?`Label must be at most ${UI_APP_LABEL_MAX_LENGTH} characters (got ${trimmed.length}).`:!0:"Label cannot be empty."}function validateUiAppMoreInfo(value){let trimmed=value.trim();return trimmed.length>UI_APP_MORE_INFO_MAX_LENGTH?`More info must be at most ${UI_APP_MORE_INFO_MAX_LENGTH} characters (got ${trimmed.length}).`:!0}function validateSurfacePoint(point){return String(point??"").trim()?!0:"Surface point cannot be empty."}var AUTHORABLE_EXTENSION_TYPES=[EXTENSION_TYPE_ACTION_LINK,EXTENSION_TYPE_IFRAME];function validateUiAppContext(fields){let seen=new Set;for(let field of fields){let trimmed=String(field??"").trim();if(!trimmed)return"Context field names cannot be empty.";if(seen.has(trimmed))return`Duplicate context field "${trimmed}".`;seen.add(trimmed)}return!0}function asText(value){return typeof value=="string"?value:typeof value=="number"||typeof value=="boolean"||typeof value=="bigint"?String(value):""}function isPresentField(value){return value===void 0?!1:typeof value!="string"||value.trim()!==""}function validateUiApp(uiApp){if(!uiApp||typeof uiApp!="object")throw new CliError('app-config.json has an invalid "ui_app" block \u2014 expected an object. Fix the file, or recreate the app with `brevo app create` and choose "UI app".');let block=uiApp,extensionType=asText(block.extension_type);if(!AUTHORABLE_EXTENSION_TYPES.includes(extensionType))throw new CliError(`Unsupported ui_app.extension_type "${extensionType}". Must be one of: ${AUTHORABLE_EXTENSION_TYPES.join(", ")}.`);rejectPreBex290Fields(block),rejectRootCtaFields(block),validateSurfacePointList(block.surface_point_list,extensionType)}function rejectPreBex290Fields(block){if(block.heading!==void 0)throw new CliError("ui_app.heading was renamed to ui_app.label (it is the menu entry's text and the card's CTA). Rename the field in app-config.json.");if(block.subheading!==void 0)throw new CliError("ui_app.subheading was renamed to ui_app.more_info (it is the menu entry's second line and the card's description). Rename the field in app-config.json.");if(block.context!==void 0)throw new CliError('ui_app.context is no longer a top-level field \u2014 record context is now per placement. Move each field list into the matching `surface_point_list` entry, e.g. [{ "surface_point_name": "contactDetails.header.menu", "context": ["recordId"] }].')}function rejectRootCtaFields(block){let moved=[["label",'"label": "Open in Acme"'],["more_info",'"more_info": "See this record in Acme"'],["redirect_link",'"redirect_link": "https://example.com/open"'],["modal_iframe_url",'"modal_iframe_url": "https://example.com/embed"']];for(let[key,hint]of moved)if(block[key]!==void 0)throw new CliError(`ui_app.${key} moved into each surface_point_list entry (each placement carries its own) \u2014 e.g. [{ "surface_point_name": "contactDetails.header.menu", ${hint} }]. Move it in app-config.json.`);if(block.link_target!==void 0)throw new CliError("ui_app.link_target moved onto each surface_point_list entry (BEX-426), and is not authored in app-config.json at all \u2014 `brevo app upload` injects it per placement. Remove it from the file.")}function validateSurfacePointList(entries,extensionType){if(!Array.isArray(entries)||entries.length===0)throw new CliError('ui_app.surface_point_list must list at least one placement (e.g. [{ "surface_point_name": "contactDetails.header.menu", "context": ["recordId"] }]). An empty list makes the platform fall back to its default widget slots, which is unlikely to be where you want the app.');let names=[];for(let entry of entries){if(!entry||typeof entry!="object"||Array.isArray(entry))throw new CliError('ui_app.surface_point_list entries must be objects, e.g. { "surface_point_name": "contactDetails.header.menu", "context": ["recordId"] }. A bare string is the pre-BEX-290 shape.');let row=entry;if(row.surface_point_name===void 0)throw new CliError('ui_app.surface_point_list entries must carry "surface_point_name" (e.g. { "surface_point_name": "contactDetails.header.menu" }).'+(row.surface_point!==void 0?' "surface_point" is not a field \u2014 rename it to "surface_point_name".':""));let check=validateSurfacePoint(asText(row.surface_point_name));if(check!==!0)throw new CliError(`ui_app.surface_point_list: ${check}`);let name=asText(row.surface_point_name).trim();names.push(name),validateEntryContext(row,name),validateEntrySize(row,name),validateEntryCtaFields(row,name,extensionType)}if(new Set(names).size!==names.length)throw new CliError("ui_app.surface_point_list contains duplicate extension points.")}var SIZE_AXIS_PATTERN=/^([1-9]\d*)(px|%)$/;function validateSurfacePointSize(size){if(!size||typeof size!="object"||Array.isArray(size))return'must be an object, e.g. { "width": "280px", "height": "160px" }.';let{width,height}=size;for(let[axis,value]of Object.entries({width,height})){if(value===void 0)continue;let match=typeof value=="string"?SIZE_AXIS_PATTERN.exec(value):null;if(!match)return`${axis} must be a positive integer with a px or % unit, e.g. "280px" or "50%".`;if(match[2]==="%"&&Number(match[1])>100)return`${axis} "${match[0]}" is out of range \u2014 a % axis must be between 1% and 100%.`}return!0}function validateEntryContext(row,name){if(row.context===void 0)return;if(!Array.isArray(row.context))throw new CliError(`ui_app.surface_point_list["${name}"].context must be an array of field names, e.g. ["recordId"].`);let contextCheck=validateUiAppContext(row.context.map(asText));if(contextCheck!==!0)throw new CliError(`ui_app.surface_point_list["${name}"].context: ${contextCheck}`)}function validateEntrySize(row,name){if(row.size===void 0)return;let sizeCheck=validateSurfacePointSize(row.size);if(sizeCheck!==!0)throw new CliError(`ui_app.surface_point_list["${name}"].size: ${sizeCheck}`)}function validateEntryCtaFields(row,name,extensionType){let at=field=>`ui_app.surface_point_list["${name}"].${field}`,labelCheck=validateUiAppLabel(asText(row.label));if(labelCheck!==!0)throw new CliError(`${at("label")}: ${labelCheck}`);let moreInfoCheck=validateUiAppMoreInfo(asText(row.more_info));if(moreInfoCheck!==!0)throw new CliError(`${at("more_info")}: ${moreInfoCheck}`);if(extensionType===EXTENSION_TYPE_IFRAME){let urlCheck2=validateUiAppUrl(asText(row.modal_iframe_url));if(urlCheck2!==!0)throw new CliError(`${at("modal_iframe_url")}: ${urlCheck2}`);if(isPresentField(row.link_target))throw new CliError(`${at("link_target")} has no effect on "${EXTENSION_TYPE_IFRAME}" extensions, which embed their URL in a modal rather than navigating to it. Remove it.`);if(isPresentField(row.redirect_link))throw new CliError(`${at("redirect_link")} cannot be combined with "${EXTENSION_TYPE_IFRAME}": a menu entry would follow the redirect instead of opening the modal, while a card would open the modal. Remove it, or use "${EXTENSION_TYPE_ACTION_LINK}" instead.`);return}let urlCheck=validateUiAppUrl(asText(row.redirect_link));if(urlCheck!==!0)throw new CliError(`${at("redirect_link")}: ${urlCheck}`);if(row.link_target!==void 0&&!UPLOADABLE_LINK_TARGETS.includes(asText(row.link_target)))throw new CliError(`Invalid ${at("link_target")} "${asText(row.link_target)}". Must be one of: ${UPLOADABLE_LINK_TARGETS.join(", ")}.`);if(isPresentField(row.modal_iframe_url))throw new CliError(`${at("modal_iframe_url")} is only used by "${EXTENSION_TYPE_IFRAME}" extensions and is ignored for "${EXTENSION_TYPE_ACTION_LINK}". Remove it, or use redirect_link instead.`)}function parseAccountId(value){let trimmed=String(value??"").trim();if(!trimmed)throw new CliError("Invalid account ID: value cannot be empty.");if(!/^\d+$/.test(trimmed))throw new CliError(`Invalid account ID: "${trimmed}" is not a numeric Brevo account ID.`);return trimmed}function parsePositiveInt(value,flagName){let n=Number.parseInt(value,10);if(!Number.isFinite(n)||n<=0)throw new CliError(`Invalid ${flagName}: "${value}" is not a positive integer.`);return n}function parseAppId(value){let trimmed=value.trim();if(trimmed.length===0)throw new CliError("Invalid --app-id: value cannot be empty.");return trimmed}function isUiAppConfigShape(config){return!!config?.ui_app}function isUiAppRecordShape(app){return app?app.ui_app?!0:!app.client_id&&!app.redirect_uris?.length:!1}function getConfigDir(){return process.env.BREVO_CONFIG_HOME||path.join(os.homedir(),".brevo")}function getCredentialsPath(){return path.join(getConfigDir(),"credentials.json")}function ensureDir(){fs.mkdirSync(getConfigDir(),{recursive:!0,mode:448})}var APP_NAME_CACHE_TTL_MS=600*1e3;function sanitizeAppNames(value){if(!value||typeof value!="object")return;let out={};for(let[key,raw]of Object.entries(value))if(typeof raw=="string"&&raw.trim())out[key]={name:raw,savedAt:0};else if(raw&&typeof raw=="object"){let entry=raw;typeof entry.name=="string"&&entry.name.trim()&&typeof entry.savedAt=="number"&&Number.isFinite(entry.savedAt)&&(out[key]={name:entry.name,savedAt:entry.savedAt})}return Object.keys(out).length>0?out:void 0}function sanitizeApps(apps){let sanitized={};for(let[key,value]of Object.entries(apps))if(value&&typeof value=="object"){let entry=value;typeof entry.clientId=="string"&&typeof entry.clientSecret=="string"&&(sanitized[key]={clientId:entry.clientId,clientSecret:entry.clientSecret})}return sanitized}function readCredentials(){try{let parsed=JSON.parse(fs.readFileSync(getCredentialsPath(),"utf-8"));if(parsed.profiles){let profileName=typeof parsed.activeProfile=="string"&&parsed.activeProfile||"default",firstKey=Object.keys(parsed.profiles)[0],profile=parsed.profiles[profileName]??(firstKey?parsed.profiles[firstKey]:void 0),migrated={auth:typeof profile?.apiKey=="string"&&profile.apiKey?{kind:"api-key",apiKey:profile.apiKey}:void 0,accountEmail:profile?.accountEmail,organizationId:profile?.organizationId,userId:profile?.userId,apps:sanitizeApps(parsed.apps??{})};try{writeCredentials(migrated)}catch{}return migrated}if(!parsed.auth&&typeof parsed.apiKey=="string"&&parsed.apiKey){let migrated={auth:{kind:"api-key",apiKey:parsed.apiKey},accountEmail:parsed.accountEmail,organizationId:parsed.organizationId,userId:parsed.userId,apps:sanitizeApps(parsed.apps??{})};try{writeCredentials(migrated)}catch{}return migrated}return{auth:sanitizeAuth(parsed.auth),accountEmail:parsed.accountEmail,organizationId:parsed.organizationId,userId:parsed.userId,apps:sanitizeApps(parsed.apps??{}),appNames:sanitizeAppNames(parsed.appNames)}}catch{return{apps:{}}}}function sanitizeAuth(raw){if(!raw||typeof raw!="object")return;let v=raw;if(v.kind==="api-key"&&typeof v.apiKey=="string"&&v.apiKey)return{kind:"api-key",apiKey:v.apiKey};if(v.kind==="oauth"&&typeof v.accessToken=="string"&&v.accessToken&&typeof v.refreshToken=="string"&&v.refreshToken&&typeof v.tokenType=="string"&&v.tokenType&&typeof v.expiresAt=="number"&&Number.isFinite(v.expiresAt))return{kind:"oauth",accessToken:v.accessToken,refreshToken:v.refreshToken,expiresAt:v.expiresAt,tokenType:v.tokenType,scope:typeof v.scope=="string"?v.scope:void 0}}function writeCredentials(creds){ensureDir();let filePath=getCredentialsPath();fs.writeFileSync(filePath,JSON.stringify(creds,null,2),{mode:384});try{fs.chmodSync(filePath,384)}catch{}}function getAuthCred(){return process.env.BREVO_API_KEY?{kind:"api-key",apiKey:process.env.BREVO_API_KEY}:readCredentials().auth}function getEmail(){return readCredentials().accountEmail}function getOrganizationId(){return readCredentials().organizationId}function getUserId(){return readCredentials().userId}function saveCredentials(apiKey,account){let creds=readCredentials();creds.auth={kind:"api-key",apiKey},creds.accountEmail=account.email,creds.organizationId=account.organizationId,creds.userId=account.userId,writeCredentials(creds)}function saveOauthCredentials(tokens,account){let creds=readCredentials();creds.auth={kind:"oauth",accessToken:tokens.accessToken,refreshToken:tokens.refreshToken,expiresAt:Date.now()+tokens.expiresIn*1e3,tokenType:tokens.tokenType,scope:tokens.scope},account?(creds.accountEmail=account.email,creds.organizationId=account.organizationId,creds.userId=account.userId):(delete creds.accountEmail,delete creds.organizationId,delete creds.userId),writeCredentials(creds)}function updateOauthTokens(tokens){let creds=readCredentials();creds.auth={kind:"oauth",accessToken:tokens.accessToken,refreshToken:tokens.refreshToken,expiresAt:Date.now()+tokens.expiresIn*1e3,tokenType:tokens.tokenType,scope:tokens.scope},writeCredentials(creds)}function clearCredentials(){let creds=readCredentials();delete creds.auth,delete creds.accountEmail,delete creds.organizationId,delete creds.userId,writeCredentials(creds)}function deleteCredentialsFile(){try{fs.unlinkSync(getCredentialsPath())}catch(error){if(typeof error=="object"&&error!==null&&"code"in error&&error.code==="ENOENT")return;throw error}}function hasAppCredentials(){return Object.keys(readCredentials().apps).length>0}function countAppCredentials(){return Object.keys(readCredentials().apps).length}function isAuthenticated(){return!!getAuthCred()}function saveAppCredentials(appId,cred){let creds=readCredentials();creds.apps[appId]=cred,writeCredentials(creds)}function clearAppsCache(){let creds=readCredentials();creds.apps={},delete creds.appNames,writeCredentials(creds)}function getAppCredentials(appId){return readCredentials().apps[appId]}function deleteAppCredentials(appId){if(!appId)return;let creds=readCredentials();appId in creds.apps&&(delete creds.apps[appId],writeCredentials(creds))}function saveAppName(appId,name){if(!appId||!name)return;let creds=readCredentials();creds.appNames={...creds.appNames,[appId]:{name,savedAt:Date.now()}},writeCredentials(creds)}function getAppNames(){let creds=readCredentials(),cache=creds.appNames??{},now=Date.now(),fresh={},result={},pruned=!1;for(let[id,entry]of Object.entries(cache))now-entry.savedAt<APP_NAME_CACHE_TTL_MS?(fresh[id]=entry,result[id]=entry.name):pruned=!0;if(pruned){creds.appNames=Object.keys(fresh).length>0?fresh:void 0;try{writeCredentials(creds)}catch{}}return result}function deleteAppName(appId){if(!appId)return;let creds=readCredentials();if(!creds.appNames||!(appId in creds.appNames))return;let{[appId]:_removed,...rest}=creds.appNames;creds.appNames=Object.keys(rest).length>0?rest:void 0,writeCredentials(creds)}var PROJECT_CONFIG_FILE="app-config.json";function readProjectConfig(){return readProjectConfigAt(process.cwd())}function readNormalizedAppId(raw){let rawAppId=raw.appId;if(typeof rawAppId=="string")return rawAppId.trim()||void 0;if(typeof rawAppId=="number"&&Number.isFinite(rawAppId))return String(rawAppId)}function buildAuthOverride(rawAuth){if(!rawAuth||typeof rawAuth!="object")return;let auth=rawAuth,override,scopes=auth.scopes;if((Array.isArray(scopes)||typeof scopes=="string")&&(override={...auth,scopes:splitScopes(scopes)}),"redirectUrls"in auth){override=override??{...auth};let legacyRedirects=auth.redirectUrls;!Array.isArray(override.redirectUris)&&Array.isArray(legacyRedirects)&&(override.redirectUris=legacyRedirects),delete override.redirectUrls}return override&&"type"in override?delete override.type:"type"in auth&&(override={...auth},delete override.type),override}function readDistributionType(rawRecord,rawAuth){let newDistributionType=rawRecord.distribution_type;if(typeof newDistributionType=="string"&&newDistributionType.trim())return newDistributionType.trim();let legacyAuthType=rawAuth&&typeof rawAuth=="object"?rawAuth.type:void 0;if(typeof legacyAuthType=="string"&&legacyAuthType.trim()&&legacyAuthType!=="none")return legacyAuthType.trim();let legacyDistribution=rawRecord.distribution;return typeof legacyDistribution=="string"&&legacyDistribution.trim()?legacyDistribution.trim():"private"}function readProjectConfigAt(dir){try{let raw=JSON.parse(fs.readFileSync(path.resolve(dir,PROJECT_CONFIG_FILE),"utf-8"));if(!raw||typeof raw!="object")return null;let rawRecord=raw,appId=readNormalizedAppId(rawRecord);if(!appId)return null;let rawAuth=rawRecord.auth,authOverride=buildAuthOverride(rawAuth),distributionType=readDistributionType(rawRecord,rawAuth),{distribution:_legacyDistribution,permittedUrls:_permittedUrls,support:_support,...rawWithoutLegacyDistribution}=rawRecord,rawUiApp=rawWithoutLegacyDistribution.ui_app;return"ui_app"in rawWithoutLegacyDistribution&&(!rawUiApp||typeof rawUiApp!="object")&&delete rawWithoutLegacyDistribution.ui_app,{...rawWithoutLegacyDistribution,appId,distribution_type:distributionType,...authOverride?{auth:authOverride}:{}}}catch{return null}}function hasLocalApp(){let cfg=readProjectConfig();return cfg?.appId!=null&&cfg.appId!==""}function findEnclosingProjectDir(){let dir=path.dirname(process.cwd());for(;;){if(readProjectConfigAt(dir))return dir;let parent=path.dirname(dir);if(parent===dir)return null;dir=parent}}function isUiAppConfig(config){return isUiAppConfigShape(config)}function writeProjectConfig(config){let configPath=path.resolve(process.cwd(),PROJECT_CONFIG_FILE);fs.writeFileSync(configPath,JSON.stringify(config,null,2)+`
7
8
  `,"utf-8")}function isNonEmptyString(value){return typeof value=="string"&&value.trim()!==""}function backfillProjectConfigFromServer(appId,server){let configPath=path.resolve(process.cwd(),PROJECT_CONFIG_FILE),raw;try{raw=JSON.parse(fs.readFileSync(configPath,"utf-8"))}catch{return[]}if(!raw||typeof raw!="object")return[];let normalized=readProjectConfig();if(normalized?.appId!==appId)return[];let rawRecord=raw,backfilled=[],next={...normalized};!isNonEmptyString(rawRecord.version)&&isNonEmptyString(server.version)&&(next.version=server.version,backfilled.push("version"));let rawAuth=rawRecord.auth,legacyAuthType=rawAuth&&typeof rawAuth=="object"?rawAuth.type:void 0;return isNonEmptyString(rawRecord.distribution_type)||isNonEmptyString(rawRecord.distribution)||isNonEmptyString(legacyAuthType)&&legacyAuthType!=="none"||(next.distribution_type=server.distribution_type??normalized.distribution_type,backfilled.push("distribution_type")),backfilled.length===0?[]:(writeProjectConfig(next),backfilled)}function scaffoldFileCount(done,written,total){return written===total?`${done} (${total} files)`:written===0?`already in place (${total} files, nothing rewritten)`:`${done} (${written} of ${total} files written)`}function numberedSteps(cdDir,steps){let commandWidth=Math.max(...steps.map(([command])=>command.length)),offset=cdDir?1:0;return[...cdDir?[`1. cd ${cdDir}`]:[],...steps.map(([command,note],i)=>`${i+1+offset}. ${command.padEnd(commandWidth)} (${note})`)]}var coreMessages={UPDATE_AVAILABLE:(current,latest)=>`Update available: ${current} \u2192 ${latest}`,UPDATE_RUN:name=>`Run: npm install -g ${name}`,UPDATE_RUN_YARN:name=>`Or: yarn global add ${name}`,UPDATE_RUN_BREW:"Or: brew upgrade brevo",FORCE_UPDATE_REQUIRED:(current,latest)=>`Update required: v${current} is no longer supported (latest v${latest}).`,FORCE_UPDATE_HINT:"Update to continue using the Brevo CLI:",CLI_VERSION_NOTICE_FALLBACK:"A newer version of the Brevo CLI is available.",AUTH_WELCOME:"Welcome to Brevo CLI",AUTH_PROMPT_METHOD:"How would you like to authenticate?",AUTH_PROMPT_API_KEY:"Paste your API key:",AUTH_SUCCESS:email=>`Authenticated as ${email}`,AUTH_INVALID_KEY:"Invalid API key. Please check and try again.",AUTH_HINT:(keysUrl,docsUrl)=>`
8
9
  To authenticate, you need a Brevo API key.
9
10
  Create one at: ${keysUrl}
@@ -13,7 +14,7 @@
13
14
 
14
15
  Do this: re-run with \`--distribution private\`
15
16
  Note: \`distribution_type\` is fixed at creation \u2014 \`${CLI.APP_UPLOAD}\` can't change it later
16
- Brevo said: ${serverMessage}`,APP_CREATE_REDIRECT_PROMPT:"OAuth callback URL \u2014 where users are sent after authorizing your app:",APP_CREATE_REDIRECT_HINT:cmd=>`Tip: The default below is a local test-server callback URL, used when you run \`${cmd}\`. Keep it to test your app locally, then add your production callback URL when you go live.`,APP_CREATE_REDIRECT_ANOTHER:"Add another redirect URL?",APP_CREATE_REDIRECT_EMPTY:"Redirect URL cannot be empty",APP_CREATE_REDIRECT_INVALID:"Invalid format. Must start with http:// or https://",APP_CREATE_LOGO_PROMPT:"App logo URL (optional \u2014 leave blank to skip):",APP_CREATE_LOGO_INVALID:"Invalid format. Must be a valid https:// URL (e.g. https://example.com/logo.png).",APP_CREATE_PORT_IN_USE:(port,available)=>`Port ${port} is in use. Defaulting to port ${available}.`,APP_CREATE_PORT_SCAN_FAILED:port=>`Warning: Could not find a free port near ${port}. Defaulting to ${port} \u2014 it may conflict with a running process.`,APP_CREATE_LIMIT_REACHED:"You have reached the maximum number of OAuth apps allowed for your account. To make room, delete an existing app: brevo app delete",APP_CREATE_BOX_TITLE:"App created",APP_CREATE_BOX_SCOPES_LABEL:"Default scopes:",APP_CREATE_BOX_SCOPE_HINT:`You can add more scopes later by editing \`auth.scopes\` in app-config.json and running \`${CLI.APP_UPLOAD}\`.`,APP_SCAFFOLD_FEATURE_CONFIRM:label=>label?`Scaffold the ${label}?`:"Do you want to scaffold a feature?",APP_CREATE_BASE_SUCCESS:(written,total)=>`Project structure ${scaffoldFileCount("created",written,total)}`,APP_CREATE_BASE_ONLY_NEXT:cdDir=>numberedSteps(cdDir,[[CLI.APP_SCAFFOLD,"add a feature \u2014 e.g. the OAuth test server"]]),APP_CREATE_JSON_SCAFFOLD_DIR_EXISTS:dir=>`Skipped scaffold: directory already exists (${dir}). cd into it and run \`${CLI.APP_SCAFFOLD}\` to add a feature.`,APP_CREATE_DIR_EXISTS_SKIPPED:dir=>`Skipped scaffolding: directory already exists (${dir}). cd into it and run \`${CLI.APP_SCAFFOLD}\` to add a feature.`,APP_CREATE_ALREADY_LINKED:name=>`App "${name}" is already linked in this directory (app-config.json found). Move to a different directory to create a new app, or run \`${CLI.APP_SCAFFOLD}\` here to add a feature to this project.`,APP_CREATE_DIR_UNRESOLVED:"Could not resolve the output directory for scaffolding.",APP_CREATE_UI_NEXT:cdDir=>numberedSteps(cdDir,[[CLI.APP_UPLOAD,"validate and save your configuration"],[CLI.APP_INSTALL(),"make it available in an account"]]),APP_CREATE_UI_PAGES_SPINNER:"Loading record pages...",APP_CREATE_UI_POINTS_SPINNER:"Loading placements...",APP_CREATE_UI_POINTS_FETCH_FAILED:"Could not load the available placements from the Brevo API \u2014 the UI-app flow needs them to offer where your app can appear. Check your connection and try again. Creating an OAuth app does not need this and still works.",APP_CREATE_UI_POINTS_EMPTY:"The Brevo API returned no available placements for UI apps. This usually means the extension-point registry has not been seeded in this environment \u2014 try again later.",APP_CREATE_UI_POINTS_NONE_FOR_TYPE:extensionType=>`None of the available placements can host a "${extensionType}" extension. This environment's extension-point registry may predate it \u2014 try again later.`,APP_CREATE_UI_SURFACE_PROMPT:"Which record page should it appear on?",APP_CREATE_UI_PLACEMENT_PAGE_PROMPT:page=>`Where should it appear on the ${page} page?`,APP_CREATE_UI_INTEGRATION_PROMPT:"What type of integration are you adding?",APP_CREATE_UI_INTEGRATION_EXTERNAL_LINK:"Link (Opens your URL in a new tab)",APP_CREATE_UI_LABEL_PROMPT:"Label \u2014 the menu entry\u2019s text, and the card\u2019s button text:",APP_CREATE_UI_MORE_INFO_PROMPT:"More info (optional) \u2014 the menu entry\u2019s subtext, and the card\u2019s description:",APP_CREATE_UI_REDIRECT_LINK_PROMPT:"Redirect link \u2014 the destination URL (record context arrives as query params):",APP_CREATE_UI_BOX_TITLE:"UI app created",APP_CREATE_UI_BOX_LABEL_NOTE:(label,appName)=>`The menu entry is labelled "${label}". On a card that text becomes the button, and the card's title is the app name ("${appName}").`,APP_CREATE_UI_BOX_EXAMPLE_URL_LABEL:"Brevo will open, for example:",APP_CREATE_UI_BOX_EXAMPLE_URL_NOTE:"Values are placeholders. Read them as query parameters \u2014 the path is never templated.",APP_CREATE_UI_BOX_HINT:`Edit the \`ui_app\` block in app-config.json to change any of this \u2014 add more placements as extra \`surface_point_list\` entries, each with its own label and redirect link \u2014 then run \`${CLI.APP_UPLOAD}\`.`,APP_INSTALL_SELECT:"Select an app to install:",APP_INSTALL_ACCOUNT_LABEL:(accountId,companyName,self)=>{let name=companyName?.trim();return self?name?`${name} (your own account, org ID ${accountId})`:`your own account (org ID ${accountId})`:name?`${name} (account ${accountId})`:`account ${accountId}`},APP_INSTALL_SUMMARY:"Installing this configuration (as stored on the server):",APP_INSTALL_SUMMARY_NO_VERSION:"(unknown)",APP_INSTALL_CONFIG_DRIFT:`Your local app-config.json differs from the configuration above. The install uses what the server has stored \u2014 run \`${CLI.APP_UPLOAD}\` first if you meant to install your local changes.`,APP_INSTALL_CONFIRM:(name,appId,account)=>`Install app "${name}" (${appId}) into ${account}?`,APP_INSTALL_CANCELLED:"Install cancelled.",APP_INSTALL_SUCCESS:(appId,account)=>`App ${appId} installed into ${account}.`,APP_INSTALL_NOT_UI_APP:appId=>`App ${appId} is an OAuth app, and only UI apps are installed into an account. An OAuth app becomes usable when a user authorizes it, so there is nothing to install.
17
+ Brevo said: ${serverMessage}`,APP_CREATE_REDIRECT_PROMPT:"OAuth callback URL \u2014 where users are sent after authorizing your app:",APP_CREATE_REDIRECT_HINT:cmd=>`Tip: The default below is a local test-server callback URL, used when you run \`${cmd}\`. Keep it to test your app locally, then add your production callback URL when you go live.`,APP_CREATE_REDIRECT_ANOTHER:"Add another redirect URL?",APP_CREATE_REDIRECT_EMPTY:"Redirect URL cannot be empty",APP_CREATE_REDIRECT_INVALID:"Invalid format. Must start with http:// or https://",APP_CREATE_LOGO_PROMPT:"App logo URL (optional \u2014 leave blank to skip):",APP_CREATE_LOGO_INVALID:"Invalid format. Must be a valid https:// URL (e.g. https://example.com/logo.png).",APP_CREATE_PORT_IN_USE:(port,available)=>`Port ${port} is in use. Defaulting to port ${available}.`,APP_CREATE_PORT_SCAN_FAILED:port=>`Warning: Could not find a free port near ${port}. Defaulting to ${port} \u2014 it may conflict with a running process.`,APP_CREATE_LIMIT_REACHED:"You have reached the maximum number of OAuth apps allowed for your account. To make room, delete an existing app: brevo app delete",APP_CREATE_BOX_TITLE:"App created",APP_CREATE_BOX_SCOPES_LABEL:"Default scopes:",APP_CREATE_BOX_SCOPE_HINT:`You can add more scopes later by editing \`auth.scopes\` in app-config.json and running \`${CLI.APP_UPLOAD}\`.`,APP_SCAFFOLD_FEATURE_CONFIRM:label=>label?`Scaffold the ${label}?`:"Do you want to scaffold a feature?",APP_CREATE_BASE_SUCCESS:(written,total)=>`Project structure ${scaffoldFileCount("created",written,total)}`,APP_CREATE_BASE_ONLY_NEXT:cdDir=>numberedSteps(cdDir,[[CLI.APP_SCAFFOLD,"add a feature \u2014 e.g. the OAuth test server"]]),APP_CREATE_JSON_SCAFFOLD_DIR_EXISTS:dir=>`Skipped scaffold: directory already exists (${dir}). cd into it and run \`${CLI.APP_SCAFFOLD}\` to add a feature.`,APP_CREATE_DIR_EXISTS_SKIPPED:dir=>`Skipped scaffolding: directory already exists (${dir}). cd into it and run \`${CLI.APP_SCAFFOLD}\` to add a feature.`,APP_CREATE_ALREADY_LINKED:name=>`App "${name}" is already linked in this directory (app-config.json found). Move to a different directory to create a new app, or run \`${CLI.APP_SCAFFOLD}\` here to add a feature to this project.`,APP_CREATE_DIR_UNRESOLVED:"Could not resolve the output directory for scaffolding.",APP_CREATE_UI_NEXT:cdDir=>numberedSteps(cdDir,[[CLI.APP_UPLOAD,"validate and save your configuration"],[CLI.APP_INSTALL(),"make it available in an account"]]),APP_CREATE_UI_PAGES_SPINNER:"Loading record pages...",APP_CREATE_UI_POINTS_SPINNER:"Loading placements...",APP_CREATE_UI_POINTS_FETCH_FAILED:"Could not load the available placements from the Brevo API \u2014 the UI-app flow needs them to offer where your app can appear. Check your connection and try again. Creating an OAuth app does not need this and still works.",APP_CREATE_UI_POINTS_EMPTY:"The Brevo API returned no available placements for UI apps. This usually means the extension-point registry has not been seeded in this environment \u2014 try again later.",APP_CREATE_UI_POINTS_NONE_FOR_TYPE:extensionType=>`None of the available placements can host a "${extensionType}" extension. This environment's extension-point registry may predate it \u2014 try again later.`,APP_CREATE_UI_NONINTERACTIVE_EXTENSION_TYPE:extensionType=>`Non-interactive UI app creation only supports "actionLink" today (got "${extensionType}"). Create the app interactively instead, or edit app-config.json and use \`brevo app upload\` for other extension types.`,APP_CREATE_UI_NONINTERACTIVE_BOTH_INPUTS:"--ui-config and --ui-app cannot be used together. Choose one.",APP_CREATE_UI_NONINTERACTIVE_MISSING_FLAGS:flags=>`Missing required flag(s) for --ui-app: ${flags.join(", ")}.`,APP_CREATE_UI_NONINTERACTIVE_OAUTH_FLAG:flag=>`${flag} is for OAuth apps only and cannot be combined with --ui-config or --ui-app.`,APP_CREATE_UI_NONINTERACTIVE_CONFIG_INVALID:(file,reason)=>`Could not read --ui-config "${file}": ${reason}`,APP_CREATE_UI_NONINTERACTIVE_UNKNOWN_RECORD_PAGE:(page,valid)=>`Unknown --record-page "${page}". Valid record pages: ${valid.join(", ")}.`,APP_CREATE_UI_NONINTERACTIVE_UNKNOWN_PLACEMENT:(placement,page,valid)=>`Unknown --placement "${placement}" for record page "${page}". Valid placements: ${valid.join(", ")}.`,APP_CREATE_UI_SURFACE_PROMPT:"Which record page should it appear on?",APP_CREATE_UI_PLACEMENT_PAGE_PROMPT:page=>`Where should it appear on the ${page} page?`,APP_CREATE_UI_INTEGRATION_PROMPT:"What type of integration are you adding?",APP_CREATE_UI_INTEGRATION_EXTERNAL_LINK:"Link (Opens your URL in a new tab)",APP_CREATE_UI_LABEL_PROMPT:"Label \u2014 the menu entry\u2019s text, and the card\u2019s button text:",APP_CREATE_UI_MORE_INFO_PROMPT:"More info (optional) \u2014 the menu entry\u2019s subtext, and the card\u2019s description:",APP_CREATE_UI_REDIRECT_LINK_PROMPT:"Redirect link \u2014 the destination URL (record context arrives as query params):",APP_CREATE_UI_BOX_TITLE:"UI app created",APP_CREATE_UI_BOX_LABEL_NOTE:(label,appName)=>`The menu entry is labelled "${label}". On a card that text becomes the button, and the card's title is the app name ("${appName}").`,APP_CREATE_UI_BOX_EXAMPLE_URL_LABEL:"Brevo will open, for example:",APP_CREATE_UI_BOX_EXAMPLE_URL_NOTE:"Values are placeholders. Read them as query parameters \u2014 the path is never templated.",APP_CREATE_UI_BOX_HINT:`Edit the \`ui_app\` block in app-config.json to change any of this \u2014 add more placements as extra \`surface_point_list\` entries, each with its own label and redirect link \u2014 then run \`${CLI.APP_UPLOAD}\`.`,APP_INSTALL_SELECT:"Select an app to install:",APP_INSTALL_ACCOUNT_LABEL:(accountId,companyName,self)=>{let name=companyName?.trim();return self?name?`${name} (your own account, org ID ${accountId})`:`your own account (org ID ${accountId})`:name?`${name} (account ${accountId})`:`account ${accountId}`},APP_INSTALL_SUMMARY:"Installing this configuration (as stored on the server):",APP_INSTALL_SUMMARY_NO_VERSION:"(unknown)",APP_INSTALL_CONFIG_DRIFT:`Your local app-config.json differs from the configuration above. The install uses what the server has stored \u2014 run \`${CLI.APP_UPLOAD}\` first if you meant to install your local changes.`,APP_INSTALL_CONFIRM:(name,appId,account)=>`Install app "${name}" (${appId}) into ${account}?`,APP_INSTALL_CANCELLED:"Install cancelled.",APP_INSTALL_SUCCESS:(appId,account)=>`App ${appId} installed into ${account}.`,APP_INSTALL_NOT_UI_APP:appId=>`App ${appId} is an OAuth app, and only UI apps are installed into an account. An OAuth app becomes usable when a user authorizes it, so there is nothing to install.
17
18
 
18
19
  \`${CLI.APP_LIST}\` shows each app's type.`,APP_UNINSTALL_NOT_UI_APP:appId=>`App ${appId} is an OAuth app, and only UI apps are installed into an account, so there is nothing to uninstall.
19
20
 
@@ -137,7 +138,7 @@ Examples:
137
138
  `)}function printFileTree(filePaths){for(let line of formatFileTree(filePaths).split(`
138
139
  `))logInfo(line)}function computeSlug(name){return(name||"my-app").toLowerCase().replaceAll(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")||"my-app"}async function fetchAppContext(appId,silent,uiApp,fallbackApp){let spinner=createSpinner("Fetching app details...",{silent}),result;try{result=await appService.resolveAppCredentials(appId,{tolerateMissing:!!fallbackApp})}finally{spinner.stop()}let appDetails=result?.app??null;result?(result.diffs.length>0&&logWarn(`Local credentials for app ${appId} differ from server (${result.diffs.join(", ")}). Updating local cache.`),appService.syncAppCredentials(appId,result.app)):fallbackApp&&(appDetails=fallbackApp,silent||logWarn(messages.APP_SCAFFOLD_SERVER_READBACK_FAILED(appId)));let serverRedirectUrls=appDetails?.redirect_uris??[],redirectUris=serverRedirectUrls.length>0?serverRedirectUrls:[DEFAULT_REDIRECT_URI],localhostUri=redirectUris.find(url=>url.startsWith("http://localhost")||url.startsWith("http://127.0.0.1"));return{appDetails,clientId:appDetails?.client_id||PLACEHOLDER_CLIENT_ID,clientSecret:appDetails?.client_secret||"YOUR_CLIENT_SECRET",redirectUris,redirectUri:localhostUri||DEFAULT_REDIRECT_URI,...uiApp?{uiApp}:{}}}async function resolveProjectDirectory(defaultDir,jsonMode=!1,validateTarget){let outputDir=jsonMode?defaultDir:(await import_inquirer3.default.prompt([{type:"input",name:"outputDir",message:messages.APP_SCAFFOLD_DIR_PROMPT,default:defaultDir}])).outputDir,targetDir=path4.resolve(outputDir),existed=fs4.existsSync(targetDir);if(validateTarget?.(targetDir),!existed)return{targetDir,mergeOnly:!1,chooseAgain:!1,existed:!1};if(jsonMode)return{targetDir,unresolved:!0};let{action}=await import_inquirer3.default.prompt([{type:"list",name:"action",message:messages.APP_SCAFFOLD_DIR_EXISTS,choices:indentChoices([{name:"Overwrite existing files",value:"overwrite"},{name:"Merge (keep existing, add missing)",value:"merge"},{name:"Choose a different path",value:"new"}])}]);return action==="new"?{targetDir,mergeOnly:!1,chooseAgain:!0,existed:!0}:{targetDir,mergeOnly:action==="merge",chooseAgain:!1,existed:!0}}function applyProjectDirectory(decision,jsonMode=!1){if(decision.unresolved||decision.chooseAgain)return;let{targetDir,existed}=decision;existed?jsonMode||(targetDir===process.cwd()?logInfo(messages.APP_SCAFFOLD_TARGET_IS_CWD):logInfo(messages.APP_SCAFFOLD_USING_EXISTING_DIR(path4.relative(process.cwd(),targetDir)))):(jsonMode||logInfo(messages.APP_SCAFFOLD_CREATING_DIR(path4.relative(process.cwd(),targetDir))),fs4.mkdirSync(targetDir,{recursive:!0})),process.chdir(targetDir)}function diffLocalConfig(localConfig,ctx){let diffs=[],serverName=ctx.appDetails?.name;serverName&&localConfig.appName!==serverName&&diffs.push({field:"appName",local:localConfig.appName||"(none)",server:serverName});let serverDistribution=ctx.appDetails?.distribution_type??"private";if(localConfig.distribution_type!==serverDistribution&&diffs.push({field:"distribution_type",local:localConfig.distribution_type,server:serverDistribution}),!isUiAppConfig(localConfig)){let localRedirects=[...localConfig.auth?.redirectUris??[]].sort((a,b)=>a.localeCompare(b)),serverRedirects=[...ctx.redirectUris].sort((a,b)=>a.localeCompare(b));JSON.stringify(localRedirects)!==JSON.stringify(serverRedirects)&&diffs.push({field:"redirectUris",local:localRedirects.join(", ")||"(none)",server:serverRedirects.join(", ")||"(none)"})}if(!isUiAppConfig(localConfig)){let localScopes=[...localConfig.auth?.scopes??[]].sort((a,b)=>a.localeCompare(b)),serverScopes=[...ctx.appDetails?.scopes??[]].filter(s=>s!==LEGACY_ALL_SCOPE).sort((a,b)=>a.localeCompare(b));JSON.stringify(localScopes)!==JSON.stringify(serverScopes)&&diffs.push({field:"scopes",local:localScopes.join(", ")||"(none)",server:serverScopes.join(", ")||"(none)"})}let localLogo=localConfig.logoUri??"",serverLogo=ctx.appDetails?.logo_uri??"";localLogo!==serverLogo&&diffs.push({field:"logoUri",local:localLogo||"(none)",server:serverLogo||"(none)"});let localVersion=localConfig.version??"",serverVersion=ctx.appDetails?.version??"";return localVersion!==serverVersion&&diffs.push({field:"version",local:localVersion||"(none)",server:serverVersion||"(none)"}),diffs}function writeScaffoldFiles(files,targetDir,mergeOnly){let written=0;for(let file of files){let filePath=path4.join(targetDir,file.name);if(fs4.mkdirSync(path4.dirname(filePath),{recursive:!0}),mergeOnly&&fs4.existsSync(filePath))continue;let writeOptions=file.name.endsWith(".env.local")?{mode:384}:{};fs4.writeFileSync(filePath,file.content,{encoding:"utf-8",...writeOptions}),written++}return written}function renderUiAppJson(uiApp){return uiApp?JSON.stringify(uiApp,null,2).replaceAll(`
139
140
  `,`
140
- `):""}function buildTemplateVars(appId,ctx,targetDir){let appName=(ctx.appDetails?.name||path4.basename(targetDir)).replaceAll(/["\\\n\r\t]/g,"").trim()||"my-app",remoteScopes=ctx.appDetails?.scopes,legacyAllSubstituted=!ctx.uiApp&&containsLegacyAllScope(remoteScopes),granularScopes=(remoteScopes??[]).filter(s=>s!==LEGACY_ALL_SCOPE),scopes;ctx.uiApp?scopes=[]:scopes=granularScopes.length>0?granularScopes:[...DEFAULT_SCOPES];let slug=computeSlug(ctx.appDetails?.name);return{vars:{"{{APP_NAME}}":appName,"{{APP_SLUG}}":slug,"{{APP_ID}}":String(appId),"{{CLIENT_ID}}":ctx.clientId,"{{CLIENT_SECRET}}":ctx.clientSecret,"{{REDIRECT_URI}}":ctx.redirectUri,"{{REDIRECT_URLS_JSON}}":JSON.stringify(ctx.redirectUris),"{{SCOPES_JSON}}":JSON.stringify(scopes),"{{DISTRIBUTION}}":ctx.appDetails?.distribution_type??"private","{{LOGO_URI}}":ctx.appDetails?.logo_uri??"","{{APP_VERSION}}":ctx.appDetails?.version??"","{{OAUTH_BASE}}":OAUTH_BASE,"{{OAUTH_REALM}}":OAUTH_REALM,"{{UI_APP_JSON}}":renderUiAppJson(ctx.uiApp)},scopes,legacyAllSubstituted}}function runBaseScaffold(appId,ctx,targetDir,mergeOnly){let{vars,scopes,legacyAllSubstituted}=buildTemplateVars(appId,ctx,targetDir),files=loadBaseTemplates(vars);return{written:writeScaffoldFiles(files,targetDir,mergeOnly),legacyAllSubstituted,scopes,files}}async function resolveFeatureConflict(featureType,appId,ctx,targetDir,opts){if(opts.overwrite)return"overwrite";let{vars}=buildTemplateVars(appId,ctx,targetDir);if(!loadFeatureTemplates(featureType,vars).some(f=>fs4.existsSync(path4.join(targetDir,f.name)))||opts.jsonMode)return"merge";let{action}=await import_inquirer3.default.prompt([{type:"list",name:"action",message:messages.APP_SCAFFOLD_FEATURE_EXISTS,choices:indentChoices([{name:messages.APP_SCAFFOLD_FEATURE_EXISTS_OVERWRITE,value:"overwrite"},{name:messages.APP_SCAFFOLD_FEATURE_EXISTS_MERGE,value:"merge"},{name:messages.APP_SCAFFOLD_FEATURE_EXISTS_CANCEL,value:"cancel"}])}]);return action}function runFeatureScaffold(featureType,appId,ctx,targetDir,mergeOnly){let{vars}=buildTemplateVars(appId,ctx,targetDir);featureType==="oauth"&&fs4.mkdirSync(path4.join(targetDir,"src","oauth"),{recursive:!0});let files=loadFeatureTemplates(featureType,vars);return{written:writeScaffoldFiles(files,targetDir,mergeOnly),files}}function reportBaseScaffoldSuccess(result){logSuccess(messages.APP_CREATE_BASE_SUCCESS(result.written,result.files.length)),result.legacyAllSubstituted&&logWarn(messages.LEGACY_ALL_SCOPE_SCAFFOLD_SUBSTITUTED(result.scopes.join(", "))),printFileTree(result.files.map(f=>f.name))}function reportScaffoldSuccess(result){logSuccess(messages.APP_SCAFFOLD_SUCCESS(result.written,result.files.length)),result.legacyAllSubstituted&&logWarn(messages.LEGACY_ALL_SCOPE_SCAFFOLD_SUBSTITUTED(result.scopes.join(", "))),printFileTree(result.files.map(f=>f.name)),printBox(messages.APP_SCAFFOLD_NEXT_STEPS_TITLE,messages.APP_SCAFFOLD_NEXT_STEPS_LINES(result.cdDir)),logInfo(messages.APP_SCAFFOLD_SCOPES_TIP)}function computeCdHint(originalCwd,targetDir){return path4.relative(originalCwd,targetDir)||void 0}var import_inquirer4=__toESM(require("inquirer"));function featureTypes(){return Object.keys(FEATURE_TEMPLATE_MANIFESTS)}function soleFeatureType(){let types=featureTypes();return types.length===1?types[0]:void 0}var FALLBACK_FEATURE="oauth";function soleFeatureLabel(){let only=soleFeatureType();return only?FEATURE_LABELS[only]:void 0}async function promptFeatureType(interactive){let types=featureTypes(),only=soleFeatureType();if(only)return only;if(!interactive)return types[0]??FALLBACK_FEATURE;let{featureType}=await import_inquirer4.default.prompt([{type:"list",name:"featureType",message:messages.APP_SCAFFOLD_FEATURE_TYPE_PROMPT,choices:indentChoices(types.map(type=>({name:FEATURE_LABELS[type],value:type})))}]);return featureType}async function promptScaffoldFeature(){let{scaffoldRaw}=await import_inquirer4.default.prompt([{type:"input",name:"scaffoldRaw",message:messages.APP_SCAFFOLD_FEATURE_CONFIRM(soleFeatureLabel())+" (Y/n)",default:"y",validate:validateYesNo}]),val=String(scaffoldRaw).toLowerCase().trim();return val===""||val.startsWith("y")}async function finishProject(params){let{appId,ctx,targetDir,cdDir,isUiApp}=params;if(isUiApp)return printBox(messages.APP_SCAFFOLD_NEXT_STEPS_TITLE,messages.APP_CREATE_UI_NEXT(cdDir)),{cancelled:!1,feature:null,written:0};if(!(params.offerFeature&&await promptScaffoldFeature()))return logInfo(messages.APP_SCAFFOLD_SCOPES_TIP),printBox(messages.APP_SCAFFOLD_NEXT_STEPS_TITLE,messages.APP_CREATE_BASE_ONLY_NEXT(cdDir)),{cancelled:!1,feature:null,written:0};let feature=await promptFeatureType(!0),mergeOnly;if(params.onConflict==="ask"){let choice=await resolveFeatureConflict(feature,appId,ctx,targetDir,{jsonMode:!!params.jsonMode,overwrite:!!params.overwriteFlag});if(choice==="cancel")return logInfo(messages.APP_SCAFFOLD_CANCELLED),{cancelled:!0};mergeOnly=choice==="merge"}else mergeOnly=params.onConflict==="merge";let feat=runFeatureScaffold(feature,appId,ctx,targetDir,mergeOnly);return reportScaffoldSuccess({written:feat.written,legacyAllSubstituted:!1,scopes:params.baseScopes,files:feat.files,targetDir,cdDir}),{cancelled:!1,feature,written:feat.written}}var import_inquirer5=__toESM(require("inquirer"));var NONE="(none)",VALUE_ROWS=[{label:"label: ",read:e=>e.label},{label:"more info: ",read:e=>e.more_info},{label:"redirect link: ",read:e=>e.redirect_link},{label:"modal URL: ",read:e=>e.modal_iframe_url},{label:"card size: ",read:e=>formatSize(e.size)}];function formatSize(size){if(!size)return;let axes=[...size.width?[`width ${size.width}`]:[],...size.height?[`height ${size.height}`]:[]];return axes.length?axes.join(", "):void 0}function formatContext(entry){return entry.context?.length?` (context: ${entry.context.join(", ")})`:""}function formatPlacementLines(uiApp){return(uiApp.surface_point_list??[]).flatMap(entry=>[`${entry.surface_point_name}${formatContext(entry)}`,...VALUE_ROWS.flatMap(({label,read})=>{let value=read(entry);return value?[` ${label}${value}`]:[]})])}function formatPlacementDiffLines(next,current){if(!current)return formatPlacementLines(next);let currentEntries=current.surface_point_list??[],nextEntries=next.surface_point_list??[],before=new Map(currentEntries.map(entry=>[entry.surface_point_name,entry])),nextNames=new Set(nextEntries.map(entry=>entry.surface_point_name));return[...nextEntries.flatMap(entry=>{let previous=before.get(entry.surface_point_name);if(!previous){let[slot,...rest]=formatPlacementLines({surface_point_list:[entry]});return[`${slot} (new)`,...rest]}return[`${entry.surface_point_name}${diffContext(previous,entry)}`,...VALUE_ROWS.flatMap(({label,read})=>{let from=read(previous),to=read(entry);return from===to?to?[` ${label}${to}`]:[]:[` ${label}${from??NONE} \u2192 ${to??NONE}`]})]}),...currentEntries.filter(entry=>!nextNames.has(entry.surface_point_name)).map(entry=>`${entry.surface_point_name} (removed)`)]}function diffContext(previous,entry){let from=previous.context??[],to=entry.context??[];return from.join(",")===to.join(",")?formatContext(entry):` (context: ${from.length?from.join(", "):NONE} \u2192 ${to.length?to.join(", "):NONE})`}var PLACEMENT_QUESTION_PREFIX="placement:";function toUsableRows(rows){let usable=[];for(let row of rows){let segments=row.extension_point_name.split("."),[locationToken,placeToken,kindToken]=segments.length===3?segments:["","",""],location=(row.location_name??"").trim()||locationToken,section=(row.section_name??"").trim()||placeToken,component=(row.component_type??"").trim()||kindToken,slug=(row.surface_point_name??"").trim();!location||!section||!component||!slug||usable.push({...row,location_name:location,section_name:section,component_type:component,surface_point_name:slug})}return usable}function rowSupportsExtensionType(row,extensionType){if(row.status?.trim()&&row.status.trim()!=="active")return!1;let types=row.extension_type_list;return!types||types.length===0?!0:types.includes(extensionType)}async function fetchRecordPageLocations(extensionType){let spinner=createSpinner(messages.APP_CREATE_UI_PAGES_SPINNER),locations;try{locations=await appService.fetchSurfacePointLocations(extensionType)}catch{throw new CliError(messages.APP_CREATE_UI_POINTS_FETCH_FAILED)}finally{spinner.stop()}if(locations.length===0)throw new CliError(messages.APP_CREATE_UI_POINTS_EMPTY);return locations}async function readSurfacePointRows(locations,extensionType){try{return await appService.fetchSurfacePoints(locations,extensionType)}catch{return null}}async function fetchSurfacePointsForPages(locations,extensionType){let onPickedPages=rows=>toUsableRows(rows).filter(row=>locations.includes(row.location_name)),pagesCovered=rows=>new Set(rows.map(row=>row.location_name)).size,spinner=createSpinner(messages.APP_CREATE_UI_POINTS_SPINNER),usable;try{let narrowed=await readSurfacePointRows(locations,extensionType);if(usable=onPickedPages(narrowed??[]),narrowed===null||pagesCovered(usable)<locations.length){let unfiltered=await readSurfacePointRows();if(unfiltered===null&&narrowed===null)throw new CliError(messages.APP_CREATE_UI_POINTS_FETCH_FAILED);let fallback=onPickedPages(unfiltered??[]);pagesCovered(fallback)>pagesCovered(usable)&&(usable=fallback)}}finally{spinner.stop()}let hostable=usable.filter(row=>rowSupportsExtensionType(row,extensionType));if(hostable.length===0)throw new CliError(usable.length>0?messages.APP_CREATE_UI_POINTS_NONE_FOR_TYPE(extensionType):messages.APP_CREATE_UI_POINTS_EMPTY);return hostable}function placementLabel(row){return`${row.section_name} \u2014 ${row.component_type}`}async function promptSurfacePoint(locations,extensionType){let{surface}=await import_inquirer5.default.prompt([{type:"list",name:"surface",message:messages.APP_CREATE_UI_SURFACE_PROMPT,choices:indentChoices(locations.map(location=>({name:location,value:location})))}]),page=locations.find(location=>location===String(surface??"").trim()),forPage=(await fetchSurfacePointsForPages(page?[page]:[],extensionType)).filter(row=>row.location_name===page),question=`${PLACEMENT_QUESTION_PREFIX}${page}`,answer=await import_inquirer5.default.prompt([{type:"list",name:question,message:messages.APP_CREATE_UI_PLACEMENT_PAGE_PROMPT(page??""),choices:indentChoices(forPage.map(row=>({name:placementLabel(row),value:row.surface_point_name})))}]),chosen=String(answer[question]??"").trim();return forPage.filter(row=>row.surface_point_name===chosen)}async function promptIntegrationType(){let{integrationType}=await import_inquirer5.default.prompt([{type:"list",name:"integrationType",message:messages.APP_CREATE_UI_INTEGRATION_PROMPT,choices:indentChoices([{name:messages.APP_CREATE_UI_INTEGRATION_EXTERNAL_LINK,value:EXTENSION_TYPE_ACTION_LINK}])}]);return integrationType}async function resolveUiApp(){let extensionType=await promptIntegrationType(),locations=await fetchRecordPageLocations(extensionType),selectedRows=await promptSurfacePoint(locations,extensionType),{label}=await import_inquirer5.default.prompt([{type:"input",name:"label",message:messages.APP_CREATE_UI_LABEL_PROMPT,validate:validateUiAppLabel}]),{more_info}=await import_inquirer5.default.prompt([{type:"input",name:"more_info",message:messages.APP_CREATE_UI_MORE_INFO_PROMPT,validate:validateUiAppMoreInfo}]),{url}=await import_inquirer5.default.prompt([{type:"input",name:"url",message:messages.APP_CREATE_UI_REDIRECT_LINK_PROMPT,validate:validateUiAppUrl}]),uiApp={extension_type:extensionType,surface_point_list:buildSurfacePointList(selectedRows,{contextFor:row=>row.default_context_field??[],label:String(label??"").trim(),more_info:String(more_info??"").trim(),redirect_link:String(url??"").trim()})};return validateUiApp(uiApp),uiApp}function buildSurfacePointList(rows,fields){let entries=[],seen=new Set;for(let row of rows){if(seen.has(row.surface_point_name))continue;seen.add(row.surface_point_name);let context=fields.contextFor(row).map(field=>String(field).trim()).filter(Boolean);entries.push({surface_point_name:row.surface_point_name,...context.length?{context}:{},label:fields.label,...fields.more_info?{more_info:fields.more_info}:{},redirect_link:fields.redirect_link})}return entries}function buildExampleContextUrl(redirectLink,context){let url;try{url=new URL(redirectLink)}catch{return null}for(let field of context)url.searchParams.set(field,field.replaceAll(/([a-z0-9])([A-Z])/g,"$1_$2").toUpperCase());return url.toString()}function renderExampleContextUrlLines(uiApp){let withContext=uiApp.surface_point_list.find(entry=>entry.context?.length&&entry.redirect_link);if(!withContext)return[];let example=buildExampleContextUrl(withContext.redirect_link,withContext.context??[]);return example?["",`${messages.APP_CREATE_UI_BOX_EXAMPLE_URL_LABEL}`,` ${example}`,messages.APP_CREATE_UI_BOX_EXAMPLE_URL_NOTE]:[]}function renderCreatedUiApp(result,appName,uiApp,logoUri){let boxLines=[`App name: ${appName}`,`App ID: ${result.app_id}`,`Extension type: ${uiApp.extension_type}`,...formatPlacementLines(uiApp).map((line,i)=>`${i===0?"Placement: ":" "}${line}`),...logoUri?[`Logo URL: ${logoUri}`]:[],...result.version?[`App version: ${result.version}`]:[],...renderExampleContextUrlLines(uiApp),"",messages.APP_CREATE_UI_BOX_LABEL_NOTE(uiApp.surface_point_list[0]?.label??"",appName),messages.APP_CREATE_UI_BOX_HINT];printBox(messages.APP_CREATE_UI_BOX_TITLE,boxLines)}function validateHttpUrl(trimmed,invalidMessage){try{let parsed=new URL(trimmed);return parsed.protocol!=="http:"&&parsed.protocol!=="https:"?invalidMessage:!0}catch{return invalidMessage}}var validateRedirectUrl=input=>{let trimmed=input.trim();return trimmed?validateHttpUrl(trimmed,messages.APP_CREATE_REDIRECT_INVALID):messages.APP_CREATE_REDIRECT_EMPTY},validateLogoUrl=input=>{let trimmed=input.trim();return trimmed?validateHttpUrl(trimmed,messages.APP_CREATE_LOGO_INVALID):!0};function guardAgainstLinkedApp(){if(!hasLocalApp())return;let projectConfig=readProjectConfig(),linkedName=projectConfig?.appName||String(projectConfig?.appId??"");throw new CliError(messages.APP_CREATE_ALREADY_LINKED(linkedName))}async function resolveAppName(nameFlag){if(nameFlag){let nameCheck=validateAppName(nameFlag);if(nameCheck!==!0)throw new CliError(nameCheck);return nameFlag}return(await import_inquirer6.default.prompt([{type:"input",name:"name",message:messages.APP_CREATE_NAME_PROMPT,validate:validateAppName}])).name}async function resolveAppType(interactive){if(!interactive)return"oauth";let choices=[{name:messages.APP_CREATE_APP_TYPE_OAUTH,value:"oauth"}];return isFeatureAvailable("ui-app-type")&&choices.push({name:messages.APP_CREATE_APP_TYPE_UI,value:"ui"}),(await import_inquirer6.default.prompt([{type:"list",name:"appType",message:messages.APP_CREATE_APP_TYPE_PROMPT,choices:indentChoices(choices)}])).appType}function assertDistributionFlag(distributionFlag){validateEnum(distributionFlag,["private","public"],"--distribution"),distributionFlag==="public"&&assertFeatureAvailable("public-distribution")}async function resolveDistribution(distributionFlag,interactive){if(distributionFlag)return distributionFlag;if(!interactive)return"private";let choices=[{name:"Private (Used exclusively by your organisation)",value:"private"}];return(await import_inquirer6.default.prompt([{type:"list",name:"distribution",message:messages.APP_CREATE_TYPE_PROMPT,choices:indentChoices(choices)}])).distribution}async function promptAddAnotherRedirect(){let{anotherRaw}=await import_inquirer6.default.prompt([{type:"input",name:"anotherRaw",message:messages.APP_CREATE_REDIRECT_ANOTHER+" (y/N)",default:"n",validate:validateYesNo}]);return String(anotherRaw).toLowerCase().trim().startsWith("y")}async function promptRedirectUrls(quiet){let availablePort=await findAvailablePort(DEFAULT_PORT),defaultRedirect=availablePort==null||availablePort===DEFAULT_PORT?DEFAULT_REDIRECT_URI:`http://localhost:${availablePort}/auth/callback`;quiet||(availablePort==null?logInfo(messages.APP_CREATE_PORT_SCAN_FAILED(DEFAULT_PORT)):availablePort!==DEFAULT_PORT&&logInfo(messages.APP_CREATE_PORT_IN_USE(DEFAULT_PORT,availablePort)),logInfo(messages.APP_CREATE_REDIRECT_HINT(CLI.APP_START("oauth"))));let redirectUris=[],{redirectUrl:firstUrl}=await import_inquirer6.default.prompt([{type:"input",name:"redirectUrl",message:messages.APP_CREATE_REDIRECT_PROMPT,default:defaultRedirect,validate:validateRedirectUrl}]);for(redirectUris.push(firstUrl.trim());await promptAddAnotherRedirect();){let{nextUrl}=await import_inquirer6.default.prompt([{type:"input",name:"nextUrl",message:messages.APP_CREATE_REDIRECT_PROMPT,validate:validateRedirectUrl}]);redirectUris.push(nextUrl.trim())}return redirectUris}async function resolveRedirectUrls(redirectUriFlag,quiet){let flagUrls=redirectUriFlag??[];return flagUrls.length>0?flagUrls:process.stdin.isTTY?promptRedirectUrls(quiet):[DEFAULT_REDIRECT_URI]}async function resolveLogoUri(logoUriFlag,jsonMode){if(logoUriFlag||!process.stdin.isTTY||jsonMode)return logoUriFlag;let{logoUrl}=await import_inquirer6.default.prompt([{type:"input",name:"logoUrl",message:messages.APP_CREATE_LOGO_PROMPT,validate:validateLogoUrl}]);return String(logoUrl??"").trim()||void 0}async function resolveCreateDirectory(appName,interactive){let slug=computeSlug(appName);if(!interactive){let targetDir=path5.resolve(`./${slug}`);return fs5.existsSync(targetDir)?{targetDir,skipped:!0}:{targetDir,mergeOnly:!1,skipped:!1,existed:!1}}let dir=await resolveProjectDirectory(`./${slug}`);for(;!dir.unresolved&&dir.chooseAgain;)dir=await resolveProjectDirectory(`./${slug}`);if(dir.unresolved)throw new CliError(messages.APP_CREATE_DIR_UNRESOLVED);return{targetDir:dir.targetDir,mergeOnly:dir.mergeOnly,skipped:!1,existed:dir.existed}}function applyCreateDirectory(dir,jsonMode){dir.skipped||applyProjectDirectory({targetDir:dir.targetDir,mergeOnly:dir.mergeOnly,chooseAgain:!1,existed:dir.existed},jsonMode)}function buildCreatePayload(inputs){let isUiApp=!!inputs.uiApp;return{name:inputs.appName,distribution_type:inputs.distribution,...isUiApp?{ui_app:inputs.uiApp}:{auth:{scopes:[...DEFAULT_SCOPES],redirect_uris:inputs.redirectUris}},...inputs.logoUri?{logo_uri:inputs.logoUri}:{}}}async function retryCreateWithNewName(inputs){logError(messages.APP_CREATE_NAME_TAKEN);let retry=await import_inquirer6.default.prompt([{type:"input",name:"name",message:messages.APP_CREATE_NAME_PROMPT,validate:validateAppName}]),retrySpinner=createSpinner("Creating app...");try{let result=await appService.createApp(buildCreatePayload({...inputs,appName:retry.name}));return retrySpinner.stop(),{result,appName:retry.name}}catch(retryErr){throw retrySpinner.stop(),retryErr}}async function retryCreateAfterLogin(inputs){logWarn(messages.APP_CREATE_SESSION_EXPIRED);let{relogin}=await import_inquirer6.default.prompt([{type:"confirm",name:"relogin",message:messages.APP_CREATE_RELOGIN_CONFIRM,default:!0}]);if(!relogin)throw new AuthExpiredError;if(await loginCommand({suppressNextSteps:!0}),!isAuthenticated())throw new AuthExpiredError;let spinner=createSpinner("Creating app...");try{return{result:await appService.createApp(buildCreatePayload(inputs)),appName:inputs.appName}}finally{spinner.stop()}}function isPublicDistributionRefusal(err,distribution){return err instanceof ApiError&&err.statusCode===400&&distribution==="public"&&/distribution_type/i.test(err.message)}async function createAppWithRetry(inputs,jsonMode,interactive){let spinner=createSpinner("Creating app...",{silent:jsonMode});try{let result=await appService.createApp(buildCreatePayload(inputs));return spinner.stop(),{result,appName:inputs.appName}}catch(err){if(spinner.stop(),err instanceof ApiError&&err.errorCode==="APP_LIMIT_REACHED")throw jsonMode&&jsonOutput({error:"APP_LIMIT_REACHED",message:messages.APP_CREATE_LIMIT_REACHED}),new CliError(messages.APP_CREATE_LIMIT_REACHED);if(isPublicDistributionRefusal(err,inputs.distribution))throw new CliError(messages.APP_CREATE_PUBLIC_REJECTED(err.message));if(err instanceof ApiError&&err.statusCode===409)return retryCreateWithNewName(inputs);if(err instanceof AuthExpiredError&&interactive)return retryCreateAfterLogin(inputs);throw err}}function renderCreatedApp(result,appName,logoUri){let boxLines=[`App name: ${appName}`,`App ID: ${result.app_id}`,`Client ID: ${result.client_id}`,`Client secret: ${messages.CLIENT_SECRET_HIDDEN_HUMAN}`,...(result.redirect_uris??[]).map((uri,i)=>`Redirect URL ${i+1}: ${uri}`),...logoUri?[`Logo URL: ${logoUri}`]:[],...result.version?[`App version: ${result.version}`]:[],`${messages.APP_CREATE_BOX_SCOPES_LABEL} ${[...DEFAULT_SCOPES].join(", ")}`,"",messages.APP_CREATE_BOX_SCOPE_HINT];printBox(messages.APP_CREATE_BOX_TITLE,boxLines)}var createCommand=withCommandHandler(async options=>{let jsonMode=!!options.json,originalCwd=process.cwd();guardAgainstLinkedApp(),assertDistributionFlag(options.distribution);let interactive=!jsonMode&&!!process.stdin.isTTY,appName=await resolveAppName(options.name),logoUri=await resolveLogoUri(options.logoUri,jsonMode),distribution=await resolveDistribution(options.distribution,interactive),appType=await resolveAppType(interactive),redirectUris=[],uiApp;appType==="ui"?uiApp=await resolveUiApp():redirectUris=await resolveRedirectUrls(options.redirectUri,jsonMode);let dir=await resolveCreateDirectory(appName,interactive),inputs={appName,distribution,redirectUris,logoUri,uiApp},{result,appName:finalAppName}=await createAppWithRetry(inputs,jsonMode,interactive);applyCreateDirectory(dir,jsonMode),result.client_id&&result.client_secret&&saveAppCredentials(result.app_id,{clientId:result.client_id,clientSecret:result.client_secret}),finalAppName&&saveAppName(result.app_id,finalAppName);let jsonBase={appId:result.app_id,appName:finalAppName,clientId:result.client_id,clientSecret:messages.CLIENT_SECRET_HIDDEN_JSON,appType,...uiApp?{uiApp}:{redirectUri:result.redirect_uris},...logoUri?{logoUri}:{},...result.version?{version:result.version}:{}},renderBox2=()=>uiApp?renderCreatedUiApp(result,finalAppName,uiApp,logoUri):renderCreatedApp(result,finalAppName,logoUri);if(dir.skipped){if(jsonMode){jsonOutput({...jsonBase,directory:dir.targetDir,scaffoldSkipped:messages.APP_CREATE_JSON_SCAFFOLD_DIR_EXISTS(dir.targetDir)});return}renderBox2(),logInfo(messages.APP_CREATE_DIR_EXISTS_SKIPPED(dir.targetDir));return}let fallbackApp={...result,client_id:result.client_id??"",redirect_uris:result.redirect_uris??null},ctx=await fetchAppContext(result.app_id,jsonMode,uiApp,fallbackApp),base=runBaseScaffold(result.app_id,ctx,dir.targetDir,dir.mergeOnly);if(jsonMode){jsonOutput({...jsonBase,directory:dir.targetDir,scaffolded:base.written});return}renderBox2(),reportBaseScaffoldSuccess(base),await finishProject({appId:result.app_id,ctx,targetDir:dir.targetDir,baseScopes:base.scopes,cdDir:computeCdHint(originalCwd,dir.targetDir),isUiApp:!!uiApp,offerFeature:interactive,onConflict:dir.mergeOnly?"merge":"overwrite"})});var http=__toESM(require("node:http")),import_node_crypto=require("node:crypto");var MAX_BODY_BYTES=16*1024,DEFAULT_TIMEOUT_MS=3e5;function normalizeTokens(raw){return typeof raw.access_token!="string"||!raw.access_token||typeof raw.refresh_token!="string"||!raw.refresh_token||typeof raw.expires_in!="number"||!Number.isFinite(raw.expires_in)||raw.expires_in<=0||typeof raw.token_type!="string"||!raw.token_type?null:{accessToken:raw.access_token,refreshToken:raw.refresh_token,expiresIn:raw.expires_in,tokenType:raw.token_type,scope:typeof raw.scope=="string"?raw.scope:void 0}}async function runBrowserLoginFlow(opts){let proxyOrigin=new URL(opts.proxyUrl).origin,timeoutMs=opts.timeoutMs??DEFAULT_TIMEOUT_MS,openBrowser2=opts.openBrowser??(()=>{});return new Promise((resolve11,reject)=>{let settled=!1,claimSettlement=()=>settled?!1:(settled=!0,!0),server=http.createServer((req,res)=>{let pathname=new URL(req.url??"/","http://127.0.0.1").pathname,origin=req.headers.origin;if(logDebug("loopback request",{method:req.method,url:req.url,pathname,origin}),req.method==="OPTIONS"&&pathname==="/callback"){if(origin!==proxyOrigin){logDebug("loopback OPTIONS rejected: origin mismatch",{origin,expected:proxyOrigin}),res.writeHead(403).end();return}res.writeHead(204,{"Access-Control-Allow-Origin":proxyOrigin,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type","Access-Control-Max-Age":"600"}).end();return}if(req.method==="POST"&&pathname==="/callback"){if(origin!==proxyOrigin){logDebug("loopback POST rejected: origin mismatch",{origin,expected:proxyOrigin}),res.writeHead(403).end();return}let bytes=0,chunks=[];req.on("data",chunk=>{if(bytes+=chunk.length,bytes>MAX_BODY_BYTES){logDebug("loopback POST rejected: body too large",{bytes,max:MAX_BODY_BYTES}),res.writeHead(413,{Connection:"close"}).end(),req.destroy();return}chunks.push(chunk)}),req.on("end",()=>{let parsed=null;try{parsed=JSON.parse(Buffer.concat(chunks).toString("utf-8"))}catch{parsed=null}let tokens=parsed?normalizeTokens(parsed):null;if(!tokens){logDebug("loopback POST rejected: bad payload shape",{hasParsed:parsed!==null,keys:parsed?Object.keys(parsed):null}),res.writeHead(400,{"Access-Control-Allow-Origin":proxyOrigin,"Content-Type":"text/plain"}).end("Bad payload");return}logDebug("loopback POST accepted",{hasScope:tokens.scope!==void 0}),res.writeHead(204,{"Access-Control-Allow-Origin":proxyOrigin}).end(),claimSettlement()&&(server.close(),resolve11(tokens))});return}if(req.method==="GET"&&(pathname==="/"||pathname==="/callback")){res.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),res.end('<!doctype html><meta charset="utf-8"><title>Brevo CLI login</title><p>Waiting for login to complete \u2014 you can close this tab once the CLI confirms success.</p>');return}logDebug("loopback request not matched",{method:req.method,pathname}),res.writeHead(404).end()});server.on("error",err=>{logDebug("loopback server error",{message:err.message}),claimSettlement()&&reject(err)}),server.listen(0,"127.0.0.1",()=>{let port=server.address().port,attemptToken=(0,import_node_crypto.randomUUID)(),loginUrl=`${opts.proxyUrl}/login?port=${port}&t=${attemptToken}`;logDebug("loopback listening",{host:"127.0.0.1",port,proxyOrigin}),opts.onWaiting?.(loginUrl);try{openBrowser2(loginUrl)}catch{}});let timer=setTimeout(()=>{claimSettlement()&&(server.close(),reject(new CliError(messages.AUTH_BROWSER_TIMEOUT)))},timeoutMs);timer.unref?.(),server.on("close",()=>clearTimeout(timer))})}function wipeAppsCacheIfAccountChanged(newOrganizationId){let previousOrganizationId=getOrganizationId();previousOrganizationId&&previousOrganizationId!==newOrganizationId&&clearAppsCache()}async function promptApiKey(){let{key}=await import_inquirer7.default.prompt([{type:"password",name:"key",message:messages.AUTH_PROMPT_API_KEY,mask:"*",validate:input=>input.trim().length>0||"API key cannot be empty"}]);return key}async function resolveLoginMethod(forceBrowser,apiKey){if(forceBrowser){if(!process.stdin.isTTY)throw new CliError(messages.AUTH_BROWSER_NON_INTERACTIVE);return"browser"}if(apiKey)return"api-key";if(!process.stdin.isTTY)throw new CliError(messages.AUTH_BROWSER_NON_INTERACTIVE);let{chosen}=await import_inquirer7.default.prompt([{type:"list",name:"chosen",message:messages.AUTH_PROMPT_METHOD,choices:indentChoices([{name:"Browser (sign in through your browser)",value:"browser"},{name:"API key (paste from your Brevo dashboard)",value:"api-key"}]),default:"browser"}]);return chosen}async function retryApiKeyValidation(quiet){let retryKey=await promptApiKey(),retrySpinner=createSpinner("Validating API key...",{silent:quiet});try{let account=await accountService.validateApiKey(retryKey);return retrySpinner.stop(),{account,apiKey:retryKey}}catch(retryErr){throw retrySpinner.stop(),retryErr instanceof ApiError&&retryErr.statusCode===401?new CliError(messages.AUTH_INVALID_KEY,EXIT_CODES.AUTH_FAILURE):retryErr}}async function validateApiKeyWithRetry(apiKey,quiet){let spinner=createSpinner("Validating API key...",{silent:quiet});try{let account=await accountService.validateApiKey(apiKey);return spinner.stop(),{account,apiKey}}catch(err){if(spinner.stop(),!(err instanceof ApiError&&err.statusCode===401)||(logError(messages.AUTH_INVALID_KEY),quiet||logInfo(` ${messages.AUTH_GET_KEY_URL}`),!process.stdin.isTTY))throw err;return retryApiKeyValidation(quiet)}}async function loginWithApiKey(envApiKey,quiet){let apiKey=envApiKey;if(apiKey||(openBrowser(BREVO_DASHBOARD_API_KEYS_URL),quiet||process.stdout.write(messages.AUTH_HINT(BREVO_DASHBOARD_API_KEYS_URL,BREVO_API_KEY_DOCS_URL)),apiKey=await promptApiKey()),!apiKey)throw new CliError("No API key provided.");let validated=await validateApiKeyWithRetry(apiKey,quiet);if(!validated.account)throw new CliError("Authentication failed.");return wipeAppsCacheIfAccountChanged(validated.account.organization_id),saveCredentials(validated.apiKey,{email:validated.account.email,organizationId:validated.account.organization_id,userId:validated.account.user_id}),validated.account}async function loginWithBrowser(quiet){quiet||logInfo(` ${messages.AUTH_BROWSER_OPENING}`);let tokens=await runBrowserLoginFlow({proxyUrl:OAUTH_PROXY_URL,openBrowser,onWaiting:url=>{quiet||(logInfo(` ${messages.AUTH_BROWSER_FALLBACK_URL(url)}`),logInfo(` ${messages.AUTH_BROWSER_WAITING}`))}}),tokensToStore={accessToken:tokens.accessToken,refreshToken:tokens.refreshToken,expiresIn:tokens.expiresIn,tokenType:tokens.tokenType,scope:tokens.scope};saveOauthCredentials(tokensToStore),quiet||logSuccess(messages.AUTH_BROWSER_TOKENS_RECEIVED(getCredentialsPath()));let spinner=createSpinner("Finishing login...",{silent:quiet}),account;try{account=await client.getWithBearer(ENDPOINTS.ACCOUNT,tokens.accessToken,tokens.tokenType)}catch(err){throw err instanceof ApiError&&err.statusCode===401&&clearCredentials(),err}finally{spinner.stop()}if(!account)throw new CliError("Authentication failed.");return wipeAppsCacheIfAccountChanged(account.organization_id),saveOauthCredentials(tokensToStore,{email:account.email,organizationId:account.organization_id,userId:account.user_id}),account}async function showNextSteps(){let apps=[],appsSpinner=createSpinner("Checking your apps...");try{apps=await appService.fetchAppsList()}catch{}finally{appsSpinner.stop()}if(apps.length>0){printBox("What's next?",[CLI.APP_CREATE,CLI.APP_LIST,CLI.APP_SCAFFOLD,CLI.APP_CREDENTIALS()]);return}if(!process.stdin.isTTY){logInfo(`
141
+ `):""}function buildTemplateVars(appId,ctx,targetDir){let appName=(ctx.appDetails?.name||path4.basename(targetDir)).replaceAll(/["\\\n\r\t]/g,"").trim()||"my-app",remoteScopes=ctx.appDetails?.scopes,legacyAllSubstituted=!ctx.uiApp&&containsLegacyAllScope(remoteScopes),granularScopes=(remoteScopes??[]).filter(s=>s!==LEGACY_ALL_SCOPE),scopes;ctx.uiApp?scopes=[]:scopes=granularScopes.length>0?granularScopes:[...DEFAULT_SCOPES];let slug=computeSlug(ctx.appDetails?.name);return{vars:{"{{APP_NAME}}":appName,"{{APP_SLUG}}":slug,"{{APP_ID}}":String(appId),"{{CLIENT_ID}}":ctx.clientId,"{{CLIENT_SECRET}}":ctx.clientSecret,"{{REDIRECT_URI}}":ctx.redirectUri,"{{REDIRECT_URLS_JSON}}":JSON.stringify(ctx.redirectUris),"{{SCOPES_JSON}}":JSON.stringify(scopes),"{{DISTRIBUTION}}":ctx.appDetails?.distribution_type??"private","{{LOGO_URI}}":ctx.appDetails?.logo_uri??"","{{APP_VERSION}}":ctx.appDetails?.version??"","{{OAUTH_BASE}}":OAUTH_BASE,"{{OAUTH_REALM}}":OAUTH_REALM,"{{UI_APP_JSON}}":renderUiAppJson(ctx.uiApp)},scopes,legacyAllSubstituted}}function runBaseScaffold(appId,ctx,targetDir,mergeOnly){let{vars,scopes,legacyAllSubstituted}=buildTemplateVars(appId,ctx,targetDir),files=loadBaseTemplates(vars);return{written:writeScaffoldFiles(files,targetDir,mergeOnly),legacyAllSubstituted,scopes,files}}async function resolveFeatureConflict(featureType,appId,ctx,targetDir,opts){if(opts.overwrite)return"overwrite";let{vars}=buildTemplateVars(appId,ctx,targetDir);if(!loadFeatureTemplates(featureType,vars).some(f=>fs4.existsSync(path4.join(targetDir,f.name)))||opts.jsonMode)return"merge";let{action}=await import_inquirer3.default.prompt([{type:"list",name:"action",message:messages.APP_SCAFFOLD_FEATURE_EXISTS,choices:indentChoices([{name:messages.APP_SCAFFOLD_FEATURE_EXISTS_OVERWRITE,value:"overwrite"},{name:messages.APP_SCAFFOLD_FEATURE_EXISTS_MERGE,value:"merge"},{name:messages.APP_SCAFFOLD_FEATURE_EXISTS_CANCEL,value:"cancel"}])}]);return action}function runFeatureScaffold(featureType,appId,ctx,targetDir,mergeOnly){let{vars}=buildTemplateVars(appId,ctx,targetDir);featureType==="oauth"&&fs4.mkdirSync(path4.join(targetDir,"src","oauth"),{recursive:!0});let files=loadFeatureTemplates(featureType,vars);return{written:writeScaffoldFiles(files,targetDir,mergeOnly),files}}function reportBaseScaffoldSuccess(result){logSuccess(messages.APP_CREATE_BASE_SUCCESS(result.written,result.files.length)),result.legacyAllSubstituted&&logWarn(messages.LEGACY_ALL_SCOPE_SCAFFOLD_SUBSTITUTED(result.scopes.join(", "))),printFileTree(result.files.map(f=>f.name))}function reportScaffoldSuccess(result){logSuccess(messages.APP_SCAFFOLD_SUCCESS(result.written,result.files.length)),result.legacyAllSubstituted&&logWarn(messages.LEGACY_ALL_SCOPE_SCAFFOLD_SUBSTITUTED(result.scopes.join(", "))),printFileTree(result.files.map(f=>f.name)),printBox(messages.APP_SCAFFOLD_NEXT_STEPS_TITLE,messages.APP_SCAFFOLD_NEXT_STEPS_LINES(result.cdDir)),logInfo(messages.APP_SCAFFOLD_SCOPES_TIP)}function computeCdHint(originalCwd,targetDir){return path4.relative(originalCwd,targetDir)||void 0}var import_inquirer4=__toESM(require("inquirer"));function featureTypes(){return Object.keys(FEATURE_TEMPLATE_MANIFESTS)}function soleFeatureType(){let types=featureTypes();return types.length===1?types[0]:void 0}var FALLBACK_FEATURE="oauth";function soleFeatureLabel(){let only=soleFeatureType();return only?FEATURE_LABELS[only]:void 0}async function promptFeatureType(interactive){let types=featureTypes(),only=soleFeatureType();if(only)return only;if(!interactive)return types[0]??FALLBACK_FEATURE;let{featureType}=await import_inquirer4.default.prompt([{type:"list",name:"featureType",message:messages.APP_SCAFFOLD_FEATURE_TYPE_PROMPT,choices:indentChoices(types.map(type=>({name:FEATURE_LABELS[type],value:type})))}]);return featureType}async function promptScaffoldFeature(){let{scaffoldRaw}=await import_inquirer4.default.prompt([{type:"input",name:"scaffoldRaw",message:messages.APP_SCAFFOLD_FEATURE_CONFIRM(soleFeatureLabel())+" (Y/n)",default:"y",validate:validateYesNo}]),val=String(scaffoldRaw).toLowerCase().trim();return val===""||val.startsWith("y")}async function finishProject(params){let{appId,ctx,targetDir,cdDir,isUiApp}=params;if(isUiApp)return printBox(messages.APP_SCAFFOLD_NEXT_STEPS_TITLE,messages.APP_CREATE_UI_NEXT(cdDir)),{cancelled:!1,feature:null,written:0};if(!(params.offerFeature&&await promptScaffoldFeature()))return logInfo(messages.APP_SCAFFOLD_SCOPES_TIP),printBox(messages.APP_SCAFFOLD_NEXT_STEPS_TITLE,messages.APP_CREATE_BASE_ONLY_NEXT(cdDir)),{cancelled:!1,feature:null,written:0};let feature=await promptFeatureType(!0),mergeOnly;if(params.onConflict==="ask"){let choice=await resolveFeatureConflict(feature,appId,ctx,targetDir,{jsonMode:!!params.jsonMode,overwrite:!!params.overwriteFlag});if(choice==="cancel")return logInfo(messages.APP_SCAFFOLD_CANCELLED),{cancelled:!0};mergeOnly=choice==="merge"}else mergeOnly=params.onConflict==="merge";let feat=runFeatureScaffold(feature,appId,ctx,targetDir,mergeOnly);return reportScaffoldSuccess({written:feat.written,legacyAllSubstituted:!1,scopes:params.baseScopes,files:feat.files,targetDir,cdDir}),{cancelled:!1,feature,written:feat.written}}var import_inquirer5=__toESM(require("inquirer"));var NONE="(none)",VALUE_ROWS=[{label:"label: ",read:e=>e.label},{label:"more info: ",read:e=>e.more_info},{label:"redirect link: ",read:e=>e.redirect_link},{label:"modal URL: ",read:e=>e.modal_iframe_url},{label:"card size: ",read:e=>formatSize(e.size)}];function formatSize(size){if(!size)return;let axes=[...size.width?[`width ${size.width}`]:[],...size.height?[`height ${size.height}`]:[]];return axes.length?axes.join(", "):void 0}function formatContext(entry){return entry.context?.length?` (context: ${entry.context.join(", ")})`:""}function formatPlacementLines(uiApp){return(uiApp.surface_point_list??[]).flatMap(entry=>[`${entry.surface_point_name}${formatContext(entry)}`,...VALUE_ROWS.flatMap(({label,read})=>{let value=read(entry);return value?[` ${label}${value}`]:[]})])}function formatPlacementDiffLines(next,current){if(!current)return formatPlacementLines(next);let currentEntries=current.surface_point_list??[],nextEntries=next.surface_point_list??[],before=new Map(currentEntries.map(entry=>[entry.surface_point_name,entry])),nextNames=new Set(nextEntries.map(entry=>entry.surface_point_name));return[...nextEntries.flatMap(entry=>{let previous=before.get(entry.surface_point_name);if(!previous){let[slot,...rest]=formatPlacementLines({surface_point_list:[entry]});return[`${slot} (new)`,...rest]}return[`${entry.surface_point_name}${diffContext(previous,entry)}`,...VALUE_ROWS.flatMap(({label,read})=>{let from=read(previous),to=read(entry);return from===to?to?[` ${label}${to}`]:[]:[` ${label}${from??NONE} \u2192 ${to??NONE}`]})]}),...currentEntries.filter(entry=>!nextNames.has(entry.surface_point_name)).map(entry=>`${entry.surface_point_name} (removed)`)]}function diffContext(previous,entry){let from=previous.context??[],to=entry.context??[];return from.join(",")===to.join(",")?formatContext(entry):` (context: ${from.length?from.join(", "):NONE} \u2192 ${to.length?to.join(", "):NONE})`}var PLACEMENT_QUESTION_PREFIX="placement:";function toUsableRows(rows){let usable=[];for(let row of rows){let segments=row.extension_point_name.split("."),[locationToken,placeToken,kindToken]=segments.length===3?segments:["","",""],location=(row.location_name??"").trim()||locationToken,section=(row.section_name??"").trim()||placeToken,component=(row.component_type??"").trim()||kindToken,slug=(row.surface_point_name??"").trim();!location||!section||!component||!slug||usable.push({...row,location_name:location,section_name:section,component_type:component,surface_point_name:slug})}return usable}function rowSupportsExtensionType(row,extensionType){if(row.status?.trim()&&row.status.trim()!=="active")return!1;let types=row.extension_type_list;return!types||types.length===0?!0:types.includes(extensionType)}async function fetchRecordPageLocations(extensionType){let spinner=createSpinner(messages.APP_CREATE_UI_PAGES_SPINNER),locations;try{locations=await appService.fetchSurfacePointLocations(extensionType)}catch{throw new CliError(messages.APP_CREATE_UI_POINTS_FETCH_FAILED)}finally{spinner.stop()}if(locations.length===0)throw new CliError(messages.APP_CREATE_UI_POINTS_EMPTY);return locations}async function readSurfacePointRows(locations,extensionType){try{return await appService.fetchSurfacePoints(locations,extensionType)}catch{return null}}async function fetchSurfacePointsForPages(locations,extensionType){let onPickedPages=rows=>toUsableRows(rows).filter(row=>locations.includes(row.location_name)),pagesCovered=rows=>new Set(rows.map(row=>row.location_name)).size,spinner=createSpinner(messages.APP_CREATE_UI_POINTS_SPINNER),usable;try{let narrowed=await readSurfacePointRows(locations,extensionType);if(usable=onPickedPages(narrowed??[]),narrowed===null||pagesCovered(usable)<locations.length){let unfiltered=await readSurfacePointRows();if(unfiltered===null&&narrowed===null)throw new CliError(messages.APP_CREATE_UI_POINTS_FETCH_FAILED);let fallback=onPickedPages(unfiltered??[]);pagesCovered(fallback)>pagesCovered(usable)&&(usable=fallback)}}finally{spinner.stop()}let hostable=usable.filter(row=>rowSupportsExtensionType(row,extensionType));if(hostable.length===0)throw new CliError(usable.length>0?messages.APP_CREATE_UI_POINTS_NONE_FOR_TYPE(extensionType):messages.APP_CREATE_UI_POINTS_EMPTY);return hostable}function placementLabel(row){return`${row.section_name} \u2014 ${row.component_type}`}async function promptSurfacePoint(locations,extensionType){let{surface}=await import_inquirer5.default.prompt([{type:"list",name:"surface",message:messages.APP_CREATE_UI_SURFACE_PROMPT,choices:indentChoices(locations.map(location=>({name:location,value:location})))}]),page=locations.find(location=>location===String(surface??"").trim()),forPage=(await fetchSurfacePointsForPages(page?[page]:[],extensionType)).filter(row=>row.location_name===page),question=`${PLACEMENT_QUESTION_PREFIX}${page}`,answer=await import_inquirer5.default.prompt([{type:"list",name:question,message:messages.APP_CREATE_UI_PLACEMENT_PAGE_PROMPT(page??""),choices:indentChoices(forPage.map(row=>({name:placementLabel(row),value:row.surface_point_name})))}]),chosen=String(answer[question]??"").trim();return forPage.filter(row=>row.surface_point_name===chosen)}async function promptIntegrationType(){let{integrationType}=await import_inquirer5.default.prompt([{type:"list",name:"integrationType",message:messages.APP_CREATE_UI_INTEGRATION_PROMPT,choices:indentChoices([{name:messages.APP_CREATE_UI_INTEGRATION_EXTERNAL_LINK,value:EXTENSION_TYPE_ACTION_LINK}])}]);return integrationType}async function resolveUiApp(){let extensionType=await promptIntegrationType(),locations=await fetchRecordPageLocations(extensionType),selectedRows=await promptSurfacePoint(locations,extensionType),{label}=await import_inquirer5.default.prompt([{type:"input",name:"label",message:messages.APP_CREATE_UI_LABEL_PROMPT,validate:validateUiAppLabel}]),{more_info}=await import_inquirer5.default.prompt([{type:"input",name:"more_info",message:messages.APP_CREATE_UI_MORE_INFO_PROMPT,validate:validateUiAppMoreInfo}]),{url}=await import_inquirer5.default.prompt([{type:"input",name:"url",message:messages.APP_CREATE_UI_REDIRECT_LINK_PROMPT,validate:validateUiAppUrl}]),uiApp={extension_type:extensionType,surface_point_list:buildSurfacePointList(selectedRows,{contextFor:row=>row.default_context_field??[],sizeFor:row=>row.default_size??void 0,label:String(label??"").trim(),more_info:String(more_info??"").trim(),redirect_link:String(url??"").trim()})};return validateUiApp(uiApp),uiApp}async function resolveUiAppNonInteractive(input){if(input.extensionType!==EXTENSION_TYPE_ACTION_LINK)throw new CliError(messages.APP_CREATE_UI_NONINTERACTIVE_EXTENSION_TYPE(input.extensionType));let locations=await fetchRecordPageLocations(input.extensionType);if(!locations.includes(input.recordPage))throw new CliError(messages.APP_CREATE_UI_NONINTERACTIVE_UNKNOWN_RECORD_PAGE(input.recordPage,locations));let forPage=(await fetchSurfacePointsForPages([input.recordPage],input.extensionType)).filter(row=>row.location_name===input.recordPage),matched=forPage.filter(row=>row.surface_point_name===input.placement);if(matched.length===0)throw new CliError(messages.APP_CREATE_UI_NONINTERACTIVE_UNKNOWN_PLACEMENT(input.placement,input.recordPage,forPage.map(row=>row.surface_point_name)));let uiApp={extension_type:input.extensionType,surface_point_list:buildSurfacePointList(matched,{contextFor:row=>row.default_context_field??[],sizeFor:row=>row.default_size??void 0,label:input.label.trim(),more_info:input.moreInfo.trim(),redirect_link:input.url.trim()})};return validateUiApp(uiApp),uiApp}function buildSurfacePointList(rows,fields){let entries=[],seen=new Set;for(let row of rows){if(seen.has(row.surface_point_name))continue;seen.add(row.surface_point_name);let context=fields.contextFor(row).map(field=>String(field).trim()).filter(Boolean),size=sanitizeSeededSize(fields.sizeFor(row));entries.push({surface_point_name:row.surface_point_name,...context.length?{context}:{},...size?{size}:{},label:fields.label,...fields.more_info?{more_info:fields.more_info}:{},redirect_link:fields.redirect_link})}return entries}function sanitizeSeededSize(raw){if(!raw||typeof raw!="object")return;let width=typeof raw.width=="string"?raw.width.trim():"",height=typeof raw.height=="string"?raw.height.trim():"";if(!(!width&&!height))return{...width?{width}:{},...height?{height}:{}}}function buildExampleContextUrl(redirectLink,context){let url;try{url=new URL(redirectLink)}catch{return null}for(let field of context)url.searchParams.set(field,field.replaceAll(/([a-z0-9])([A-Z])/g,"$1_$2").toUpperCase());return url.toString()}function renderExampleContextUrlLines(uiApp){let withContext=uiApp.surface_point_list.find(entry=>entry.context?.length&&entry.redirect_link);if(!withContext)return[];let example=buildExampleContextUrl(withContext.redirect_link,withContext.context??[]);return example?["",`${messages.APP_CREATE_UI_BOX_EXAMPLE_URL_LABEL}`,` ${example}`,messages.APP_CREATE_UI_BOX_EXAMPLE_URL_NOTE]:[]}function renderCreatedUiApp(result,appName,uiApp,logoUri){let boxLines=[`App name: ${appName}`,`App ID: ${result.app_id}`,`Extension type: ${uiApp.extension_type}`,...formatPlacementLines(uiApp).map((line,i)=>`${i===0?"Placement: ":" "}${line}`),...logoUri?[`Logo URL: ${logoUri}`]:[],...result.version?[`App version: ${result.version}`]:[],...renderExampleContextUrlLines(uiApp),"",messages.APP_CREATE_UI_BOX_LABEL_NOTE(uiApp.surface_point_list[0]?.label??"",appName),messages.APP_CREATE_UI_BOX_HINT];printBox(messages.APP_CREATE_UI_BOX_TITLE,boxLines)}function validateHttpUrl(trimmed,invalidMessage){try{let parsed=new URL(trimmed);return parsed.protocol!=="http:"&&parsed.protocol!=="https:"?invalidMessage:!0}catch{return invalidMessage}}var validateRedirectUrl=input=>{let trimmed=input.trim();return trimmed?validateHttpUrl(trimmed,messages.APP_CREATE_REDIRECT_INVALID):messages.APP_CREATE_REDIRECT_EMPTY},validateLogoUrl=input=>{let trimmed=input.trim();return trimmed?validateHttpUrl(trimmed,messages.APP_CREATE_LOGO_INVALID):!0};function guardAgainstLinkedApp(){if(!hasLocalApp())return;let projectConfig=readProjectConfig(),linkedName=projectConfig?.appName||String(projectConfig?.appId??"");throw new CliError(messages.APP_CREATE_ALREADY_LINKED(linkedName))}async function resolveAppName(nameFlag){if(nameFlag){let nameCheck=validateAppName(nameFlag);if(nameCheck!==!0)throw new CliError(nameCheck);return nameFlag}return(await import_inquirer6.default.prompt([{type:"input",name:"name",message:messages.APP_CREATE_NAME_PROMPT,validate:validateAppName}])).name}function stringField(value){return typeof value=="string"?value:""}function parseUiConfigFile(configPath){let raw;try{raw=fs5.readFileSync(configPath,"utf-8")}catch(err){throw new CliError(messages.APP_CREATE_UI_NONINTERACTIVE_CONFIG_INVALID(configPath,err instanceof Error?err.message:String(err)))}let parsed;try{parsed=JSON.parse(raw)}catch(err){throw new CliError(messages.APP_CREATE_UI_NONINTERACTIVE_CONFIG_INVALID(configPath,err instanceof Error?err.message:String(err)))}return{extensionType:stringField(parsed.extension_type),recordPage:stringField(parsed.record_page),placement:stringField(parsed.surface_point_name),label:stringField(parsed.label),moreInfo:stringField(parsed.more_info),url:stringField(parsed.redirect_link)}}function buildUiAppInputFromFlags(opts){let missing=[["recordPage","--record-page"],["placement","--placement"],["label","--label"],["url","--url"]].filter(([key])=>!opts[key]).map(([,flag])=>flag);if(missing.length>0)throw new CliError(messages.APP_CREATE_UI_NONINTERACTIVE_MISSING_FLAGS(missing));return{extensionType:EXTENSION_TYPE_ACTION_LINK,recordPage:opts.recordPage,placement:opts.placement,label:opts.label,moreInfo:opts.moreInfo??"",url:opts.url}}function resolveUiAppNonInteractiveInput(opts){let hasConfig=!!opts.uiConfig,hasFlags=!!opts.uiApp;if(!(!hasConfig&&!hasFlags)){if(hasConfig&&hasFlags)throw new CliError(messages.APP_CREATE_UI_NONINTERACTIVE_BOTH_INPUTS);if((opts.redirectUri?.length??0)>0)throw new CliError(messages.APP_CREATE_UI_NONINTERACTIVE_OAUTH_FLAG("--redirect-uri"));return hasConfig?parseUiConfigFile(opts.uiConfig):buildUiAppInputFromFlags(opts)}}async function resolveAppType(interactive){if(!interactive)return"oauth";let choices=[{name:messages.APP_CREATE_APP_TYPE_OAUTH,value:"oauth"}];return isFeatureAvailable("ui-app-type")&&choices.push({name:messages.APP_CREATE_APP_TYPE_UI,value:"ui"}),(await import_inquirer6.default.prompt([{type:"list",name:"appType",message:messages.APP_CREATE_APP_TYPE_PROMPT,choices:indentChoices(choices)}])).appType}function assertDistributionFlag(distributionFlag){validateEnum(distributionFlag,["private","public"],"--distribution"),distributionFlag==="public"&&assertFeatureAvailable("public-distribution")}async function resolveDistribution(distributionFlag,interactive){if(distributionFlag)return distributionFlag;if(!interactive)return"private";let choices=[{name:"Private (Used exclusively by your organisation)",value:"private"}];return(await import_inquirer6.default.prompt([{type:"list",name:"distribution",message:messages.APP_CREATE_TYPE_PROMPT,choices:indentChoices(choices)}])).distribution}async function promptAddAnotherRedirect(){let{anotherRaw}=await import_inquirer6.default.prompt([{type:"input",name:"anotherRaw",message:messages.APP_CREATE_REDIRECT_ANOTHER+" (y/N)",default:"n",validate:validateYesNo}]);return String(anotherRaw).toLowerCase().trim().startsWith("y")}async function promptRedirectUrls(quiet){let availablePort=await findAvailablePort(DEFAULT_PORT),defaultRedirect=availablePort==null||availablePort===DEFAULT_PORT?DEFAULT_REDIRECT_URI:`http://localhost:${availablePort}/auth/callback`;quiet||(availablePort==null?logInfo(messages.APP_CREATE_PORT_SCAN_FAILED(DEFAULT_PORT)):availablePort!==DEFAULT_PORT&&logInfo(messages.APP_CREATE_PORT_IN_USE(DEFAULT_PORT,availablePort)),logInfo(messages.APP_CREATE_REDIRECT_HINT(CLI.APP_START("oauth"))));let redirectUris=[],{redirectUrl:firstUrl}=await import_inquirer6.default.prompt([{type:"input",name:"redirectUrl",message:messages.APP_CREATE_REDIRECT_PROMPT,default:defaultRedirect,validate:validateRedirectUrl}]);for(redirectUris.push(firstUrl.trim());await promptAddAnotherRedirect();){let{nextUrl}=await import_inquirer6.default.prompt([{type:"input",name:"nextUrl",message:messages.APP_CREATE_REDIRECT_PROMPT,validate:validateRedirectUrl}]);redirectUris.push(nextUrl.trim())}return redirectUris}async function resolveRedirectUrls(redirectUriFlag,quiet){let flagUrls=redirectUriFlag??[];return flagUrls.length>0?flagUrls:process.stdin.isTTY?promptRedirectUrls(quiet):[DEFAULT_REDIRECT_URI]}async function resolveLogoUri(logoUriFlag,jsonMode){if(logoUriFlag||!process.stdin.isTTY||jsonMode)return logoUriFlag;let{logoUrl}=await import_inquirer6.default.prompt([{type:"input",name:"logoUrl",message:messages.APP_CREATE_LOGO_PROMPT,validate:validateLogoUrl}]);return String(logoUrl??"").trim()||void 0}async function resolveCreateDirectory(appName,interactive){let slug=computeSlug(appName);if(!interactive){let targetDir=path5.resolve(`./${slug}`);return fs5.existsSync(targetDir)?{targetDir,skipped:!0}:{targetDir,mergeOnly:!1,skipped:!1,existed:!1}}let dir=await resolveProjectDirectory(`./${slug}`);for(;!dir.unresolved&&dir.chooseAgain;)dir=await resolveProjectDirectory(`./${slug}`);if(dir.unresolved)throw new CliError(messages.APP_CREATE_DIR_UNRESOLVED);return{targetDir:dir.targetDir,mergeOnly:dir.mergeOnly,skipped:!1,existed:dir.existed}}function applyCreateDirectory(dir,jsonMode){dir.skipped||applyProjectDirectory({targetDir:dir.targetDir,mergeOnly:dir.mergeOnly,chooseAgain:!1,existed:dir.existed},jsonMode)}function buildCreatePayload(inputs){let isUiApp=!!inputs.uiApp;return{name:inputs.appName,distribution_type:inputs.distribution,...isUiApp?{ui_app:inputs.uiApp}:{auth:{scopes:[...DEFAULT_SCOPES],redirect_uris:inputs.redirectUris}},...inputs.logoUri?{logo_uri:inputs.logoUri}:{}}}async function retryCreateWithNewName(inputs){logError(messages.APP_CREATE_NAME_TAKEN);let retry=await import_inquirer6.default.prompt([{type:"input",name:"name",message:messages.APP_CREATE_NAME_PROMPT,validate:validateAppName}]),retrySpinner=createSpinner("Creating app...");try{let result=await appService.createApp(buildCreatePayload({...inputs,appName:retry.name}));return retrySpinner.stop(),{result,appName:retry.name}}catch(retryErr){throw retrySpinner.stop(),retryErr}}async function retryCreateAfterLogin(inputs){logWarn(messages.APP_CREATE_SESSION_EXPIRED);let{relogin}=await import_inquirer6.default.prompt([{type:"confirm",name:"relogin",message:messages.APP_CREATE_RELOGIN_CONFIRM,default:!0}]);if(!relogin)throw new AuthExpiredError;if(await loginCommand({suppressNextSteps:!0}),!isAuthenticated())throw new AuthExpiredError;let spinner=createSpinner("Creating app...");try{return{result:await appService.createApp(buildCreatePayload(inputs)),appName:inputs.appName}}finally{spinner.stop()}}function isPublicDistributionRefusal(err,distribution){return err instanceof ApiError&&err.statusCode===400&&distribution==="public"&&/distribution_type/i.test(err.message)}async function createAppWithRetry(inputs,jsonMode,interactive){let spinner=createSpinner("Creating app...",{silent:jsonMode});try{let result=await appService.createApp(buildCreatePayload(inputs));return spinner.stop(),{result,appName:inputs.appName}}catch(err){if(spinner.stop(),err instanceof ApiError&&err.errorCode==="APP_LIMIT_REACHED")throw jsonMode&&jsonOutput({error:"APP_LIMIT_REACHED",message:messages.APP_CREATE_LIMIT_REACHED}),new CliError(messages.APP_CREATE_LIMIT_REACHED);if(isPublicDistributionRefusal(err,inputs.distribution))throw new CliError(messages.APP_CREATE_PUBLIC_REJECTED(err.message));if(err instanceof ApiError&&err.statusCode===409)return retryCreateWithNewName(inputs);if(err instanceof AuthExpiredError&&interactive)return retryCreateAfterLogin(inputs);throw err}}async function resolveUiAppOrRedirectUris(appType,nonInteractiveUiAppInput,redirectUriFlag,jsonMode){if(appType!=="ui")return{redirectUris:await resolveRedirectUrls(redirectUriFlag,jsonMode),uiApp:void 0};let uiApp=nonInteractiveUiAppInput?await resolveUiAppNonInteractive(nonInteractiveUiAppInput):await resolveUiApp();return{redirectUris:[],uiApp}}function cacheAppIdentity(result,finalAppName){result.client_id&&result.client_secret&&saveAppCredentials(result.app_id,{clientId:result.client_id,clientSecret:result.client_secret}),finalAppName&&saveAppName(result.app_id,finalAppName)}function buildCreateJsonBase(result,finalAppName,appType,uiApp,logoUri){return{appId:result.app_id,appName:finalAppName,clientId:result.client_id,clientSecret:messages.CLIENT_SECRET_HIDDEN_JSON,appType,...uiApp?{uiApp}:{redirectUri:result.redirect_uris},...logoUri?{logoUri}:{},...result.version?{version:result.version}:{}}}function reportSkippedDirectory(jsonMode,jsonBase,dir,renderBox2){if(jsonMode){jsonOutput({...jsonBase,directory:dir.targetDir,scaffoldSkipped:messages.APP_CREATE_JSON_SCAFFOLD_DIR_EXISTS(dir.targetDir)});return}renderBox2(),logInfo(messages.APP_CREATE_DIR_EXISTS_SKIPPED(dir.targetDir))}function buildFallbackOAuthApp(result){return{...result,client_id:result.client_id??"",redirect_uris:result.redirect_uris??null}}function renderCreatedApp(result,appName,logoUri){let boxLines=[`App name: ${appName}`,`App ID: ${result.app_id}`,`Client ID: ${result.client_id}`,`Client secret: ${messages.CLIENT_SECRET_HIDDEN_HUMAN}`,...(result.redirect_uris??[]).map((uri,i)=>`Redirect URL ${i+1}: ${uri}`),...logoUri?[`Logo URL: ${logoUri}`]:[],...result.version?[`App version: ${result.version}`]:[],`${messages.APP_CREATE_BOX_SCOPES_LABEL} ${[...DEFAULT_SCOPES].join(", ")}`,"",messages.APP_CREATE_BOX_SCOPE_HINT];printBox(messages.APP_CREATE_BOX_TITLE,boxLines)}var createCommand=withCommandHandler(async options=>{let jsonMode=!!options.json,originalCwd=process.cwd();guardAgainstLinkedApp(),assertDistributionFlag(options.distribution);let nonInteractiveUiAppInput=resolveUiAppNonInteractiveInput(options),interactive=!jsonMode&&!!process.stdin.isTTY,appName=await resolveAppName(options.name),logoUri=await resolveLogoUri(options.logoUri,jsonMode),distribution=await resolveDistribution(options.distribution,interactive),appType=nonInteractiveUiAppInput?"ui":await resolveAppType(interactive),{redirectUris,uiApp}=await resolveUiAppOrRedirectUris(appType,nonInteractiveUiAppInput,options.redirectUri,jsonMode),dir=await resolveCreateDirectory(appName,interactive),inputs={appName,distribution,redirectUris,logoUri,uiApp},{result,appName:finalAppName}=await createAppWithRetry(inputs,jsonMode,interactive);applyCreateDirectory(dir,jsonMode),cacheAppIdentity(result,finalAppName);let jsonBase=buildCreateJsonBase(result,finalAppName,appType,uiApp,logoUri),renderBox2=()=>uiApp?renderCreatedUiApp(result,finalAppName,uiApp,logoUri):renderCreatedApp(result,finalAppName,logoUri);if(dir.skipped){reportSkippedDirectory(jsonMode,jsonBase,dir,renderBox2);return}let fallbackApp=buildFallbackOAuthApp(result),ctx=await fetchAppContext(result.app_id,jsonMode,uiApp,fallbackApp),base=runBaseScaffold(result.app_id,ctx,dir.targetDir,dir.mergeOnly);if(jsonMode){jsonOutput({...jsonBase,directory:dir.targetDir,scaffolded:base.written});return}renderBox2(),reportBaseScaffoldSuccess(base),await finishProject({appId:result.app_id,ctx,targetDir:dir.targetDir,baseScopes:base.scopes,cdDir:computeCdHint(originalCwd,dir.targetDir),isUiApp:!!uiApp,offerFeature:interactive,onConflict:dir.mergeOnly?"merge":"overwrite"})});var http=__toESM(require("node:http")),import_node_crypto=require("node:crypto");var MAX_BODY_BYTES=16*1024,DEFAULT_TIMEOUT_MS=3e5;function normalizeTokens(raw){return typeof raw.access_token!="string"||!raw.access_token||typeof raw.refresh_token!="string"||!raw.refresh_token||typeof raw.expires_in!="number"||!Number.isFinite(raw.expires_in)||raw.expires_in<=0||typeof raw.token_type!="string"||!raw.token_type?null:{accessToken:raw.access_token,refreshToken:raw.refresh_token,expiresIn:raw.expires_in,tokenType:raw.token_type,scope:typeof raw.scope=="string"?raw.scope:void 0}}async function runBrowserLoginFlow(opts){let proxyOrigin=new URL(opts.proxyUrl).origin,timeoutMs=opts.timeoutMs??DEFAULT_TIMEOUT_MS,openBrowser2=opts.openBrowser??(()=>{});return new Promise((resolve11,reject)=>{let settled=!1,claimSettlement=()=>settled?!1:(settled=!0,!0),server=http.createServer((req,res)=>{let pathname=new URL(req.url??"/","http://127.0.0.1").pathname,origin=req.headers.origin;if(logDebug("loopback request",{method:req.method,url:req.url,pathname,origin}),req.method==="OPTIONS"&&pathname==="/callback"){if(origin!==proxyOrigin){logDebug("loopback OPTIONS rejected: origin mismatch",{origin,expected:proxyOrigin}),res.writeHead(403).end();return}res.writeHead(204,{"Access-Control-Allow-Origin":proxyOrigin,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type","Access-Control-Max-Age":"600"}).end();return}if(req.method==="POST"&&pathname==="/callback"){if(origin!==proxyOrigin){logDebug("loopback POST rejected: origin mismatch",{origin,expected:proxyOrigin}),res.writeHead(403).end();return}let bytes=0,chunks=[];req.on("data",chunk=>{if(bytes+=chunk.length,bytes>MAX_BODY_BYTES){logDebug("loopback POST rejected: body too large",{bytes,max:MAX_BODY_BYTES}),res.writeHead(413,{Connection:"close"}).end(),req.destroy();return}chunks.push(chunk)}),req.on("end",()=>{let parsed=null;try{parsed=JSON.parse(Buffer.concat(chunks).toString("utf-8"))}catch{parsed=null}let tokens=parsed?normalizeTokens(parsed):null;if(!tokens){logDebug("loopback POST rejected: bad payload shape",{hasParsed:parsed!==null,keys:parsed?Object.keys(parsed):null}),res.writeHead(400,{"Access-Control-Allow-Origin":proxyOrigin,"Content-Type":"text/plain"}).end("Bad payload");return}logDebug("loopback POST accepted",{hasScope:tokens.scope!==void 0}),res.writeHead(204,{"Access-Control-Allow-Origin":proxyOrigin}).end(),claimSettlement()&&(server.close(),resolve11(tokens))});return}if(req.method==="GET"&&(pathname==="/"||pathname==="/callback")){res.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),res.end('<!doctype html><meta charset="utf-8"><title>Brevo CLI login</title><p>Waiting for login to complete \u2014 you can close this tab once the CLI confirms success.</p>');return}logDebug("loopback request not matched",{method:req.method,pathname}),res.writeHead(404).end()});server.on("error",err=>{logDebug("loopback server error",{message:err.message}),claimSettlement()&&reject(err)}),server.listen(0,"127.0.0.1",()=>{let port=server.address().port,attemptToken=(0,import_node_crypto.randomUUID)(),loginUrl=`${opts.proxyUrl}/login?port=${port}&t=${attemptToken}`;logDebug("loopback listening",{host:"127.0.0.1",port,proxyOrigin}),opts.onWaiting?.(loginUrl);try{openBrowser2(loginUrl)}catch{}});let timer=setTimeout(()=>{claimSettlement()&&(server.close(),reject(new CliError(messages.AUTH_BROWSER_TIMEOUT)))},timeoutMs);timer.unref?.(),server.on("close",()=>clearTimeout(timer))})}function wipeAppsCacheIfAccountChanged(newOrganizationId){let previousOrganizationId=getOrganizationId();previousOrganizationId&&previousOrganizationId!==newOrganizationId&&clearAppsCache()}async function promptApiKey(){let{key}=await import_inquirer7.default.prompt([{type:"password",name:"key",message:messages.AUTH_PROMPT_API_KEY,mask:"*",validate:input=>input.trim().length>0||"API key cannot be empty"}]);return key}async function resolveLoginMethod(forceBrowser,apiKey){if(forceBrowser){if(!process.stdin.isTTY)throw new CliError(messages.AUTH_BROWSER_NON_INTERACTIVE);return"browser"}if(apiKey)return"api-key";if(!process.stdin.isTTY)throw new CliError(messages.AUTH_BROWSER_NON_INTERACTIVE);let{chosen}=await import_inquirer7.default.prompt([{type:"list",name:"chosen",message:messages.AUTH_PROMPT_METHOD,choices:indentChoices([{name:"Browser (sign in through your browser)",value:"browser"},{name:"API key (paste from your Brevo dashboard)",value:"api-key"}]),default:"browser"}]);return chosen}async function retryApiKeyValidation(quiet){let retryKey=await promptApiKey(),retrySpinner=createSpinner("Validating API key...",{silent:quiet});try{let account=await accountService.validateApiKey(retryKey);return retrySpinner.stop(),{account,apiKey:retryKey}}catch(retryErr){throw retrySpinner.stop(),retryErr instanceof ApiError&&retryErr.statusCode===401?new CliError(messages.AUTH_INVALID_KEY,EXIT_CODES.AUTH_FAILURE):retryErr}}async function validateApiKeyWithRetry(apiKey,quiet){let spinner=createSpinner("Validating API key...",{silent:quiet});try{let account=await accountService.validateApiKey(apiKey);return spinner.stop(),{account,apiKey}}catch(err){if(spinner.stop(),!(err instanceof ApiError&&err.statusCode===401)||(logError(messages.AUTH_INVALID_KEY),quiet||logInfo(` ${messages.AUTH_GET_KEY_URL}`),!process.stdin.isTTY))throw err;return retryApiKeyValidation(quiet)}}async function loginWithApiKey(envApiKey,quiet){let apiKey=envApiKey;if(apiKey||(openBrowser(BREVO_DASHBOARD_API_KEYS_URL),quiet||process.stdout.write(messages.AUTH_HINT(BREVO_DASHBOARD_API_KEYS_URL,BREVO_API_KEY_DOCS_URL)),apiKey=await promptApiKey()),!apiKey)throw new CliError("No API key provided.");let validated=await validateApiKeyWithRetry(apiKey,quiet);if(!validated.account)throw new CliError("Authentication failed.");return wipeAppsCacheIfAccountChanged(validated.account.organization_id),saveCredentials(validated.apiKey,{email:validated.account.email,organizationId:validated.account.organization_id,userId:validated.account.user_id}),validated.account}async function loginWithBrowser(quiet){quiet||logInfo(` ${messages.AUTH_BROWSER_OPENING}`);let tokens=await runBrowserLoginFlow({proxyUrl:OAUTH_PROXY_URL,openBrowser,onWaiting:url=>{quiet||(logInfo(` ${messages.AUTH_BROWSER_FALLBACK_URL(url)}`),logInfo(` ${messages.AUTH_BROWSER_WAITING}`))}}),tokensToStore={accessToken:tokens.accessToken,refreshToken:tokens.refreshToken,expiresIn:tokens.expiresIn,tokenType:tokens.tokenType,scope:tokens.scope};saveOauthCredentials(tokensToStore),quiet||logSuccess(messages.AUTH_BROWSER_TOKENS_RECEIVED(getCredentialsPath()));let spinner=createSpinner("Finishing login...",{silent:quiet}),account;try{account=await client.getWithBearer(ENDPOINTS.ACCOUNT,tokens.accessToken,tokens.tokenType)}catch(err){throw err instanceof ApiError&&err.statusCode===401&&clearCredentials(),err}finally{spinner.stop()}if(!account)throw new CliError("Authentication failed.");return wipeAppsCacheIfAccountChanged(account.organization_id),saveOauthCredentials(tokensToStore,{email:account.email,organizationId:account.organization_id,userId:account.user_id}),account}async function showNextSteps(){let apps=[],appsSpinner=createSpinner("Checking your apps...");try{apps=await appService.fetchAppsList()}catch{}finally{appsSpinner.stop()}if(apps.length>0){printBox("What's next?",[CLI.APP_CREATE,CLI.APP_LIST,CLI.APP_SCAFFOLD,CLI.APP_CREDENTIALS()]);return}if(!process.stdin.isTTY){logInfo(`
141
142
  ${messages.AUTH_NEXT}
142
143
  `);return}process.stdout.write(`
143
144
  `);let{shouldCreate}=await import_inquirer7.default.prompt([{type:"confirm",name:"shouldCreate",message:messages.AUTH_CREATE_APP_PROMPT,default:!0}]);if(shouldCreate){process.stdout.write(`
@@ -672,7 +673,7 @@ footer a { color: var(--accent); }
672
673
  `)}}};function shouldSkipAutoRefresh(opts={}){let env=opts.env??process.env,argv=opts.argv??process.argv;return!!(env.CI==="true"||env.CI==="1"||argv.includes("--json")||argv.length>2&&argv[2]==="skill:cli"||env.BREVO_NO_SKILL_AUTOREFRESH==="1"||env.BREVO_NO_SKILL_AUTOREFRESH==="true")}function unknownSkillMessage(name){let available=SKILL_CATALOG.map(s=>s.name).join(", ");return`Unknown skill "${name}". Available: ${available}`}var installCommand=withCommandHandler(async options=>{let results=skillService.installAll();if(options.json){jsonOutput(results);return}let installedFresh=!1;for(let r of results)r.status==="already-installed"?logInfo(`
673
674
  ${messages.SKILL_INSTALL_ALREADY(r.name,r.version)}`):(logSuccess(messages.SKILL_INSTALL_SUCCESS(r.name,r.version,r.path)),installedFresh=!0);installedFresh&&logInfo(`
674
675
  ${messages.SKILL_INSTALL_CLAUDE_ONLY}`)});var uninstallCommand=withCommandHandler(async options=>{let results=skillService.uninstallAll();if(options.json){jsonOutput(results.map(r=>({uninstalled:!0,...r})));return}if(results.length===0){logInfo(`
675
- ${messages.SKILL_UNINSTALL_NONE}`);return}for(let r of results)logSuccess(messages.SKILL_UNINSTALL_SUCCESS(r.name,r.path))});var topLevelCommands=[{name:"login",description:"Authenticate with your Brevo account",options:[{flags:"--browser",description:"Force browser-based login"},{flags:"--json",description:"Output as JSON"}],examples:["brevo login","brevo login --browser","BREVO_API_KEY=xkeysib-... brevo login"],handler:opts=>loginCommand({browser:!!opts.browser,json:!!opts.json})},{name:"logout",description:"Clear stored credentials",options:[{flags:"--force",description:"Skip confirmation (for CI)"},{flags:"--json",description:"Output as JSON"}],handler:opts=>logoutCommand({force:!!opts.force,json:!!opts.json})},{name:"whoami",description:"Show current authenticated user",options:[{flags:"--json",description:"Output as JSON"}],handler:opts=>whoamiCommand({json:!!opts.json})}],appCommandGroup={name:"app",description:"Manage OAuth applications",commands:[{name:"init",description:"Quick setup \u2014 login, create app, and scaffold in one go",examples:["brevo app init"],handler:()=>initCommand({})},{name:"create",description:createDescription(),examples:["brevo app create",'brevo app create --name "My App" --distribution private',...isFeatureAvailable("public-distribution")?['brevo app create --name "My App" --distribution public']:[],'brevo app create --name "My App" --distribution private --redirect-uri http://localhost:3009/auth/callback','brevo app create --name "My App" --distribution private --redirect-uri http://localhost:3009/auth/callback --redirect-uri https://myapp.com/callback --json','brevo app create --name "My App" --distribution private --logo-uri https://example.com/logo.png'],options:[{flags:"--name <name>",description:"App name"},{flags:"--distribution <type>",description:`Distribution type (${distributionValues()})`},{flags:"--redirect-uri <url>",description:"Redirect URI (repeatable, OAuth apps only)",parser:collectUrls},{flags:"--logo-uri <url>",description:"App logo URL (http or https)",parser:v=>(validateUrl(v,"logo URL"),v)},{flags:"--json",description:"Output as JSON"}],handler:opts=>createCommand({name:opts.name,distribution:opts.distribution,redirectUri:opts.redirectUri,logoUri:opts.logoUri,json:!!opts.json})},{name:"list",description:"List all apps in your account",examples:["brevo app list","brevo app list --json"],options:[{flags:"--json",description:"Output as JSON"}],handler:opts=>listCommand({json:!!opts.json})},{name:"credentials",description:"Show client ID and secret for an app",examples:[`brevo app credentials --app-id ${EXAMPLE_APP_ID}`,`brevo app credentials --app-id ${EXAMPLE_APP_ID} --reveal-secret --json`],options:[{flags:"--app-id <id>",description:"App ID",parser:v=>parseAppId(v)},{flags:"--reveal-secret",description:"Show the client secret"},{flags:"--json",description:"Output as JSON"}],handler:opts=>credentialsCommand({appId:opts.appId,revealSecret:!!opts.revealSecret,json:!!opts.json})},{name:"upload",description:"Push app-config.json to Brevo, validated and synced with the server",examples:["brevo app upload","brevo app upload --yes","brevo app upload --json"],options:[{flags:"--yes",description:"Skip confirmation prompt"},{flags:"--json",description:"Output as JSON"}],handler:opts=>uploadCommand({yes:!!opts.yes,json:!!opts.json})},{name:"delete",description:"Delete an app",examples:[`brevo app delete --app-id ${EXAMPLE_APP_ID}`,`brevo app delete --app-id ${EXAMPLE_APP_ID} --force`],options:[{flags:"--app-id <id>",description:"App ID",parser:v=>parseAppId(v)},{flags:"--force",description:"Skip confirmation (for CI)"},{flags:"--json",description:"Output as JSON"}],handler:opts=>deleteCommand({appId:opts.appId,force:!!opts.force,json:!!opts.json})},{name:"scaffold",description:"Add a feature to the app in this directory, or set an empty directory up for an existing app",examples:["brevo app scaffold",`brevo app scaffold --app-id ${EXAMPLE_APP_ID}`,"brevo app scaffold --overwrite","brevo app scaffold --json",`brevo app scaffold --app-id ${EXAMPLE_APP_ID} --json`],options:[{flags:"--app-id <id>",description:"Set an empty directory up for an app you already have",parser:v=>parseAppId(v)},{flags:"--overwrite",description:"Overwrite existing feature files instead of merging (skips the prompt)"},{flags:"--json",description:"Output as JSON"}],handler:opts=>scaffoldCommand({appId:opts.appId,json:!!opts.json,overwrite:!!opts.overwrite})},{name:"available-scopes",description:"List OAuth scopes supported by the IdP",examples:["brevo app available-scopes","brevo app available-scopes --web","brevo app available-scopes --json"],options:[{flags:"--json",description:"Output as JSON"},{flags:"--web",description:"Open the scope catalog in a local browser page"}],handler:opts=>scopesCommand({json:!!opts.json,web:!!opts.web})},{name:"start",description:"Run a scaffolded feature locally",arguments:[{name:"[feature]",description:"Feature to start (e.g. oauth)"}],examples:["brevo app start oauth","brevo app start oauth --port 3000"],options:[{flags:"--port <port>",description:"Server port (default: 3009)",parser:v=>parsePositiveInt(v,"--port")}],handler:(opts,feature)=>startCommand({feature,port:opts.port})},{name:"install",requires:"account-install",description:"Install an app into a Brevo account",arguments:[{name:"[account-id]",description:"Brevo account (tenant) ID (defaults to your own account)"}],examples:["brevo app install","brevo app install 99999",`brevo app install 99999 --app-id ${EXAMPLE_APP_ID}`,"brevo app install 99999 --force --json"],options:[{flags:"--app-id <id>",description:"App ID (uses app-config.json if omitted)",parser:v=>parseAppId(v)},{flags:"--force",description:"Skip confirmation (for CI)"},{flags:"--json",description:"Output as JSON"}],handler:(opts,accountId)=>appInstallCommand({accountId,appId:opts.appId,force:!!opts.force,json:!!opts.json})},{name:"uninstall",requires:"account-install",description:"Uninstall an app from a Brevo account",arguments:[{name:"[account-id]",description:"Brevo account (tenant) ID (defaults to your own account)"}],examples:["brevo app uninstall","brevo app uninstall 99999",`brevo app uninstall 99999 --app-id ${EXAMPLE_APP_ID}`,"brevo app uninstall 99999 --force --json"],options:[{flags:"--app-id <id>",description:"App ID (uses app-config.json if omitted)",parser:v=>parseAppId(v)},{flags:"--force",description:"Skip confirmation (for CI)"},{flags:"--json",description:"Output as JSON"}],handler:(opts,accountId)=>appUninstallCommand({accountId,appId:opts.appId,force:!!opts.force,json:!!opts.json})}]},skillCommandGroup={name:"skill:cli",description:"Install the brevo-cli Claude Code skill (Claude only)",commands:[{name:"install",description:"Install the brevo-cli skill into ~/.claude/skills/ (Claude only \u2014 other AI tools should read agent-context/AGENTS.md instead)",examples:["brevo skill:cli install","brevo skill:cli install --json"],options:[{flags:"--json",description:"Output as JSON"}],handler:opts=>installCommand({json:!!opts.json})},{name:"uninstall",description:"Remove the brevo-cli skill from ~/.claude/skills/ (Claude only)",examples:["brevo skill:cli uninstall","brevo skill:cli uninstall --json"],options:[{flags:"--json",description:"Output as JSON"}],handler:opts=>uninstallCommand({json:!!opts.json})}]};var fs10=__toESM(require("node:fs")),os4=__toESM(require("node:os")),path11=__toESM(require("node:path"));var REGISTRY_URL=name=>`https://registry.npmjs.org/${encodeURIComponent(name).replace("%40","@")}/latest`,TTL_MS=720*60*1e3,FETCH_TIMEOUT_MS=2e3,NOTIFY_WAIT_MS=1500,CACHE_FILE="update-check.json";function getCachePath(override,env=process.env){if(override)return override;let dir=env.BREVO_CONFIG_HOME||path11.join(os4.homedir(),".brevo");return path11.join(dir,CACHE_FILE)}function shouldShowBannerBefore(argv){let args2=argv.slice(2);return args2.length===0||args2.includes("--help")||args2.includes("-h")||args2.includes("--version")||args2.includes("-V")?!0:args2[0]==="app"&&(args2[1]==="init"||args2[1]==="create")}function shouldSkipCheck(opts){let env=opts.env??process.env,argv=opts.argv??process.argv,isTTY2=opts.isTTY??!!process.stdout.isTTY;return!!(env.CI==="true"||env.CI==="1"||!isTTY2||env.NO_UPDATE_NOTIFIER==="1"||env.NO_UPDATE_NOTIFIER==="true"||env.BREVO_NO_UPDATE_NOTIFIER==="1"||env.BREVO_NO_UPDATE_NOTIFIER==="true"||argv.includes("--no-update-notifier"))}function parseVersion(v){let match=/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(v.trim());if(!(!match?.[1]||!match?.[2]||!match?.[3]))return{major:Number.parseInt(match[1],10),minor:Number.parseInt(match[2],10),patch:Number.parseInt(match[3],10),prerelease:match[4]??""}}function comparePrereleaseIdentifiers(ai,bi){if(ai===bi)return 0;let aNum=/^\d+$/.test(ai),bNum=/^\d+$/.test(bi);if(aNum&&bNum){let diff=Number.parseInt(ai,10)-Number.parseInt(bi,10);return diff===0?0:diff>0?1:-1}return aNum?-1:bNum||ai>bi?1:-1}function comparePrerelease(a,b){if(a===b)return 0;let aParts=a.split("."),bParts=b.split("."),len=Math.min(aParts.length,bParts.length);for(let i=0;i<len;i++){let cmp=comparePrereleaseIdentifiers(aParts[i]??"",bParts[i]??"");if(cmp!==0)return cmp}return aParts.length===bParts.length?0:aParts.length>bParts.length?1:-1}function compareVersions(current,latest){let c=parseVersion(current),l=parseVersion(latest);return!c||!l?0:l.major!==c.major?l.major-c.major:l.minor!==c.minor?l.minor-c.minor:l.patch!==c.patch?l.patch-c.patch:c.prerelease&&!l.prerelease?1:!c.prerelease&&l.prerelease?-1:comparePrerelease(l.prerelease,c.prerelease)}function isNewer(current,latest){return compareVersions(current,latest)>0}function isMajorBehind(current,latest){let c=parseVersion(current),l=parseVersion(latest);return!c||!l?!1:l.major>c.major}function readCache(cachePath){try{let raw=JSON.parse(fs10.readFileSync(cachePath,"utf-8"));if(raw&&typeof raw=="object"&&typeof raw.latest=="string"&&typeof raw.lastChecked=="number"&&Number.isFinite(raw.lastChecked))return{latest:raw.latest,lastChecked:raw.lastChecked}}catch{}}function writeCache(cachePath,cache){try{fs10.mkdirSync(path11.dirname(cachePath),{recursive:!0,mode:448}),fs10.writeFileSync(cachePath,JSON.stringify(cache,null,2),"utf-8")}catch{}}async function fetchLatestVersion(name,opts){let fetchImpl=opts?.fetchImpl??fetch,timeoutMs=opts?.fetchTimeoutMs??FETCH_TIMEOUT_MS,controller=new AbortController,timer=setTimeout(()=>controller.abort(),timeoutMs);try{let res=await fetchImpl(REGISTRY_URL(name),{signal:controller.signal,headers:{Accept:"application/json"}});if(!res.ok)return;let json=await res.json();return typeof json.version=="string"?json.version:void 0}catch{return}finally{clearTimeout(timer)}}function renderBox(lines){let inner=Math.max(...lines.map(l=>l.length))+4,top="\u256D"+"\u2500".repeat(inner)+"\u256E",bot="\u2570"+"\u2500".repeat(inner)+"\u256F",pad=s=>" "+s+" ".repeat(inner-s.length-2);return["",` ${top}`,...lines.map(l=>` \u2502${pad(l)}\u2502`),` ${bot}`,""].join(`
676
+ ${messages.SKILL_UNINSTALL_NONE}`);return}for(let r of results)logSuccess(messages.SKILL_UNINSTALL_SUCCESS(r.name,r.path))});var topLevelCommands=[{name:"login",description:"Authenticate with your Brevo account",options:[{flags:"--browser",description:"Force browser-based login"},{flags:"--json",description:"Output as JSON"}],examples:["brevo login","brevo login --browser","BREVO_API_KEY=xkeysib-... brevo login"],handler:opts=>loginCommand({browser:!!opts.browser,json:!!opts.json})},{name:"logout",description:"Clear stored credentials",options:[{flags:"--force",description:"Skip confirmation (for CI)"},{flags:"--json",description:"Output as JSON"}],handler:opts=>logoutCommand({force:!!opts.force,json:!!opts.json})},{name:"whoami",description:"Show current authenticated user",options:[{flags:"--json",description:"Output as JSON"}],handler:opts=>whoamiCommand({json:!!opts.json})}],appCommandGroup={name:"app",description:"Manage OAuth applications",commands:[{name:"init",description:"Quick setup \u2014 login, create app, and scaffold in one go",examples:["brevo app init"],handler:()=>initCommand({})},{name:"create",description:createDescription(),examples:["brevo app create",'brevo app create --name "My App" --distribution private',...isFeatureAvailable("public-distribution")?['brevo app create --name "My App" --distribution public']:[],'brevo app create --name "My App" --distribution private --redirect-uri http://localhost:3009/auth/callback','brevo app create --name "My App" --distribution private --redirect-uri http://localhost:3009/auth/callback --redirect-uri https://myapp.com/callback --json','brevo app create --name "My App" --distribution private --logo-uri https://example.com/logo.png','brevo app create --name "My App" --ui-app --record-page contactDetails --placement contactDetails.header.menu --label "Open in Acme" --url https://example.com/open --json','brevo app create --name "My App" --ui-config ./ui-app.json --json'],options:[{flags:"--name <name>",description:"App name"},{flags:"--distribution <type>",description:`Distribution type (${distributionValues()})`},{flags:"--redirect-uri <url>",description:"Redirect URI (repeatable, OAuth apps only)",parser:collectUrls},{flags:"--logo-uri <url>",description:"App logo URL (http or https)",parser:v=>(validateUrl(v,"logo URL"),v)},{flags:"--ui-config <file>",description:"Create an actionLink UI app from a JSON file (non-interactive; see --help)"},{flags:"--ui-app",description:"Create an actionLink UI app from flags below"},{flags:"--record-page <slug>",description:"UI app record page (with --ui-app)"},{flags:"--placement <surface_point_name>",description:"UI app placement slot (with --ui-app)"},{flags:"--label <text>",description:"UI app menu/card label, max 48 chars (with --ui-app)"},{flags:"--more-info <text>",description:"UI app supporting text, max 255 chars, optional (with --ui-app)"},{flags:"--url <url>",description:"UI app destination URL (with --ui-app)"},{flags:"--json",description:"Output as JSON"}],handler:opts=>createCommand({name:opts.name,distribution:opts.distribution,redirectUri:opts.redirectUri,logoUri:opts.logoUri,uiConfig:opts.uiConfig,uiApp:!!opts.uiApp,recordPage:opts.recordPage,placement:opts.placement,label:opts.label,moreInfo:opts.moreInfo,url:opts.url,json:!!opts.json})},{name:"list",description:"List all apps in your account",examples:["brevo app list","brevo app list --json"],options:[{flags:"--json",description:"Output as JSON"}],handler:opts=>listCommand({json:!!opts.json})},{name:"credentials",description:"Show client ID and secret for an app",examples:[`brevo app credentials --app-id ${EXAMPLE_APP_ID}`,`brevo app credentials --app-id ${EXAMPLE_APP_ID} --reveal-secret --json`],options:[{flags:"--app-id <id>",description:"App ID",parser:v=>parseAppId(v)},{flags:"--reveal-secret",description:"Show the client secret"},{flags:"--json",description:"Output as JSON"}],handler:opts=>credentialsCommand({appId:opts.appId,revealSecret:!!opts.revealSecret,json:!!opts.json})},{name:"upload",description:"Push app-config.json to Brevo, validated and synced with the server",examples:["brevo app upload","brevo app upload --yes","brevo app upload --json"],options:[{flags:"--yes",description:"Skip confirmation prompt"},{flags:"--json",description:"Output as JSON"}],handler:opts=>uploadCommand({yes:!!opts.yes,json:!!opts.json})},{name:"delete",description:"Delete an app",examples:[`brevo app delete --app-id ${EXAMPLE_APP_ID}`,`brevo app delete --app-id ${EXAMPLE_APP_ID} --force`],options:[{flags:"--app-id <id>",description:"App ID",parser:v=>parseAppId(v)},{flags:"--force",description:"Skip confirmation (for CI)"},{flags:"--json",description:"Output as JSON"}],handler:opts=>deleteCommand({appId:opts.appId,force:!!opts.force,json:!!opts.json})},{name:"scaffold",description:"Add a feature to the app in this directory, or set an empty directory up for an existing app",examples:["brevo app scaffold",`brevo app scaffold --app-id ${EXAMPLE_APP_ID}`,"brevo app scaffold --overwrite","brevo app scaffold --json",`brevo app scaffold --app-id ${EXAMPLE_APP_ID} --json`],options:[{flags:"--app-id <id>",description:"Set an empty directory up for an app you already have",parser:v=>parseAppId(v)},{flags:"--overwrite",description:"Overwrite existing feature files instead of merging (skips the prompt)"},{flags:"--json",description:"Output as JSON"}],handler:opts=>scaffoldCommand({appId:opts.appId,json:!!opts.json,overwrite:!!opts.overwrite})},{name:"available-scopes",description:"List OAuth scopes supported by the IdP",examples:["brevo app available-scopes","brevo app available-scopes --web","brevo app available-scopes --json"],options:[{flags:"--json",description:"Output as JSON"},{flags:"--web",description:"Open the scope catalog in a local browser page"}],handler:opts=>scopesCommand({json:!!opts.json,web:!!opts.web})},{name:"start",description:"Run a scaffolded feature locally",arguments:[{name:"[feature]",description:"Feature to start (e.g. oauth)"}],examples:["brevo app start oauth","brevo app start oauth --port 3000"],options:[{flags:"--port <port>",description:"Server port (default: 3009)",parser:v=>parsePositiveInt(v,"--port")}],handler:(opts,feature)=>startCommand({feature,port:opts.port})},{name:"install",requires:"account-install",description:"Install an app into a Brevo account",arguments:[{name:"[account-id]",description:"Brevo account (tenant) ID (defaults to your own account)"}],examples:["brevo app install","brevo app install 99999",`brevo app install 99999 --app-id ${EXAMPLE_APP_ID}`,"brevo app install 99999 --force --json"],options:[{flags:"--app-id <id>",description:"App ID (uses app-config.json if omitted)",parser:v=>parseAppId(v)},{flags:"--force",description:"Skip confirmation (for CI)"},{flags:"--json",description:"Output as JSON"}],handler:(opts,accountId)=>appInstallCommand({accountId,appId:opts.appId,force:!!opts.force,json:!!opts.json})},{name:"uninstall",requires:"account-install",description:"Uninstall an app from a Brevo account",arguments:[{name:"[account-id]",description:"Brevo account (tenant) ID (defaults to your own account)"}],examples:["brevo app uninstall","brevo app uninstall 99999",`brevo app uninstall 99999 --app-id ${EXAMPLE_APP_ID}`,"brevo app uninstall 99999 --force --json"],options:[{flags:"--app-id <id>",description:"App ID (uses app-config.json if omitted)",parser:v=>parseAppId(v)},{flags:"--force",description:"Skip confirmation (for CI)"},{flags:"--json",description:"Output as JSON"}],handler:(opts,accountId)=>appUninstallCommand({accountId,appId:opts.appId,force:!!opts.force,json:!!opts.json})}]},skillCommandGroup={name:"skill:cli",description:"Install the brevo-cli Claude Code skill (Claude only)",commands:[{name:"install",description:"Install the brevo-cli skill into ~/.claude/skills/ (Claude only \u2014 other AI tools should read agent-context/AGENTS.md instead)",examples:["brevo skill:cli install","brevo skill:cli install --json"],options:[{flags:"--json",description:"Output as JSON"}],handler:opts=>installCommand({json:!!opts.json})},{name:"uninstall",description:"Remove the brevo-cli skill from ~/.claude/skills/ (Claude only)",examples:["brevo skill:cli uninstall","brevo skill:cli uninstall --json"],options:[{flags:"--json",description:"Output as JSON"}],handler:opts=>uninstallCommand({json:!!opts.json})}]};var fs10=__toESM(require("node:fs")),os4=__toESM(require("node:os")),path11=__toESM(require("node:path"));var REGISTRY_URL=name=>`https://registry.npmjs.org/${encodeURIComponent(name).replace("%40","@")}/latest`,TTL_MS=720*60*1e3,FETCH_TIMEOUT_MS=2e3,NOTIFY_WAIT_MS=1500,CACHE_FILE="update-check.json";function getCachePath(override,env=process.env){if(override)return override;let dir=env.BREVO_CONFIG_HOME||path11.join(os4.homedir(),".brevo");return path11.join(dir,CACHE_FILE)}function shouldShowBannerBefore(argv){let args2=argv.slice(2);return args2.length===0||args2.includes("--help")||args2.includes("-h")||args2.includes("--version")||args2.includes("-V")?!0:args2[0]==="app"&&(args2[1]==="init"||args2[1]==="create")}function shouldSkipCheck(opts){let env=opts.env??process.env,argv=opts.argv??process.argv,isTTY2=opts.isTTY??!!process.stdout.isTTY;return!!(env.CI==="true"||env.CI==="1"||!isTTY2||env.NO_UPDATE_NOTIFIER==="1"||env.NO_UPDATE_NOTIFIER==="true"||env.BREVO_NO_UPDATE_NOTIFIER==="1"||env.BREVO_NO_UPDATE_NOTIFIER==="true"||argv.includes("--no-update-notifier"))}function parseVersion(v){let match=/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(v.trim());if(!(!match?.[1]||!match?.[2]||!match?.[3]))return{major:Number.parseInt(match[1],10),minor:Number.parseInt(match[2],10),patch:Number.parseInt(match[3],10),prerelease:match[4]??""}}function comparePrereleaseIdentifiers(ai,bi){if(ai===bi)return 0;let aNum=/^\d+$/.test(ai),bNum=/^\d+$/.test(bi);if(aNum&&bNum){let diff=Number.parseInt(ai,10)-Number.parseInt(bi,10);return diff===0?0:diff>0?1:-1}return aNum?-1:bNum||ai>bi?1:-1}function comparePrerelease(a,b){if(a===b)return 0;let aParts=a.split("."),bParts=b.split("."),len=Math.min(aParts.length,bParts.length);for(let i=0;i<len;i++){let cmp=comparePrereleaseIdentifiers(aParts[i]??"",bParts[i]??"");if(cmp!==0)return cmp}return aParts.length===bParts.length?0:aParts.length>bParts.length?1:-1}function compareVersions(current,latest){let c=parseVersion(current),l=parseVersion(latest);return!c||!l?0:l.major!==c.major?l.major-c.major:l.minor!==c.minor?l.minor-c.minor:l.patch!==c.patch?l.patch-c.patch:c.prerelease&&!l.prerelease?1:!c.prerelease&&l.prerelease?-1:comparePrerelease(l.prerelease,c.prerelease)}function isNewer(current,latest){return compareVersions(current,latest)>0}function isMajorBehind(current,latest){let c=parseVersion(current),l=parseVersion(latest);return!c||!l?!1:l.major>c.major}function readCache(cachePath){try{let raw=JSON.parse(fs10.readFileSync(cachePath,"utf-8"));if(raw&&typeof raw=="object"&&typeof raw.latest=="string"&&typeof raw.lastChecked=="number"&&Number.isFinite(raw.lastChecked))return{latest:raw.latest,lastChecked:raw.lastChecked}}catch{}}function writeCache(cachePath,cache){try{fs10.mkdirSync(path11.dirname(cachePath),{recursive:!0,mode:448}),fs10.writeFileSync(cachePath,JSON.stringify(cache,null,2),"utf-8")}catch{}}async function fetchLatestVersion(name,opts){let fetchImpl=opts?.fetchImpl??fetch,timeoutMs=opts?.fetchTimeoutMs??FETCH_TIMEOUT_MS,controller=new AbortController,timer=setTimeout(()=>controller.abort(),timeoutMs);try{let res=await fetchImpl(REGISTRY_URL(name),{signal:controller.signal,headers:{Accept:"application/json"}});if(!res.ok)return;let json=await res.json();return typeof json.version=="string"?json.version:void 0}catch{return}finally{clearTimeout(timer)}}function renderBox(lines){let inner=Math.max(...lines.map(l=>l.length))+4,top="\u256D"+"\u2500".repeat(inner)+"\u256E",bot="\u2570"+"\u2500".repeat(inner)+"\u256F",pad=s=>" "+s+" ".repeat(inner-s.length-2);return["",` ${top}`,...lines.map(l=>` \u2502${pad(l)}\u2502`),` ${bot}`,""].join(`
676
677
  `)}function withNotice(box,serverMessage){let line=serverMessage??messages.CLI_VERSION_NOTICE_FALLBACK;return`
677
678
  ${color(COLOR_RED,line)}
678
679
  ${box}`}function formatBanner(current,latest,name,serverMessage){return withNotice(renderBox([messages.UPDATE_AVAILABLE(current,latest),messages.UPDATE_RUN(name),messages.UPDATE_RUN_YARN(name),messages.UPDATE_RUN_BREW]),serverMessage)}function formatForceUpdateBanner(current,latest,name,serverMessage){return withNotice(renderBox([messages.FORCE_UPDATE_REQUIRED(current,latest),messages.FORCE_UPDATE_HINT,messages.UPDATE_RUN(name),messages.UPDATE_RUN_YARN(name),messages.UPDATE_RUN_BREW]),serverMessage)}function formatBlockedBanner(current,latest,name,serverMessage){return latest?formatForceUpdateBanner(current,latest,name,serverMessage):withNotice(renderBox([messages.FORCE_UPDATE_HINT,messages.UPDATE_RUN(name),messages.UPDATE_RUN_YARN(name),messages.UPDATE_RUN_BREW]),serverMessage)}function startUpdateCheck(opts){if(shouldSkipCheck(opts))return{pending:Promise.resolve()};let cachePath=getCachePath(opts.cachePath,opts.env),now=opts.now?opts.now():Date.now(),ttl=opts.ttlMs??TTL_MS,cache=readCache(cachePath);if(!(!cache||now-cache.lastChecked>ttl))return{cachedLatest:cache?.latest,pending:Promise.resolve()};let handle={cachedLatest:cache?.latest,pending:Promise.resolve()};return handle.pending=(async()=>{let latest=await fetchLatestVersion(opts.pkg.name,opts);latest&&(handle.cachedLatest=latest,writeCache(cachePath,{latest,lastChecked:now}))})(),handle}async function notifyUpdate(handle,pkg2,output=process.stderr,waitMs=NOTIFY_WAIT_MS){handle.notified||(await Promise.race([handle.pending,new Promise(resolve11=>setTimeout(resolve11,waitMs).unref?.())]),handle.cachedLatest&&isNewer(pkg2.version,handle.cachedLatest)&&(handle.notified=!0,output.write(formatBanner(pkg2.version,handle.cachedLatest,pkg2.name,handle.notice)+`