wp-guided-tour — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited wp-guided-tour (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Model note: IIFE bundle setup and PHP config scaffolding are mechanical (haiku). JS scope detection from URL + hash, and debugging selector mismatches against live DOM, needsonnet.
Not for: Front-end-only SPAs or non-WordPress JS apps — requires WP admin backend context. Guided tours in themes — this skill targets plugin-owned admin pages only.
Download the Driver.js v1 IIFE build (NOT the ESM build):
driver.js.iife.js → assets/admin/js/driverjs/driver.js.iife.jsdriver.css → assets/admin/js/driverjs/driver.cssThe IIFE build exposes window.driver.js.driver (double namespace). Always call it as:
window.driver.js.driver({ ... })// Enqueue on all admin pages (is_admin() block)
$this->enqueue_script(
'shopflow_guided_tour_driverjs',
SHOPFLOW_ASSETS_URL . 'admin/js/driverjs/driver.js.iife.js',
array()
);
$this->enqueue_style(
'shopflow_guided_tour_driverjs',
SHOPFLOW_ASSETS_URL . 'admin/js/driverjs/driver.css',
array()
);
$this->enqueue_script(
'shopflow_guided_tour',
SHOPFLOW_ASSETS_URL . 'admin/js/guided-tour.js',
array( 'shopflow_guided_tour_driverjs' )
);
// Add tour configs to the main localized object
$localized['tours'] = shopflow_get_tour_configs();The $localized array must be passed to localize_script() on the main SPA script (not the tour script) so window.SHOPFLOW.tours is available before guided-tour.js runs.
functions.php)function shopflow_get_tour_configs() {
return apply_filters( 'shopflow_tour_configs', array(
'dashboard' => array(
'autoStart' => true, // only one scope should be true
'pages' => array( 'shopflow' ),
'steps' => array(
array(
// Centered popover — no element key
'popover' => array(
'title' => __( 'Welcome!', 'my-plugin' ),
'description' => __( 'Quick intro text.', 'my-plugin' ),
'side' => 'bottom',
),
),
array(
// Element-targeted step
'element' => '#my-stable-id',
'popover' => array(
'title' => __( 'Step Title', 'my-plugin' ),
'description' => __( 'Step description.', 'my-plugin' ),
'side' => 'right',
),
),
),
),
) );
}Rules:
autoStart: true (the primary onboarding page)__() on title and description — run makepot after adding new stepsside values: top, bottom, left, rightalign: 'start' — it's the default; explicit is noisepages array is metadata only; actual detection is done by JS getCurrentScope()guided-tour.js)function getCurrentScope() {
const urlParams = new URLSearchParams(window.location.search);
const page = urlParams.get('page');
if (!page || !page.startsWith('myprefix')) return null;
// Non-SPA pages (full page reloads)
if (page === 'myprefix-settings') return 'settings';
if (page === 'myprefix-wizard') return 'wizard';
// Main SPA — differentiate by hash route
// Strip pagination suffix like /page/2
const hash = window.location.hash.replace('#', '').replace(/\/page\/\d+$/, '');
if (!hash || hash === '/' || hash === '/dashboard') return 'dashboard';
if (hash.startsWith('/products/add')) return 'add-product';
if (hash === '/orders/new') return 'create-order';
if (hash === '/orders') return 'orders';
if (hash === '/customers') return 'customers';
if (hash.startsWith('/reports')) return 'reports';
return null;
}Key points:
page.startsWith('myprefix') — NOT page.startsWith('myprefix-') (would miss the bare slug page=myprefix).startsWith) for pages with sub-routeshashchange listener re-runs scope detection for SPA navigation: window.addEventListener('hashchange', () => setTimeout(autoStartTours, 500));let currentTour = null;
function startTour(scope = null) {
if (!window.driver?.js?.driver) {
console.warn('Driver.js not loaded');
return false;
}
const targetScope = scope || getCurrentScope();
if (!targetScope || !window.SHOPFLOW?.tours[targetScope]) return false;
if (currentTour) currentTour.destroy();
const steps = window.SHOPFLOW.tours[targetScope].steps;
const lastIndex = steps.length - 1;
// Inject completion tracking ONLY on the final step's Next/Done click.
// onDestroyed fires for BOTH completion AND early dismiss — do NOT use it
// for completion tracking.
const stepsWithCompletion = steps.map((step, i) => {
if (i !== lastIndex) return step;
return {
...step,
popover: {
...step.popover,
onNextClick: () => {
markTourCompleted(targetScope);
currentTour.destroy();
},
},
};
});
currentTour = window.driver.js.driver({
showProgress: true,
smoothScroll: true,
showButtons: ['next', 'previous', 'close'],
steps: stepsWithCompletion,
onDestroyed: () => { currentTour = null; },
});
setTimeout(() => currentTour.drive(), 100);
return true;
}Critical: onDestroyed fires on close AND completion. Never call markTourCompleted there. Inject it into the last step's onNextClick only.
| Pattern | Good/Bad | Reason |
|---|---|---|
#my-stable-id | ✅ | Most stable |
.unique-class-combo | ✅ | Stable if combo is unique |
.my-class:first-of-type | ❌ | :first-of-type matches by tag, not class |
:nth-child(2) | ❌ | Breaks on DOM reorder |
| Tailwind responsive variants | ⚠️ | Need backslash escaping in PHP strings |
Tailwind escaping in PHP:
// CSS selector: .border-[#F0EDFB]
// In PHP string:
'element' => '.border-\\[\\#F0EDFB\\]',Test every selector in browser console before committing:
!!document.querySelector('.my-selector') // must return true on target pageImportant: Test each selector on its OWN page. A selector for the orders tour will return false on the dashboard — that's expected.
After implementing tours, verify in browser:
// 1. All tours loaded
Object.keys(window.SHOPFLOW.tours) // should list all scopes
// 2. Scope detection works on current page
getCurrentScope() // should return expected scope string
// 3. All selectors resolve (run on EACH tour's own page)
window.SHOPFLOW.tours['my-scope'].steps
.filter(s => s.element)
.map(s => ({ el: s.element, found: !!document.querySelector(s.element) }))
// 4. Tour renders
localStorage.clear()
startTour('my-scope')
// 5. Completion tracking — click Done on last step
localStorage.getItem('myprefix_my-scope_tour_completed') // → "true"
// 6. Dismiss tracking — restart, click X on step 1
// localStorage key must NOT be setcomposer run makepot — all __() strings in tour configs must be in .potshopflow_get_tour_configs() has a matching case in getCurrentScope()autoStart: truesmoothScroll: true in driver config (prevents jarring jumps on long pages)align: 'start' in steps (redundant default)references/scope-detection.md — getCurrentScope() patterns for URL-only and hash-routed SPA pages, common pitfalls.references/php-tour-config.md — Full PHP config shape with all fields, Tailwind selector escaping, and filter pattern.references/driver-js-lifecycle.md — startTour() implementation with correct completion tracking, autoStartTours() pattern, IIFE namespace.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.