22# Remotely installs the Disembark plugin, connects, and initiates a backup using an embedded Playwright script.
33# ----------------------------------------------------
44function run_disembark() {
5- local target_url ="$1"
5+ local target_url_input ="$1"
66 local debug_flag="$2"
77
8- echo "🚀 Starting Disembark process for ${target_url }..."
8+ echo "🚀 Starting Disembark process for ${target_url_input }..."
99
1010 # --- 1. Pre-flight Checks for disembark-cli ---
11- if [ -z "$target_url " ];then
11+ if [ -z "$target_url_input " ];then
1212 echo "❌ Error: Missing required URL argument." >&2
1313 show_command_help "disembark"
1414 return 1
1515 fi
1616
1717 if ! setup_disembark; then return 1; fi
1818
19- # --- 2. Attempt backup with a potentially stored token ---
19+ # --- 2. Smart URL Parsing ---
20+ local base_url
21+ local login_path
22+
23+ # Check if the input URL contains /wp-admin or /wp-login.php
24+ if [[ "$target_url_input" == *"/wp-admin"* || "$target_url_input" == *"/wp-login.php"* ]]; then
25+ # If it's a backend URL, extract the base and the path
26+ base_url=$(echo "$target_url_input" | sed -E 's#(https?://[^/]+).*#\1#')
27+ login_path=$(echo "$target_url_input" | sed -E "s#$base_url##")
28+ echo " - Detected backend URL. Base: '$base_url', Path: '$login_path'"
29+ # Handle URLs with a path component that don't end in a slash (potential custom login)
30+ elif [[ "$target_url_input" == *"/"* && "${target_url_input: -1}" != "/" ]]; then
31+ local path_part
32+ # Use sed -n with 'p' to ensure it only outputs on a successful match
33+ path_part=$(echo "$target_url_input" | sed -n -E 's#https?://[^/]+(/.*)#\1#p')
34+
35+ # If path_part is empty, it means there was no path after the domain (e.g., https://example.com)
36+ if [ -z "$path_part" ]; then
37+ base_url="${target_url_input%/}"
38+ login_path="/wp-login.php"
39+ echo " - Homepage URL detected. Assuming default login path: '$login_path'"
40+ # Check for deep links inside WordPress content directories
41+ elif [[ "$path_part" == *"/wp-content"* || "$path_part" == *"/wp-includes"* ]]; then
42+ base_url="$target_url_input"
43+ login_path="/wp-login.php"
44+ echo " - Deep link detected. Assuming default login for base URL."
45+ # Otherwise, assume it's a custom login path
46+ else
47+ base_url=$(echo "$target_url_input" | sed -E 's#(https?://[^/]+).*#\1#')
48+ login_path="$path_part"
49+ echo " - Custom login path detected. Base: '$base_url', Path: '$login_path'"
50+ fi
51+ # Handle homepage URLs that might end with a slash
52+ else
53+ base_url="${target_url_input%/}" # Remove trailing slash if present
54+ login_path="/wp-login.php"
55+ echo " - Homepage URL detected. Assuming default login path: '$login_path'"
56+ fi
57+
58+
59+ # --- 3. Attempt backup with a potentially stored token ---
2060 echo "✅ Attempting backup using a stored token..."
21- if "$DISEMBARK_CMD" backup "$target_url "; then
61+ if "$DISEMBARK_CMD" backup "$base_url "; then
2262 echo "✨ Backup successful using a pre-existing token."
2363 return 0
2464 fi
2565
26- # --- 3 . If backup fails, proceed to full browser authentication ---
66+ # --- 4 . If backup fails, proceed to full browser authentication ---
2767 echo "⚠️ Backup with stored token failed. A new connection token is likely required."
2868 echo "Proceeding with browser authentication..."
2969
3070 # --- Pre-flight Checks for browser automation ---
3171 if ! setup_playwright; then return 1; fi
3272 if ! setup_gum; then return 1; fi
3373
34- # --- Get Credentials Interactively ---
35- echo "Please provide WordPress administrator credentials:"
36- local username
37- username=$(gum input --placeholder="Enter WordPress username...")
38- if [ -z "$username" ]; then
39- echo "No username provided. Aborting." >&2
40- return 1
41- fi
42-
43- local password
44- password=$(gum input --placeholder="Enter WordPress password..." --password)
45- if [ -z "$password" ]; then
46- echo "No password provided. Aborting." >&2
47- return 1
48- fi
49-
5074 # --- Define the Playwright script using a Heredoc ---
5175 local PLAYWRIGHT_SCRIPT
5276 PLAYWRIGHT_SCRIPT=$(cat <<'EOF'
@@ -60,12 +84,15 @@ const path = require('path');
6084const PLUGIN_ZIP_URL = 'https://github.com/DisembarkHost/disembark-connector/releases/latest/download/disembark-connector.zip';
6185
6286async function main() {
63- const [, , targetUrl , username, password, debugFlag] = process.argv;
87+ const [, , baseUrl, loginPath , username, password, debugFlag] = process.argv;
6488
65- if (!targetUrl || !username || !password) {
66- console.error('Usage: node disembark-browser.js <url > <username> <password> [debug]');
89+ if (!baseUrl || !loginPath || !username || !password) {
90+ console.error('Usage: node disembark-browser.js <baseUrl> <loginPath > <username> <password> [debug]');
6791 process.exit(1);
6892 }
93+
94+ const loginUrl = baseUrl + loginPath;
95+ const adminUrl = baseUrl + '/wp-admin/';
6996
7097 const isHeadless = debugFlag !== 'true';
7198 const browser = await chromium.launch({ headless: isHeadless });
@@ -76,24 +103,54 @@ async function main() {
76103 const page = await context.newPage();
77104 try {
78105 // 1. LOGIN
79- process.stdout.write(' - Step 1/5: Authenticating with WordPress...');
80- await page.goto(`${targetUrl}/wp-login.php`, { waitUntil: 'domcontentloaded' });
81- await page.waitForSelector('#user_login', { state: 'visible', timeout: 30000 });
106+ process.stdout.write(` - Step 1/5: Authenticating with WordPress at ${loginUrl}...`);
107+ await page.goto(loginUrl, { waitUntil: 'domcontentloaded' });
108+
109+ if (!(await page.isVisible('#user_login'))) {
110+ console.log(' Failed.');
111+ console.error('LOGIN_URL_INVALID');
112+ process.exit(2);
113+ }
114+
82115 await page.fill('#user_login', username);
83- await page.waitForSelector('#user_pass', { state: 'visible', timeout: 30000 });
84116 await page.fill('#user_pass', password);
85- await page.waitForSelector('#wp-submit', { state: 'visible', timeout: 30000 });
86117 await page.click('#wp-submit');
87- await page.waitForSelector('#wpadminbar', { timeout: 60000 });
118+
119+ try {
120+ await page.waitForSelector('#wpadminbar, #login_error, #correct-admin-email', { timeout: 60000 });
121+ } catch (e) {
122+ throw new Error('Authentication timed out. The page did not load the admin bar, a login error, or the admin email confirmation screen.');
123+ }
124+
125+ if (await page.isVisible('#login_error')) {
126+ const errorText = await page.locator('#login_error').textContent();
127+ console.error(`LOGIN_FAILED: ${errorText.trim()}`);
128+ process.exit(3);
129+ }
130+
131+ if (await page.isVisible('#correct-admin-email')) {
132+ process.stdout.write(' Admin email confirmation required. Submitting...');
133+ await page.click('#correct-admin-email');
134+ await page.waitForSelector('#wpadminbar', { timeout: 60000 });
135+ }
88136
89137 if (!(await page.isVisible('#wpadminbar'))) {
90- throw new Error('Authentication failed. Please check credentials or for 2FA/CAPTCHA .');
138+ throw new Error('Authentication failed. Admin bar not found after login .');
91139 }
140+
141+ // --- Recovery Step: Navigate to the main dashboard to bypass any welcome/update screens ---
142+ if (!page.url().startsWith(adminUrl)) {
143+ process.stdout.write(' Navigating to main dashboard to bypass intermediate pages...');
144+ await page.goto(adminUrl, { waitUntil: 'networkidle' });
145+ await page.waitForSelector('#wpadminbar'); // Re-confirm we are in the admin area
146+ process.stdout.write(' Done.');
147+ }
148+
92149 console.log(' Success!');
93150
94151 // 2. CHECK IF PLUGIN EXISTS & ACTIVATE IF NEEDED
95152 process.stdout.write(' - Step 2/5: Checking plugin status...');
96- await page.goto(`${targetUrl}/wp-admin/ plugins.php`, { waitUntil: 'networkidle' });
153+ await page.goto(`${adminUrl} plugins.php`, { waitUntil: 'networkidle' });
97154
98155 const pluginRow = page.locator('tr[data-slug="disembark-connector"]');
99156 if (await pluginRow.count() > 0) {
@@ -135,45 +192,38 @@ async function main() {
135192
136193 console.log(' Download complete.');
137194 process.stdout.write(' - Uploading and installing...');
138- await page.goto(`${targetUrl}/wp-admin/ plugin-install.php?tab=upload`);
195+ await page.goto(`${adminUrl} plugin-install.php?tab=upload`);
139196 await page.setInputFiles('input#pluginzip', pluginZipPath);
140197 await page.waitForSelector('input#install-plugin-submit:not([disabled])', { timeout: 10000 });
141198
142199 await page.click('input#install-plugin-submit');
143200
144- // Define selectors for all possible outcomes after installation attempt.
145201 const activationLinkSelector = 'a:has-text("Activate Plugin"), a.activate-now, .button.activate-now';
146202 const alreadyInstalledSelector = 'body:has-text("Destination folder already exists.")';
147- const mixedSuccessSelector = 'body:has-text("Plugin installed successfully.")'; // For your specific error case
203+ const mixedSuccessSelector = 'body:has-text("Plugin installed successfully.")';
148204 const genericErrorSelector = '.wrap > .error, .wrap > #message.error';
149205
150206 try {
151- // Wait for ANY of the outcomes to appear on the page.
152207 await page.waitForSelector(
153208 `${activationLinkSelector}, ${alreadyInstalledSelector}, ${mixedSuccessSelector}, ${genericErrorSelector}`,
154209 { timeout: 90000 }
155210 );
156211 } catch (e) {
157- // If none of the expected outcomes appear, throw a specific timeout error.
158- throw new Error('Timed out waiting for a response after clicking "Install Now". The page may have hung or produced an unexpected result.');
212+ throw new Error('Timed out waiting for a response after clicking "Install Now".');
159213 }
160214
161- // Now, check which outcome occurred and act accordingly.
162215 if (await page.locator(activationLinkSelector).count() > 0) {
163- // Outcome 1: Success, the plugin was installed and needs activation.
164216 const activateButton = page.locator(activationLinkSelector);
165217 await Promise.all([
166218 page.waitForNavigation({ waitUntil: 'networkidle' }),
167219 activateButton.first().click(),
168220 ]);
169221 console.log(' Installed & Activated!');
170222 } else if (await page.locator(alreadyInstalledSelector).count() > 0) {
171- // Outcome 2: The plugin was already installed.
172223 console.log(' Plugin already installed.');
173224 } else if (await page.locator(mixedSuccessSelector).count() > 0) {
174- // Outcome 3: Install was successful, but the page crashed.
175225 console.log(' Install succeeded, but page reported an error. Navigating to plugins page to activate...');
176- await page.goto(`${targetUrl}/wp-admin/ plugins.php`, { waitUntil: 'networkidle' });
226+ await page.goto(`${adminUrl} plugins.php`, { waitUntil: 'networkidle' });
177227 const pluginRow = page.locator('tr[data-slug="disembark-connector"]');
178228 const activateLink = pluginRow.locator('a.edit:has-text("Activate")');
179229 if (await activateLink.count() > 0) {
@@ -186,7 +236,6 @@ async function main() {
186236 console.log(' - Plugin was already active on the plugins page.');
187237 }
188238 } else {
189- // Outcome 4: A generic WordPress error occurred.
190239 const errorText = await page.locator(genericErrorSelector).first().textContent();
191240 throw new Error(`Plugin installation failed with a WordPress error: ${errorText.trim()}`);
192241 }
@@ -197,7 +246,7 @@ async function main() {
197246
198247 // 4. RETRIEVE TOKEN
199248 process.stdout.write(' - Step 4/5: Retrieving connection token...');
200- const tokenPageUrl = `${targetUrl}/wp-admin/ plugin-install.php?tab=plugin-information&plugin=disembark-connector`;
249+ const tokenPageUrl = `${adminUrl} plugin-install.php?tab=plugin-information&plugin=disembark-connector`;
201250 await page.goto(tokenPageUrl, { waitUntil: 'networkidle' });
202251
203252 const tokenElement = page.locator('div#section-description > code');
@@ -221,34 +270,106 @@ async function main() {
221270main();
222271EOF
223272)
273+ # --- Helper function for getting credentials ---
274+ _get_credentials() {
275+ echo "Please provide WordPress administrator credentials:"
276+ username=$("$GUM_CMD" input --placeholder="Enter WordPress username...")
277+ if [ -z "$username" ]; then
278+ echo "No username provided. Aborting." >&2
279+ return 1
280+ fi
281+
282+ password=$("$GUM_CMD" input --placeholder="Enter WordPress password..." --password)
283+ if [ -z "$password" ]; then
284+ echo "No password provided. Aborting." >&2
285+ return 1
286+ fi
287+ return 0
288+ }
224289
225- # --- 4. Run Browser Automation ---
226- echo "🤖 Launching browser to automate login and plugin setup..."
227-
228- # Enable pipefail to catch errors from the node script before the pipe to tee
229- set -o pipefail
230- local full_output
231- full_output=$(echo "$PLAYWRIGHT_SCRIPT" | node - "$target_url" "$username" "$password" "$debug_flag" | tee /dev/tty)
232- local exit_code=$?
233-
234- # Disable pipefail after the command to avoid affecting other parts of the script
235- set +o pipefail
290+ # --- Helper for running playwright ---
291+ run_playwright_and_get_token() {
292+ local url_to_run="$1"
293+ local path_to_run="$2"
294+ local user_to_run="$3"
295+ local pass_to_run="$4"
296+
297+ echo "🤖 Launching browser to automate login and plugin setup..."
298+ echo " - Attempting login at: ${url_to_run}${path_to_run}"
299+
300+ set -o pipefail
301+ local full_output
302+ full_output=$(echo "$PLAYWRIGHT_SCRIPT" | node - "$url_to_run" "$path_to_run" "$user_to_run" "$pass_to_run" "$debug_flag" | tee /dev/tty)
303+ local exit_code=$?
304+ set +o pipefail
305+
306+ if [ $exit_code -eq 3 ]; then
307+ return 3 # Bad credentials
308+ elif [ $exit_code -eq 2 ]; then
309+ return 2 # Invalid URL
310+ elif [ $exit_code -ne 0 ]; then
311+ return 1 # General failure
312+ fi
313+
314+ echo "$full_output" | tail -n 1 | tr -d '[:space:]'
315+ return 0
316+ }
236317
237- if [ $exit_code -ne 0 ]; then
238- # The error message from the script has already been displayed by tee
239- echo "❌ Browser automation failed." >&2
318+ # --- Authentication Loop ---
319+ local token
320+ local playwright_exit_code
321+
322+ # Initial credential prompt
323+ if ! _get_credentials; then return 1; fi
324+
325+ while true; do
326+ token=$(run_playwright_and_get_token "$base_url" "$login_path" "$username" "$password")
327+ playwright_exit_code=$?
328+
329+ if [ $playwright_exit_code -eq 0 ]; then
330+ break # Success
331+ elif [ $playwright_exit_code -eq 2 ]; then # Invalid URL
332+ echo "⚠️ The login URL '${base_url}${login_path}' appears to be incorrect."
333+ local new_login_url
334+ new_login_url=$("$GUM_CMD" input --placeholder="Enter the full, correct WordPress Admin URL...")
335+
336+ if [ -z "$new_login_url" ]; then
337+ echo "No URL provided. Aborting." >&2; return 1;
338+ fi
339+ base_url=$(echo "$new_login_url" | sed -E 's#(https?://[^/]+).*#\1#')
340+ login_path=$(echo "$new_login_url" | sed -E "s#$base_url##")
341+ continue # Retry loop with new URL
342+ elif [ $playwright_exit_code -eq 3 ]; then # Bad credentials
343+ echo "⚠️ Login failed. The credentials may be incorrect."
344+ if "$GUM_CMD" confirm "Re-enter credentials and try again?"; then
345+ if ! _get_credentials; then return 1; fi
346+ continue # Retry loop with new credentials
347+ else
348+ echo "Authentication cancelled." >&2; return 1;
349+ fi
350+ else # General failure
351+ echo "❌ Browser automation failed. Please check errors above." >&2
352+ return 1
353+ fi
354+ done
355+
356+ # --- Final Check and Backup ---
357+ if [ $playwright_exit_code -ne 0 ] || [ -z "$token" ]; then
358+ echo "❌ Could not retrieve a token. Aborting." >&2
240359 return 1
241360 fi
242361
243- local token
244- token=$(echo "$full_output" | tail -n 1 | tr -d '[:space:]')
245-
246- echo "✅ Browser automation successful. Token retrieved: ${token}"
247-
248- # --- 5. Connect and Backup ---
362+ echo "✅ Browser automation successful. Token retrieved."
249363 echo "📞 Connecting and starting backup with disembark-cli..."
250- "$DISEMBARK_CMD" connect "${target_url}" "${token}"
251- "$DISEMBARK_CMD" backup "${target_url}"
364+ if ! "$DISEMBARK_CMD" connect "${base_url}" "${token}"; then
365+ echo "❌ Error: Failed to connect using the retrieved token." >&2
366+ return 1
367+ fi
368+
369+ if ! "$DISEMBARK_CMD" backup "${base_url}"; then
370+ echo "❌ Error: Backup command failed after connecting." >&2
371+ return 1
372+ fi
252373
253374 echo "✨ Disembark process complete!"
254375}
0 commit comments