@swell/cli
Version:
Swell's command line interface/utility
799 lines (798 loc) • 34.2 kB
JavaScript
import { confirm, input, select } from '@inquirer/prompts';
import { $ } from 'execa';
import * as fs from 'node:fs';
import * as path from 'node:path';
import Stream from 'node:stream';
import ora from 'ora';
import { default as swellConfig } from './lib/app-config.js';
import { ConfigType, FrontendProjectTypes, allConfigFilesInDir, appConfigFromFile, filePathExists, filePathExistsAsync, findAppConfig, getAppSlugId, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
import { selectEnvironmentId } from './lib/stores.js';
import { getGlobIgnorePathsChecker } from './lib/apps/paths.js';
import { default as localConfig } from './lib/config.js';
import { toAppId } from './lib/create/index.js';
import style from './lib/style.js';
import { RemoteAppCommand } from './remote-app-command.js';
const PUSH_CONCURRENCY = 3;
const WATCH_WINDOW_MS = 100;
export class PushAppCommand extends RemoteAppCommand {
frontendPath = '';
logWatchChanges = true;
onWatchChange;
watchingChangeQueue = new Map();
watchingFiles = new Set();
watchingIgnoreFilter = null;
watchingTimer = null;
watchListener = async (eventType, filename) => {
if (!filename) {
return;
}
if (this.watchingIgnoreFilter && this.watchingIgnoreFilter(filename)) {
return;
}
const configFile = filename;
const pathParsed = path.parse(configFile);
const fileDirs = pathParsed.dir.split(path.sep);
// only the first directory is relevant for us
const configDir = fileDirs[0];
// we only want to watch files in the root of the app directory
// and ignore build files etc
// TODO: renaming a folder doesn't seem to work
// because isWatching = false for the new files
if (!configDir) {
return;
}
const configType = getConfigTypeFromPath(configDir);
if (!configType) {
return;
}
const isWatching = this.watchingFiles.has(configFile);
let watchFileEvent;
// per node docs:
//
// On most platforms, 'rename' is emitted whenever a filename appears
// or disappears in the directory.
//
// we want however to identify delete file events so we can send the
// correct API request
if (eventType === 'rename') {
watchFileEvent = 'new';
if (!(await filePathExistsAsync(path.join(this.appPath, configFile)))) {
watchFileEvent = 'deleted';
this.watchingFiles.delete(configFile);
}
else if (!isWatching) {
return;
}
this.watchingFiles.add(configFile);
}
else if (isWatching) {
watchFileEvent = 'change';
}
else {
return;
}
try {
await this.handleWatchFileChange(configFile, configType, watchFileEvent);
}
catch (error) {
// we don't want to break the watcher if an error is thrown while
// handling a file change
// for tests though, we want to break the watcher
if (process.env.NODE_ENV === 'test') {
// throwing does not break the test so we need to hard exit and log
// the error
console.error(style.error(error));
// eslint-disable-next-line no-process-exit, unicorn/no-process-exit
process.exit(1);
}
}
};
async buildFrontend(projectType) {
this.log(`Building ${projectType.name} frontend...\n`);
// TODO: check package.json for a "build" command and run that if it exists
await this.exec(projectType.buildCommand);
}
async chooseAppToPull(query) {
const typeLabelPlural = this.appType === 'theme' ? 'themes' : 'apps';
const typeLabelPrefixed = this.appType === 'theme' ? 'a theme' : 'an app';
const typeQuery = this.appType === 'theme' ? { type: 'theme' } : { type: 'storefront' };
const apps = await this.api.get({ adminPath: '/apps' }, { query: { limit: null, ...typeQuery, ...query } });
if (!apps.count) {
const currentStore = localConfig.getDefaultStore();
this.error(`There are no ${typeLabelPlural} associated with your store ${style.storeId(currentStore)}`);
}
const appSlugId = (await select({
choices: apps.results.map((app) => ({
name: `${style.appConfigValue(app.name)} (${getAppSlugId(app)})`,
value: app.private_id,
})),
message: `Choose ${typeLabelPrefixed} to pull`,
}));
const appId = apps.results.find((app) => app.private_id === appSlugId).id;
const app = await this.getApp(appId);
this.log();
return app;
}
async createAppStorefront(hasOtherStorefronts = true) {
let appId;
let themeAppId;
if (this.app.type === 'theme') {
themeAppId = this.app.id;
const storefrontApps = await this.handleRequestErrors(async () => this.api.get({
// TODO: improve this filter query
adminPath: `/apps?type=storefront&$or[0][kind]=shop&$or[1][kind]=`,
}));
const compatibleStorefrontApps = storefrontApps.results.filter((app) => app.installed &&
app.storefront?.theme?.provider === 'app' &&
this.app.theme?.storefront?.app === toAppId(app.private_id));
if (compatibleStorefrontApps.length === 0) {
this.error('No compatible storefront apps found for this theme. Install a new storefront app first.');
}
const targetApp = compatibleStorefrontApps.length === 1
? compatibleStorefrontApps[0]
: await select({
choices: compatibleStorefrontApps.map((app) => ({
name: `${style.appConfigValue(app.name)} v${app.version}`,
value: app,
})),
message: 'Choose a storefront app',
});
appId = targetApp.id;
this.log(`\n${style.success('✔')} Using ${style.appConfigValue(targetApp.name)} v${targetApp.version}\n`);
}
else if (this.app.type === 'storefront') {
appId = this.app.id;
const themeApps = await this.handleRequestErrors(async () => this.api.get({
adminPath: `/apps?type=theme`,
}));
const compatibleThemeApps = themeApps.results.filter((app) => app.installed &&
Object.keys(app.compatibilities || {}).includes(this.app.private_id));
if (compatibleThemeApps.length === 0) {
this.error('No compatible theme apps found for this storefront. Install a new theme app first.');
}
let targetApp;
if (compatibleThemeApps.length === 1) {
targetApp = compatibleThemeApps[0];
}
else {
targetApp = await select({
choices: compatibleThemeApps.map((app) => ({
name: `${style.appConfigValue(app.name)} v${app.version}`,
value: app,
})),
message: 'Choose a theme app',
});
this.log();
}
themeAppId = targetApp.id;
this.log(`${style.success('✔')} Using ${style.appConfigValue(targetApp.name)} v${targetApp.version}\n`);
}
else {
this.error('App type not supported for storefront creation');
}
let name;
// Leave name blank when syncing theme app to avoid blocking push
if (hasOtherStorefronts && !this.themeSyncApp) {
name = await input({
default: this.app.name,
message: 'Name your storefront',
});
if (name === this.app.name) {
name = undefined; // undefined allows the api to auto increment name
}
}
return this.handleRequestErrors(async () => this.api.post({
adminPath: `/storefronts`,
}, {
body: {
app_id: appId,
name,
theme_app_id: themeAppId,
},
onAsyncGetPath: (response) => ({
adminPath: `/storefronts/${response.id}`,
}),
}));
}
async deployAppFrontend(force, log) {
const currentStore = localConfig.getDefaultStore();
const projectType = this.getFrontendProjectType(false);
if (!projectType) {
return;
}
const deploymentHash = this.getFrontendDeploymentHash();
// Build and deploy only if there are files to push
if (deploymentHash &&
(force || deploymentHash !== this.app.frontend?.deployment_hash)) {
await this.buildFrontend(projectType);
const deploymentUrl = await this.deployFrontend(projectType);
await this.updateFrontendDeployment(currentStore, projectType, deploymentUrl, deploymentHash);
}
if (log !== false) {
if (this.app.type === 'storefront' && this.storefront) {
this.logStorefrontFrontendUrl(this.storefront);
}
else if (this.app.type !== 'storefront') {
// Get updated app with `installed` property
this.app = await this.getApp(this.app.id);
this.logAppFrontendUrl();
}
}
}
async deployFrontend(projectType) {
// TODO: check package.json for a "deploy" command and run that if it exists
// Note it must return a URL somehow
try {
return this.wranglerDeployFrontend(projectType);
}
catch (error) {
this.error(`Error deploying frontend: ${error.message}`);
}
}
async ensureAppExists(file, shouldCreate = true) {
const klass = this.ctor;
const currentStore = localConfig.getDefaultStore();
await this.ensureLoggedIn();
if (klass.orientation?.env) {
await this.api.setStoreEnv(currentStore, klass.orientation?.env);
}
const app = await this.getAppWithConfig('').catch((_error) =>
// Ignore so we can continue to create it
null);
const isTheme = app?.type === 'theme';
if (app && !isTheme && this.appType === 'theme') {
this.error(`App ${style.appConfigValue(app?.name)} is not a theme.`);
}
// Confirm development app unless theme without app syncing
if (!isTheme || this.themeSyncApp) {
const confirmExistingApp = await this.confirmRemoveInstalledApp(app, currentStore);
if (confirmExistingApp === false) {
return false;
}
}
// Do not create or update if app already exists and not forcing it
if (!shouldCreate && app) {
this.app = app;
return true;
}
// If app exists but owned by another client, create a new instance
if (app?.client_id && app.client_id !== currentStore) {
// Create a new app
this.app = await this.getCreateUpdateApp();
if (!this.app) {
return false;
}
}
else if (!file) {
// Update existing app if not targeting a file
this.app = await this.getCreateUpdateApp(app);
}
return true;
}
async ensureLoggedIn() {
try {
const user = await this.api.get({ adminPath: 'user' });
if (!user) {
throw new Error('Failed to log in');
}
}
catch {
this.log('Logging in...\n');
await this.config.runCommand('login');
this.log();
return true;
}
}
async exec(command, onOutput) {
const $$ = $({
cwd: this.frontendPath,
shell: true,
stderr: onOutput ? 'pipe' : 'inherit',
stdin: 'inherit',
stdout: onOutput ? 'pipe' : 'inherit',
});
if (onOutput) {
const outStream = new Stream.Writable();
outStream._write = (chunk, _encoding, next) => {
const string = chunk.toString();
const out = onOutput(string);
if (out !== false) {
console.log(string);
}
next();
};
await $$ `${command}`.pipeStdout?.(outStream).pipeStderr?.(outStream);
}
else {
await $$ `${command}`;
}
}
async getAllAppStorefronts(params) {
const query = Object.entries(params || {})
.map(([key, value]) => `${key}=${value}`)
.join('&');
return this.handleRequestErrors(async () => this.api.get({
adminPath: `/apps/${this.app.id}/storefronts?${query}`,
}));
}
async getAllStorefronts(params) {
const query = Object.entries(params || {})
.map(([key, value]) => `${key}=${value}`)
.join('&');
return this.handleRequestErrors(async () => this.api.get({
adminPath: `/storefronts?${query}`,
}));
}
async getAppStorefront(params) {
const query = Object.entries(params || {})
.map(([key, value]) => `${key}=${value}`)
.join('&');
try {
return this.handleRequestErrors(async () => this.api.get({
adminPath: `/apps/${this.app.id}/storefront?${query}`,
}));
}
catch {
// noop
}
}
async getAppToPull(args, shouldChoose = true) {
const { appId, targetPath } = args;
const klass = this.ctor;
const currentStore = localConfig.getDefaultStore();
await this.api.setStoreEnv(currentStore, klass.orientation?.env);
this.resolveAppPath(appId, targetPath);
if (isPathDirectory(this.appPath)) {
this.swellConfig = swellConfig(this.appPath);
const localAppId = this.swellConfig.get('id');
const localAppType = this.swellConfig.get('type');
if (localAppId) {
if (appId && appId !== localAppId) {
this.error(`${localAppType === 'theme' ? 'A theme' : 'An app'} already exists in this path with a different ID: ${style.appConfigValue(localAppId)}`);
}
return this.getExistingApp(localAppId);
}
}
const app = appId
? await this.getExistingApp(appId)
: shouldChoose
? await this.chooseAppToPull()
: undefined;
if (shouldChoose) {
this.swellConfig = swellConfig(this.appPath);
}
return app;
}
async getExistingApp(appId) {
try {
return this.getApp(appId);
}
catch (error) {
if (error.status === 404) {
const labelPrefix = this.appType === 'theme' ? 'Theme' : 'App';
this.error(`${labelPrefix} '${appId}' not found.`);
}
else {
throw error;
}
}
}
getFrontendDeploymentHash() {
const localConfigs = this.getLocalAppConfigs();
let frontendHashes = localConfigs
.filter((config) => config.filePath?.startsWith(`frontend${path.sep}`))
.map((config) => config.hash)
.sort()
.join('|');
if (frontendHashes.length > 0) {
const packageHash = localConfigs.find((config) => config.filePath === 'package.json')?.hash;
frontendHashes += `|${packageHash}`;
}
return frontendHashes.length > 0 ? hashString(frontendHashes) : undefined;
}
getFrontendProjectType(required = true) {
this.frontendPath = path.join(this.appPath, 'frontend');
if (!required) {
const frontendExists = filePathExists(this.frontendPath);
if (!frontendExists) {
return null;
}
}
const projectType = getFrontendProjectType(this.appPath);
if (!projectType) {
this.error(`No valid frontend app found in ${this.appPath}/${this.frontendPath}. Supported frameworks include: ${FrontendProjectTypes.map((type) => type.slug).join(', ')}.`);
}
return projectType;
}
async getStorefrontToPull(args = {}, flags = {}) {
const { appId, targetPath } = args;
const targetStorefront = flags['storefront-id'];
const targetApp = await this.getAppToPull(args, false);
this.resolveAppPath(appId, targetPath);
// Only use default if path exists
let defaultStorefront;
if (isPathDirectory(this.appPath)) {
defaultStorefront = localConfig.getDefaultStorefront(this.appPath);
}
await this.setStorefrontEnv(flags, defaultStorefront);
let storefrontId = targetStorefront || targetApp ? defaultStorefront?.id : undefined;
if (!storefrontId || flags['storefront-select']) {
const storefronts = await this.getAllStorefronts(targetApp?.id
? this.appType === 'theme'
? { theme_app_id: targetApp?.id }
: { app_id: targetApp?.id }
: { type: this.appType });
if (storefronts.count > 0) {
storefrontId = await select({
choices: storefronts.results.map((storefront) => ({
name: `${style.appConfigValue(storefront.name)} (${storefront.id}) ${storefront.primary ? '[primary]' : ''}`,
value: storefront.id,
})),
message: 'Choose a storefront',
});
this.storefront = storefronts.results.find((storefront) => storefront.id === storefrontId);
const appId = this.appType === 'theme'
? this.storefront.theme_app_id
: this.storefront.app_id;
this.resolveAppPath(this.storefront.name, targetPath);
this.app = await this.getApp(appId);
}
else {
// Theme apps should always have a storefront, so this shouldn't be necessary
// However it's a good fallback in case the storefront was deleted
// Choosing a theme app will proceed to pull app files
this.app = await this.chooseAppToPull();
this.resolveAppPath(this.app.name, targetPath);
}
}
else {
this.app = targetApp;
}
if (storefrontId && !this.storefront) {
this.storefront = await this.getAppStorefront({
storefront_id: storefrontId,
type: 'theme',
});
if (!this.storefront) {
this.error(`Storefront ${style.appConfigValue(storefrontId)} not found.`);
}
const appId = this.appType === 'theme'
? this.storefront.theme_app_id
: this.storefront.app_id;
this.app = await this.getApp(appId);
}
this.swellConfig = swellConfig(this.appPath);
return this.storefront;
}
async getStorefrontToPush(flags = {}) {
const targetStorefront = flags['storefront-id'];
const defaultStorefront = localConfig.getDefaultStorefront(this.appPath);
let storefrontId = targetStorefront || defaultStorefront?.id;
// Force storefront select if pushing app from default storefront in live environment
if (this.themeSyncApp && defaultStorefront?.id && !defaultStorefront.env) {
storefrontId = '';
}
if (!storefrontId || flags['storefront-select']) {
const storefronts = await this.getAllAppStorefronts({
type: this.appType,
});
// Choose storefront if multiple storefronts exist
// Except if we are syncing a theme app, then force create new storefront if not found
if (storefronts.count > 0 && !this.themeSyncApp) {
storefrontId = await select({
choices: [
...storefronts.results.map((storefront) => ({
name: `${style.appConfigValue(storefront.name)} (${storefront.id}) ${storefront.primary ? '[primary]' : ''}`,
value: storefront.id,
})),
{
name: 'Create new storefront',
value: null,
},
],
message: 'Choose a storefront',
});
}
if (!storefrontId) {
this.storefront = await this.createAppStorefront(storefronts.count > 0);
}
}
if (storefrontId) {
this.storefront = await this.getAppStorefront({
storefront_id: storefrontId,
type: this.appType,
});
if (!this.storefront) {
if (targetStorefront) {
this.error(`Storefront ${style.appConfigValue(storefrontId)} not found.`);
}
this.log(`Storefront ${style.appConfigValue(storefrontId)} not found.\n`);
localConfig.setDefaultStorefront(this.appPath, '');
return this.getStorefrontToPush();
}
}
this.app = await this.getApp(this.app.id);
return this.storefront;
}
async setStorefrontEnv(flags, defaultStorefront) {
const currentStore = localConfig.getDefaultStore();
const targetStorefront = flags['storefront-id'];
if (defaultStorefront &&
!targetStorefront &&
!flags['storefront-id'] &&
!flags['storefront-select']) {
const klass = this.ctor;
if (klass.orientation?.env === undefined) {
await this.api.setStoreEnv(currentStore, defaultStorefront?.env);
}
}
else if (flags['env']) {
await this.api.setStoreEnv(currentStore, flags['env']);
}
else {
const hasTestEnv = await this.api.isTestEnvEnabled();
if (hasTestEnv) {
const env = await selectEnvironmentId();
await this.api.setStoreEnv(currentStore, env);
}
}
}
async handleWatchFileChange(configFile, configType, event) {
let config;
switch (event) {
case 'deleted': {
config = findAppConfig(this.app, configFile, configType);
if (!config) {
return;
}
this.watchingChangeQueue.set(configFile, {
action: 'remove',
appConfig: config,
configFile,
});
break;
}
default: {
config = appConfigFromFile(configFile, configType, this.appPath);
this.watchingChangeQueue.set(configFile, {
action: 'push',
appConfig: config,
configFile,
});
break;
}
}
if (this.watchingTimer === null) {
this.watchingTimer = setTimeout(async () => {
try {
await this.runWatchChangeQueue();
}
finally {
this.watchingTimer = null;
}
}, WATCH_WINDOW_MS);
}
}
logAppFrontendUrl() {
const currentStore = localConfig.getDefaultStore();
// Shouldn't happen, but just in case
if (!this.app.installed?.id) {
return;
}
this.log(`View your app at ${style.link(this.appFrontendUrl(currentStore, this.app.installed.id))}.`);
}
logDashboardUrl(flags) {
const currentStore = localConfig.getDefaultStore();
this.log(`View the ${style.appConfigValue(this.app.name)} app in your dashboard at ${style.link(this.dashboardUrl(flags, currentStore))}.`);
}
logStorefrontConnected() {
if (!this.storefront) {
this.error('Storefront not connected');
}
const { name } = this.storefront;
this.log(`[storefront] ${style.appName(name)}\n`);
}
logStorefrontFrontendUrl(storefront, branchId) {
const currentStore = localConfig.getDefaultStore();
this.log(`View your storefront at ${style.link(this.storefrontFrontendUrl(currentStore, storefront, branchId))}.`);
}
async pushFile(relativePath) {
const basePath = relativePath.split(path.sep)[0];
const configType = getConfigTypeFromPath(basePath) || ConfigType.FILE;
const config = appConfigFromFile(relativePath, configType, this.appPath);
await this.pushRemoteFile(config);
}
async pushFilePath(relativePath, force) {
const topDir = relativePath.split(path.sep)[0];
const configType = (getConfigTypeFromPath(topDir) || 'file');
const configTypeKey = getConfigTypeKeyFromValue(configType) || 'file';
const exAppConfigs = (this.app?.configs || []).filter((config) => config.filePath?.startsWith(`${relativePath}/`));
const allFiles = [];
const pushFiles = [];
for (const { configFile } of allConfigFilesInDir(this.appPath, relativePath, configTypeKey)) {
const config = appConfigFromFile(configFile, configType, this.appPath);
allFiles.push(config);
const exConfig = exAppConfigs.find((exConfig) => config.filePath === exConfig.filePath);
if (!force && exConfig && exConfig.hash === config.hash) {
continue;
}
pushFiles.push(config);
}
const filesToRemove = exAppConfigs.filter((config) => !allFiles.some((appConfig) => appConfig.filePath === config.filePath));
const hasChanges = pushFiles.length > 0 || filesToRemove.length > 0;
if (pushFiles.length > 0) {
const pushConcurrency = configTypeKey === 'ASSET' ? 1 : PUSH_CONCURRENCY;
while (pushFiles.length > 0) {
// eslint-disable-next-line no-await-in-loop
await Promise.all(pushFiles
.splice(0, pushConcurrency)
.map(async (appConfig) => {
await this.pushRemoteFile(appConfig);
}));
}
}
// Remove any deleted files
if (filesToRemove.length > 0) {
while (filesToRemove.length > 0) {
// eslint-disable-next-line no-await-in-loop
await Promise.all(filesToRemove.splice(0, PUSH_CONCURRENCY).map(async (appConfig) => {
await this.removeRemoteFile(appConfig);
}));
}
}
if (!hasChanges && !force) {
this.log(`${style.success('✔')} All ${style.path(relativePath)} files are up to date.`);
}
else {
this.log();
this.log(`${style.success('✔')} Pushed ${style.path(relativePath)} to ${style.appConfigValue(this.app.name)}.`);
}
}
resolveAppPath(appId, targetPath) {
const resolvedAppId = appId || '';
let resolvedPath = targetPath || resolvedAppId;
// If app_id matches directory, use it as target path
if (!targetPath &&
isPathDirectory(path.resolve(process.cwd(), resolvedAppId))) {
resolvedPath = resolvedAppId;
}
// Reset app path if appId is provided
if (resolvedPath && this.appPath === process.cwd()) {
this.appPath = path.resolve(process.cwd(), resolvedPath);
}
return this.appPath;
}
async runWatchChangeQueue() {
if (this.watchingChangeQueue.size <= 0) {
return;
}
const queue = [...this.watchingChangeQueue.values()];
this.watchingChangeQueue.clear();
while (queue.length > 0) {
// eslint-disable-next-line no-await-in-loop
await Promise.all(queue.splice(0, PUSH_CONCURRENCY).map(async ({ action, appConfig }) => {
const result = await (action === 'remove'
? this.removeRemoteFile(appConfig, this.logWatchChanges)
: this.pushRemoteFile(appConfig, this.logWatchChanges));
this.onWatchChange?.(appConfig, result);
}));
}
// refetch app to get updated configs
this.app = await this.getAppWithConfig(this.app.id);
// Process next accumulated changes
return this.runWatchChangeQueue();
}
saveCurrentStorefront() {
const currentStorefront = localConfig.getDefaultStorefront(this.appPath);
if (this.storefront && this.storefront.id !== currentStorefront) {
localConfig.setDefaultStorefront(this.appPath, this.storefront.id, this.api.envId);
}
}
async setWatchingFiles() {
this.watchingFiles = new Set(await globAllFilesByPath(this.appPath));
}
async updateFrontendDeployment(currentStore, projectType, deploymentUrl, deploymentHash) {
const spinner = ora();
spinner.start('Updating frontend deployment...');
await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${this.app.id}` }, {
body: {
$client_id: currentStore,
$environment_id: 'test',
frontend: {
deployment_hash: deploymentHash,
framework: projectType.slug,
service: 'cloudflare',
url: deploymentUrl,
},
},
}), () => spinner.fail('Error updating app deployment'));
spinner.succeed('Updated frontend deployment.\n');
}
async watchForChanges({ logChanges, onChange, syncAll, } = {}) {
this.watchingIgnoreFilter = await getGlobIgnorePathsChecker(this.appPath);
this.logWatchChanges = logChanges ?? true;
this.onWatchChange = onChange;
await this.setWatchingFiles();
if (syncAll) {
await this.pushAppConfigs();
fs.watch(this.appPath, { recursive: true }, async (_eventType, _filename) => {
await this.pushAppConfigs();
await this.setWatchingFiles();
});
return;
}
fs.watch(this.appPath, { recursive: true }, this.watchListener);
}
async wranglerDeployFrontend(projectType) {
let deploymentUrl;
let interactiveError = false;
this.log(`\nDeploying to Cloudflare...\n`);
try {
await this.exec(`npx wrangler pages deploy ${this.appPath}/frontend/${projectType.deployPath}`, (string) => {
// Dependent on wrangler output
// Parse the deployment URL from the wrangler output.
const match = string.match(/Take a peek over at (http\S+)/);
if (match && match.length > 0) {
deploymentUrl = match[1];
return false;
}
if (interactiveError ||
string.includes('non-interactive mode') ||
string.includes('non-interactive environment')) {
interactiveError = true;
return false;
}
});
}
catch (error) {
// noop
if (interactiveError) {
this.log(style.funcWarn(`Your Cloudflare environment must be initialized by logging in with \`wrangler login\`, and exporting the \`CLOUDFLARE_ACCOUNT_ID\` environment variable. Refer to https://developers.cloudflare.com/workers/wrangler/configuration/ for details, and re-run this command to connect the deployment with your app.`));
// eslint-disable-next-line no-process-exit, unicorn/no-process-exit
process.exit(1);
}
else {
throw error;
}
}
if (!deploymentUrl) {
this.error('Unable to retrieve deployment URL.');
}
return deploymentUrl;
}
async confirmRemoveInstalledApp(app, currentStore) {
if (!app?.private_id) {
return true;
}
const existingInstalledApp = await this.api.get({
adminPath: `/client/apps/${app.private_id}`,
}, { query: { versioned: true } });
if (existingInstalledApp) {
const continuePush = await confirm({
default: false,
message: `${style.storeId(currentStore)} has an installed version of ${app.name ? style.appConfigValue(app.name) : 'this app'} v${existingInstalledApp.version}. Do you want to replace it with a development instance?`,
});
if (!continuePush) {
return false;
}
// uninstall the existing app
const spinner = ora();
spinner.start('Removing installed app...');
try {
await this.api.put({
adminPath: `/client/apps/${app.id}`,
}, {
body: { uninstalled: true },
});
spinner.succeed('Installed app removed.');
this.log();
}
catch (error) {
spinner.fail(`Error removing installed app from ${style.storeId(currentStore)}.`);
this.logError(error.message);
return false;
}
}
return existingInstalledApp;
}
}