Generated banner for Rp Quick Woocommerce
Generated logo for Rp Quick Woocommerce

Rp Quick Woocommerce

WixOfficialPublished by Wix

  • Skill
  • Workflow
  • Automation and integrations
  • MIT
  • Low risk
  • Has scripts

From the author

Runs the bounded deterministic public WooCommerce quick-import adapter after a qualifying source probe.
— the description by Wix

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The rp-quick-woocommerce skill provides a structured, deterministic procedure for importing data from WooCommerce to Wix. It operates under a strict quick-import mode, relying on a predefined entity graph and public data probes rather than credentials or scraping. The process involves a sequence of preflight checks, planning, setup, extraction, and import, ensuring data coverage and locale accuracy through a contract-based system. It is designed to be used only when the system router identifies a WooCommerce source and mandates quick-mode processing.

What it does

  • Performs a bounded, deterministic import of WooCommerce entities.
  • Generates source coverage and locale discovery reports.
  • Validates data mapping against a predefined contract.
  • Supports dry-run execution for safe validation without live changes.
  • Handles fallback member creation for blog content.
  • Provides a mechanism to switch to standard mode if preflight checks fail.

When to use it

  • When the router specifies managementImportMode=quick for a WooCommerce source.
  • When you need a fast, deterministic migration of a specific entity graph.
  • When you have verified the source is WooCommerce with high confidence.
  • When you need to perform a dry run to verify import logic before live execution.

Advantages

  • Eliminates the need for sensitive WooCommerce credentials.
  • Ensures data consistency through strict contract enforcement.
  • Provides clear audit trails and completion reports.
  • Prevents unauthorized data scraping or LLM-based mapping errors.
  • Allows for manual override of missing locale data.

Usage examples

“Run the preflight check for the WooCommerce import project.”
The agent executes the preflight script to validate the project directory and generate the initial quick-mode configuration.
“Generate the import plan for the current WooCommerce project.”
The agent runs the plan command, creating source coverage and locale files, and validates the contract requirements.
“Perform a dry run of the WooCommerce import.”
The agent executes the import process with the --dry-run flag, generating reports without modifying any live artifacts.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/wix/rp-quick-woocommerce/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip rp-quick-woocommerce.zip -d .agents/skills

Documentation by Wix

rp-quick-woocommerce

Use only when the router selected this resource for managementImportMode=quick and the source probe identified woocommerce with high confidence. This adapter imports only the entity graph declared in quick-mode.json; it never uses WordPress/WooCommerce credentials, browser extraction, page scraping (beyond the one <html lang> read described below), plugin endpoints, or an LLM mapping step.

Run the adapter from the repository root:

node skills/wix-replatform/resources/rp-quick-woocommerce/scripts/quick-mode.js preflight <projectDir>
node skills/wix-replatform/resources/rp-quick-woocommerce/scripts/quick-mode.js plan <projectDir>
node skills/wix-replatform/resources/rp-quick-woocommerce/scripts/quick-mode.js setup-author <projectDir>
node skills/wix-replatform/resources/rp-quick-woocommerce/scripts/quick-mode.js extract <projectDir>
node skills/wix-replatform/resources/rp-quick-woocommerce/scripts/quick-mode.js import <projectDir> [--dry-run]

plan also writes discovery/source-coverage.json: one adapter-excluded row per excluded[] entry, each dispositioned as the adapter's declared exclusion and carrying the committed excludedCapabilities translation from quick-mode.json (a target capability, or source-scope.<name> when Wix has none). The plan refuses if that translation is missing, extra, duplicated or outside both namespaces, and stamps sourceCoverageContract: 1 in quick-mode/plan.json. import refuses to write its completion summary when the plan is missing or unstamped, or its coverage file is missing or does not reconcile (a declared exclusion must name an excluded[] entry of the committed contract, with its committed capability); re-run plan to fix any of these. The summary carries sourceCoverage[] and sourceCoverageStatus.

plan also writes discovery/source-locale.json through the shared locale writer (adapter quick-woocommerce) from two bounded public reads: the first Store API product's prices.currency_code and the storefront home page's <html lang>. Time zone and country are not public, so they are unknown; a read that fails is an explicit unknown, never a plan failure. The field names come from the Store API schema and HTML, not from a live read of any store.

Setup does block until all four locale values are known. Ask the merchant for each value the public reads do not expose, then edit that field in discovery/source-locale.json to {"state":"supplied","value":"<validated value>","evidence":"merchant input","suppliedBy":"<operator>"}. Re-run plan and setup. The shared writer preserves a valid supplied value while the corresponding public capture remains unknown; never fill it from the destination site or guess.

The first two commands create the authoritative quick-mode/preflight.json, quick-mode/plan.json, and execution/execution-manifest.json. Setup discovery consumes the quick plan; normal setup execution and execution approval are still mandatory before the extract/import run. Do not replace the deterministic entity list with data discovered from pages. If an endpoint preflight fails, persist the reported blocker and create a durable choice between stopping and standard mode. The standard-mode choice must carry this effect so the CLI applies the user's selection before it resumes:

{"kind":"decision","key":"managementImportMode","choices":{"switch_to_standard":"standard"}}

The setup-author command is a setup step: it idempotently creates the dedicated fallback Blog member and records only its ID as WIX_BLOG_FALLBACK_MEMBER_ID. It uses the shared rp-target-wix writer runtime and sends no email. The import entrypoint requires normal setup verification and that setup-provisioned value before creating posts.

A dry run (--dry-run) writes only execution/dry-run-completion-report.json and execution/dry-run-final-report.md (plus the shared client's dry-run request capture). It never creates or changes a live artifact: the live completion and final reports, the crosswalk, the limit-recovery log, the import-started marker, or the post audit log logs/quick-mode-audit.ndjson. The router therefore still sends the project to the live import after a dry run.

Files

scripts/quick-mode.js

#!/usr/bin/env node
'use strict';

// Public-only, read-only WooCommerce quick-mode capture. This deliberately contains no
// credentials, browser automation, or fallback endpoint guessing.
const capabilities = require('../../../lib/execution-capability');
const siteRegional = require('../../../lib/site-regional');
const wixAuth = require('../../../lib/wix-auth');
const documentUrlGuard = require('../../../lib/document-url-guard');
const catalogBaseline = require('../../../lib/catalog-baseline');
const fs = require('node:fs/promises');
const path = require('node:path');
const crypto = require('node:crypto');

const ADAPTER_DIR = path.resolve(__dirname, '..');
const CONTRACT_PATH = path.join(ADAPTER_DIR, 'quick-mode.json');
const CONTRACT = require(CONTRACT_PATH);
const sourceCoverage = require(path.join(ADAPTER_DIR, '..', '..', 'lib', 'source-coverage.js'));
const sourceRegional = require(path.join(ADAPTER_DIR, '..', '..', 'lib', 'source-regional.js'));
const { buildQuickWooCommerceRegional } = require(path.join(ADAPTER_DIR, '..', 'rp-source-wordpress', 'lib', 'regional-capture.js'));
const recovery = require(path.join(ADAPTER_DIR, '..', 'rp-quick-runtime', 'lib', 'limit-recovery.js'));
const importProgress = require(path.join(ADAPTER_DIR, '..', 'rp-quick-runtime', 'lib', 'import-progress.js'));
// On the public Store API this is a no-op — `src` there is already the original — so
// the URLs produced here do not change. It is called anyway because naming an image field
// directly is how a whole catalog once shipped as thumbnails: the same idiom got copied onto the
// authenticated wc/v3 payload, where `src` can be the smallest crop.
const { resolveImageUrls } = require(path.join(ADAPTER_DIR, '..', 'rp-source-wordpress', 'lib', 'wp-image-url.js'));
const { composeProductDescription } = require(path.join(ADAPTER_DIR, '..', 'rp-target-wix', 'lib', 'wix-build.js'));

async function readJson(filePath) {
  return JSON.parse(await fs.readFile(filePath, 'utf8'));
}
async function writeJson(filePath, value) {
  await fs.mkdir(path.dirname(filePath), { recursive: true });
  await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}
// A dry run never touches the live audit log.
async function appendAudit(projectDir, entry, dryRun = false) {
  if (dryRun) return;
  const file = path.join(projectDir, 'logs', 'quick-mode-audit.ndjson');
  await fs.mkdir(path.dirname(file), { recursive: true });
  await fs.appendFile(file, `${JSON.stringify({ timestamp: new Date().toISOString(), ...entry })}\n`, 'utf8');
}
function origin(value) {
  const url = new URL(value);
  if (!['http:', 'https:'].includes(url.protocol)) throw new Error('sourceUrl must use http or https');
  // Every capture below fetches this origin from the operator's machine: refuse loopback, private and
  // link-local (cloud metadata) hosts. Lexical only; it does not resolve DNS. RP_TRUSTED_ORIGINS (the
  // same variable the headless bundle uses) names origins the operator explicitly trusts, e.g. a local
  // test server.
  const trusted = String(process.env.RP_TRUSTED_ORIGINS || '').split(',').map((entry) => entry.trim()).filter(Boolean);
  if (!trusted.includes(url.origin) && documentUrlGuard.isPrivateHostname(url.hostname)) throw new Error('sourceUrl must be a public host, not a loopback, private or link-local address');
  return url.origin;
}
function fingerprint(value) {
  return crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
}
async function projectContext(projectDir) {
  const decisions = await readJson(path.join(projectDir, 'orchestration', 'decisions.json'));
  const sourceUrl = decisions.sourceUrl && decisions.sourceUrl.value;
  const sourcePlatform = decisions.sourcePlatform && decisions.sourcePlatform.value;
  if (!sourceUrl) throw new Error('quick mode requires orchestration.decisions.sourceUrl');
  if (sourcePlatform !== 'woocommerce') throw new Error(`quick WooCommerce adapter requires sourcePlatform=woocommerce (got ${sourcePlatform || 'missing'})`);
  return { sourceUrl: origin(sourceUrl), decisions };
}
async function fetchCollection(baseUrl, route) {
  const records = [];
  const pages = [];
  for (let page = 1; ; page += 1) {
    const url = new URL(route, `${baseUrl}/`);
    url.searchParams.set('page', String(page));
    url.searchParams.set('per_page', '100');
    const response = await fetch(url, { headers: { accept: 'application/json' } });
    const text = await response.text();
    let body;
    try { body = text ? JSON.parse(text) : null; } catch { body = null; }
    if (!response.ok || !Array.isArray(body)) {
      return { ok: false, status: response.status, url: url.toString(), bodyType: Array.isArray(body) ? 'array' : typeof body, records, pages };
    }
    records.push(...body);
    pages.push({ page, count: body.length, url: url.toString() });
    const totalPages = Number(response.headers.get('x-wp-totalpages'));
    if (Number.isFinite(totalPages) ? page >= totalPages : body.length < 100) break;
  }
  return { ok: true, records, pages };
}
async function preflight(projectDir) {
  const { sourceUrl } = await projectContext(projectDir);
  const checks = [];
  for (const entity of CONTRACT.entities) {
    const result = await fetchCollection(sourceUrl, entity.route);
    checks.push({ entity: entity.id, route: entity.route, ok: result.ok, status: result.status || 200, url: result.url || result.pages[0]?.url || null, responseShape: result.ok ? 'array' : result.bodyType });
  }
  const out = { schemaVersion: 1, generatedAt: new Date().toISOString(), adapter: { id: CONTRACT.id, version: CONTRACT.version, fingerprint: fingerprint(CONTRACT) }, sourceUrl, status: checks.every((check) => check.ok) ? 'passed' : 'blocked', checks };
  await writeJson(path.join(projectDir, 'quick-mode', 'preflight.json'), out);
  return out;
}
// Two bounded public GETs -- the first Store API product and the storefront home page -- for the
// store's currency and language. A read that fails is an explicit unknown in the file, never a
// plan failure.
async function readPublic(url, accept, parse) {
  try {
    const response = await fetch(url, { headers: { accept }, signal: AbortSignal.timeout(15000) });
    return response.ok ? parse(await response.text()) : null;
  } catch { return null; }
}
async function captureRegional(projectDir, sourceUrl, generatedAt) {
  const products = await readPublic(new URL('/wp-json/wc/store/v1/products?per_page=1', sourceUrl), 'application/json', (body) => { const parsed = JSON.parse(body); return Array.isArray(parsed) ? parsed : null; });
  const page = await readPublic(new URL('/', sourceUrl), 'text/html', (html) => ({ html }));
  return sourceRegional.writeSourceRegional(projectDir, buildQuickWooCommerceRegional({ products, page, generatedAt }));
}
async function plan(projectDir) {
  const { sourceUrl } = await projectContext(projectDir);
  const preflightResult = await readJson(path.join(projectDir, 'quick-mode', 'preflight.json'));
  if (preflightResult.status !== 'passed') throw new Error('quick mode preflight must pass before planning');
  const adapter = { id: CONTRACT.id, version: CONTRACT.version, fingerprint: fingerprint(CONTRACT) };
  const generatedAt = new Date().toISOString();
  // Every excluded[] capability becomes a declared-exclusion coverage row, checked by the same
  // reconcile rule as a full run before the plan is written.
  const coverage = await sourceCoverage.writeQuickPlanCoverage(projectDir, CONTRACT, { rawText: await fs.readFile(CONTRACT_PATH, 'utf8'), generatedAt });
  await captureRegional(projectDir, sourceUrl, generatedAt);
  const planResult = { schemaVersion: 1, generatedAt, managementImportMode: 'quick', adapter, sourceUrl, entities: CONTRACT.entities, requiredWixCapabilities: CONTRACT.requiredWixCapabilities, excluded: CONTRACT.excluded, excludedCapabilities: CONTRACT.excludedCapabilities, sourceAuth: 'none', sourceCoverageContract: coverage.sourceCoverageContract };
  await writeJson(path.join(projectDir, 'quick-mode', 'plan.json'), planResult);
  await writeJson(path.join(projectDir, 'execution', 'execution-manifest.json'), { schemaVersion: 1, generatedAt: planResult.generatedAt, managementImportMode: 'quick', adapter, extractCommand: 'rp-quick-woocommerce extract', importCommand: 'rp-quick-woocommerce import', entities: CONTRACT.entities.map((entity) => entity.id) });
  return planResult;
}
async function extract(projectDir) {
  const { sourceUrl } = await projectContext(projectDir);
  const planResult = await readJson(path.join(projectDir, 'quick-mode', 'plan.json'));
  if (planResult.adapter.fingerprint !== fingerprint(CONTRACT)) throw new Error('quick adapter contract changed; re-run preflight and plan');
  const entities = {};
  for (const entity of CONTRACT.entities) {
    const result = await fetchCollection(sourceUrl, entity.route);
    if (!result.ok) throw new Error(`${entity.id} extraction failed: ${result.status} ${result.url}`);
    const file = path.join(projectDir, 'data', 'source-extract', `${entity.id}.ndjson`);
    await fs.mkdir(path.dirname(file), { recursive: true });
    await fs.writeFile(file, result.records.map((record) => JSON.stringify(record)).join('\n') + (result.records.length ? '\n' : ''), 'utf8');
    entities[entity.id] = { file: path.relative(projectDir, file), route: entity.route, recordCount: result.records.length, pages: result.pages };
  }
  const manifest = { schemaVersion: 1, generatedAt: new Date().toISOString(), managementImportMode: 'quick', adapter: planResult.adapter, sourceUrl, entities };
  await writeJson(path.join(projectDir, 'data', 'source-extract', 'manifest.json'), manifest);
  return manifest;
}
function nonEmpty(value) { const text = value == null ? '' : String(value).trim(); return text || undefined; }
function text(value) { return String(value || '').replace(/<[^>]*>/g, '').replace(/&amp;/g, '&').replace(/&quot;/g, '"').trim(); }
function slug(value) { return nonEmpty(value)?.toLowerCase().replace(/[_\s]+/g, '-').replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || undefined; }
function price(value, minor = 2) { const n = Number(value); return Number.isFinite(n) ? (n / (10 ** Number(minor || 2))).toFixed(Number(minor || 2)) : '0.00'; }
async function readLines(filePath) { const raw = await fs.readFile(filePath, 'utf8'); return raw.split(/\r?\n/).filter(Boolean).map(JSON.parse); }
async function loadWixEnv(projectDir) {
  const values = {};
  try {
    for (const line of (await fs.readFile(path.join(projectDir, 'config', 'wix.env'), 'utf8')).split(/\r?\n/)) {
      const m = line.trim().match(/^([A-Z0-9_]+)=(.*)$/); if (m) values[m[1]] = m[2].replace(/^['"]|['"]$/g, '').trim();
    }
  } catch (error) { if (error.code !== 'ENOENT') throw error; }
  return { ...values, ...process.env };
}
async function loadCrosswalk(projectDir) {
  try { return await readJson(path.join(projectDir, 'state', 'crosswalk', 'quick-mode.json')); }
  catch (error) { if (error.code === 'ENOENT') return {}; throw error; }
}
// A token from config/wix.env renews itself for this site; a process-env override is used as given.
function wixCredentials(projectDir, env, dryRun) { return dryRun || process.env.WIX_AUTH_TOKEN ? { authToken: env.WIX_AUTH_TOKEN } : { tokenSource: wixAuth.createWixTokenSource({ projectDir, siteId: env.WIX_SITE_ID }) }; }
async function ensureFallbackAuthor(projectDir) {
  const env = await loadWixEnv(projectDir);
  if (env.WIX_BLOG_FALLBACK_MEMBER_ID) return { status: 'already_configured', memberId: env.WIX_BLOG_FALLBACK_MEMBER_ID };
  if (!env.WIX_AUTH_TOKEN || !env.WIX_SITE_ID) throw new Error('WIX_AUTH_TOKEN and WIX_SITE_ID are required to provision the Blog fallback author');
  const writers = require(path.join(ADAPTER_DIR, '..', 'rp-target-wix', 'lib', 'wix-writers.js'));
  const dryRun = writers.createDryRunConfig(env, process.argv.slice(2)).dryRun;
  if (dryRun) return { status: 'planned_dry_run', memberId: null };
  const quickRun = capabilities.startQuickRun({ projectDir, adapterId: 'quick-woocommerce', siteId: env.WIX_SITE_ID, dryRun: false });
  const wix = writers.createWixClient({ capability: capabilities.bindQuickSetup(quickRun, 'blog-fallback-author'), ...wixCredentials(projectDir, env, false) });
  const projectId = path.basename(projectDir).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'migration';
  const email = `replatform+${projectId}@wix.com`;
  const listed = await writers.listMembers(wix, { limit: 100 });
  let member = (listed.members || []).find((item) => String(item.loginEmail || '').toLowerCase() === email);
  if (!member) member = await writers.ensureBlogFallbackAuthor(wix, { email, name: 'Imported content', slug: `imported-content-${projectId}` });
  if (!member || !member.id) throw new Error('fallback Blog author was not created');
  const configPath = path.join(projectDir, 'config', 'wix.env');
  let raw = ''; try { raw = await fs.readFile(configPath, 'utf8'); } catch (error) { if (error.code !== 'ENOENT') throw error; }
  await fs.mkdir(path.dirname(configPath), { recursive: true });
  await fs.writeFile(configPath, `${raw.replace(/\n?WIX_BLOG_FALLBACK_MEMBER_ID=.*(?:\n|$)/g, '\n').replace(/\s*$/, '\n')}WIX_BLOG_FALLBACK_MEMBER_ID=${member.id}\n`, 'utf8');
  return { status: 'provisioned', memberId: member.id };
}
async function importData(projectDir) {
  const planResult = await readJson(path.join(projectDir, 'quick-mode', 'plan.json'));
  const manifest = await readJson(path.join(projectDir, 'data', 'source-extract', 'manifest.json'));
  importProgress.assertQuickImportAdapter({ plan: planResult, manifest, current: { id: CONTRACT.id, version: CONTRACT.version, fingerprint: fingerprint(CONTRACT) } });
  // Fail closed before any write: a stamped plan with a missing or unreconciled coverage file
  // must not produce a completion summary.
  await sourceCoverage.quickSummaryCoverage(projectDir);
  const env = await loadWixEnv(projectDir);
  if (!env.WIX_AUTH_TOKEN || !env.WIX_SITE_ID) throw new Error('WIX_AUTH_TOKEN and WIX_SITE_ID are required for quick import');
  const writers = require(path.join(ADAPTER_DIR, '..', 'rp-target-wix', 'lib', 'wix-writers.js'));
  const dryRun = writers.createDryRunConfig(env, process.argv.slice(2)).dryRun;
  const quickRun = capabilities.startQuickRun({ projectDir, adapterId: 'quick-woocommerce', siteId: env.WIX_SITE_ID, dryRun });
  // The site's currency (and time zone) is checked before an entity that writes money or wall-clock
  // records gets its write capability. A refused entity gets no client and writes nothing; whatever
  // depends on it is skipped too, and every other entity still imports.
  const regionalRefused = {};
  const clientFor = async (id) => {
    const entity = CONTRACT.entities.find((row) => row.id === id);
    const upstream = entity.dependsOn.find((dependency) => regionalRefused[dependency]);
    if (upstream) { regionalRefused[id] = 'DEPENDENCY_REFUSED'; return null; }
    // Every other entity builds on the regional setup step: a held entity writes nothing (files only; no site read).
    const dependency = siteRegional.regionalDependency({ projectDir, siteId: env.WIX_SITE_ID, entityRefs: [entity.target, ...(entity.auxiliaryTargets || [])] });
    if (!dependency.ok) { regionalRefused[id] = dependency.code; return null; }
    return writers.createWixClient({ capability: capabilities.bindQuickEntity(quickRun, id), ...wixCredentials(projectDir, env, dryRun) });
  };
  const sourceDir = path.join(projectDir, 'data', 'source-extract');
  const crosswalk = await loadCrosswalk(projectDir);
  // Durable pre-write evidence, before the first destination create. Some
  // crosswalk checkpoints are deferred (blog tags, products), so the crosswalk cannot serve.
  if (!dryRun) await importProgress.writeQuickImportStartedMarker(projectDir, planResult.adapter);
  const seen = (entity, sourceId) => crosswalk[entity] && crosswalk[entity][String(sourceId)];
  const remember = (entity, sourceId, targetId) => { if (!dryRun) { crosswalk[entity] ||= {}; crosswalk[entity][String(sourceId)] = targetId; } };
  const checkpointCrosswalk = async () => { if (!dryRun) await writeJson(path.join(projectDir, 'state', 'crosswalk', 'quick-mode.json'), crosswalk); };
  const summary = { schemaVersion: 1, generatedAt: new Date().toISOString(), managementImportMode: 'quick', adapter: planResult.adapter, dryRun, imported: {}, recovered: 0, recoveryCounts: {}, excluded: CONTRACT.excluded, regionalRefused };
  const categoryIds = new Map(); const productTagIds = new Map(); const blogCategoryIds = new Map(); const blogTagIds = new Map();
  // A source record whose slug matches an item that was on the store before this import is held,
  // not adopted and not created; the completion report turns these into operator actions.
  const holdCollision = (entity, sourceEntity, sourceId, target) => {
    if (!dryRun) catalogBaseline.recordCollision(projectDir, { entity, sourceKey: String(sourceId), targetId: target.id, slug: target.slug ?? null });
    summary.skipped ||= {}; summary.skipped[sourceEntity] ||= [];
    summary.skipped[sourceEntity].push({ sourceId: String(sourceId), reason: catalogBaseline.collisionError(entity, target.id).message });
  };
  let wix = await clientFor('product_category');
  if (wix) {
    // Categories that were on the store before this import are never adopted by slug.
    const baselineCategoryIds = catalogBaseline.protectedIds(projectDir).categories;
    const destinationCategories = await writers.queryAllStoresCategories(wix);
    const heldCategories = new Map(destinationCategories.filter((item) => baselineCategoryIds.has(item.id)).map((item) => [slug(item.slug), item]));
    const existingCategories = new Map(destinationCategories.filter((item) => !baselineCategoryIds.has(item.id)).map((item) => [slug(item.slug), item]));
    for (const row of await readLines(path.join(sourceDir, 'product_category.ndjson'))) {
      if (seen('product_category', row.id) && baselineCategoryIds.has(seen('product_category', row.id))) { holdCollision('stores/category', 'product_category', row.id, { id: seen('product_category', row.id) }); continue; }
      if (seen('product_category', row.id)) { categoryIds.set(String(row.id), seen('product_category', row.id)); continue; }
      const sourceSlug = slug(row.slug); const existing = existingCategories.get(sourceSlug);
      if (!existing && heldCategories.has(sourceSlug)) { holdCollision('stores/category', 'product_category', row.id, heldCategories.get(sourceSlug)); continue; }
      const target = existing || await writers.createStoresCategory(wix, { name: text(row.name), slug: sourceSlug, description: nonEmpty(text(row.description).slice(0, 600)), treeReference: writers.STORES_TREE_REFERENCE }); categoryIds.set(String(row.id), target.id);
      remember('product_category', row.id, target.id); if (!dryRun) catalogBaseline.clearCollision(projectDir, 'stores/category', row.id); await checkpointCrosswalk();
    }
    summary.imported.product_category = categoryIds.size;
    capabilities.finishQuickEntity(quickRun, 'product_category');
  }
  wix = await clientFor('product_tag');
  if (wix) {
    const existingProductTags = new Map(((await wix.send(writers.buildListStoresProductTagsRequest())).tags || []).map((item) => [slug(item.name), item]));
    for (const row of await readLines(path.join(sourceDir, 'product_tag.ndjson'))) {
      if (seen('product_tag', row.id)) { productTagIds.set(String(row.id), seen('product_tag', row.id)); continue; }
      const key = slug(row.name); const existing = existingProductTags.get(key);
      const response = existing || (await wix.send(writers.buildCreateStoresProductTagRequest(text(row.name)))).tag;
      productTagIds.set(String(row.id), response.id); remember('product_tag', row.id, response.id); await checkpointCrosswalk();
    }
    summary.imported.product_tag = productTagIds.size;
    capabilities.finishQuickEntity(quickRun, 'product_tag');
  }
  wix = await clientFor('blog_category');
  if (wix) {
    const existingBlogCategories = new Map(((await wix.send(writers.buildQueryBlogCategoriesRequest())).categories || []).map((item) => [slug(item.slug), item]));
    for (const row of await readLines(path.join(sourceDir, 'blog_category.ndjson'))) { if (seen('blog_category', row.id)) { blogCategoryIds.set(String(row.id), seen('blog_category', row.id)); continue; } const existing = existingBlogCategories.get(slug(row.slug)); const created = existing || await writers.createBlogCategory(wix, { label: text(row.name), slug: slug(row.slug), description: nonEmpty(text(row.description)) }); blogCategoryIds.set(String(row.id), created.id); remember('blog_category', row.id, created.id); await checkpointCrosswalk(); }
    summary.imported.blog_category = blogCategoryIds.size;
    capabilities.finishQuickEntity(quickRun, 'blog_category');
  }
  wix = await clientFor('blog_tag');
  if (wix) {
    for (const row of await readLines(path.join(sourceDir, 'blog_tag.ndjson'))) { if (seen('blog_tag', row.id)) { blogTagIds.set(String(row.id), seen('blog_tag', row.id)); continue; } const created = await writers.createBlogTag(wix, { label: text(row.name) }); blogTagIds.set(String(row.id), created.id); remember('blog_tag', row.id, created.id); }
    summary.imported.blog_tag = blogTagIds.size;
    capabilities.finishQuickEntity(quickRun, 'blog_tag');
  }
  wix = await clientFor('post');
  if (wix) {
    const fallbackMemberId = env.WIX_BLOG_FALLBACK_MEMBER_ID;
    if (!fallbackMemberId) throw new Error('setup must provision WIX_BLOG_FALLBACK_MEMBER_ID before importing blog posts');
    let posts = 0;
    for (const row of await readLines(path.join(sourceDir, 'post.ndjson'))) {
      if (seen('post', row.id)) { posts += 1; continue; }
      await appendAudit(projectDir, { phase: 'post', sourceId: String(row.id), state: 'creating_draft' }, dryRun);
      const draft = await writers.createDraftPost(wix, { title: text(row.title?.rendered), memberId: fallbackMemberId, richContent: await writers.convertHtmlToRichContent(wix, row.content?.rendered || '<p></p>'), excerpt: text(row.excerpt?.rendered), slug: slug(row.slug), categoryIds: (row.categories || []).map((id) => blogCategoryIds.get(String(id))).filter(Boolean), tagIds: (row.tags || []).map((id) => blogTagIds.get(String(id))).filter(Boolean), firstPublishedDate: Number.isNaN(new Date(row.date).valueOf()) ? undefined : new Date(row.date).toISOString() });
      await appendAudit(projectDir, { phase: 'post', sourceId: String(row.id), state: 'publishing_draft', targetId: draft.id }, dryRun);
      if (!dryRun) await writers.publishDraftPost(wix, draft.id); remember('post', row.id, draft.id); posts += 1;
      await checkpointCrosswalk(); await appendAudit(projectDir, { phase: 'post', sourceId: String(row.id), state: 'completed', targetId: draft.id }, dryRun);
    }
    summary.imported.post = posts;
    capabilities.finishQuickEntity(quickRun, 'post');
  }
  wix = await clientFor('product');
  if (wix) {
    let products = 0;
    // A process can stop after a product create succeeds but before the crosswalk is
    // checkpointed. Build a complete destination slug index up front so a resumed run
    // recovers that write instead of treating the source record as a new product.
    const baselineProductIds = catalogBaseline.protectedIds(projectDir).products;
    const destinationProducts = await writers.queryAllStoresProducts(wix);
    const heldProducts = new Map(destinationProducts.filter((item) => baselineProductIds.has(item.id)).map((item) => [slug(item.slug), item]));
    const existingProducts = new Map(destinationProducts.filter((item) => !baselineProductIds.has(item.id)).map((item) => [slug(item.slug), item]));
    const productRows = await readLines(path.join(sourceDir, 'product.ndjson'));
    const productSlugCounts = new Map();
    for (const row of productRows) {
      const key = slug(row.slug);
      productSlugCounts.set(key, (productSlugCounts.get(key) || 0) + 1);
    }
    for (const row of productRows) {
      // A saved target id is checked too: an earlier run may have adopted a baseline product.
      if (seen('product', row.id) && baselineProductIds.has(seen('product', row.id))) { holdCollision('stores/product', 'product', row.id, destinationProducts.find((item) => item.id === seen('product', row.id)) || { id: seen('product', row.id) }); continue; }
      if (seen('product', row.id)) { products += 1; continue; }
      const baseSlug = slug(row.slug);
      const sourceSlug = productSlugCounts.get(baseSlug) > 1 ? `${baseSlug}-${row.id}` : baseSlug;
      const heldProduct = !existingProducts.has(sourceSlug) && heldProducts.get(sourceSlug);
      if (heldProduct) { holdCollision('stores/product', 'product', row.id, heldProduct); continue; }
      const actualPrice = price(row.prices?.price, row.prices?.currency_minor_unit); const regular = price(row.prices?.regular_price, row.prices?.currency_minor_unit);
      const normalized = recovery.normalizeProduct({ sourceId: row.id, sku: nonEmpty(row.sku) || '', tagIds: (row.tags || []).map((tag) => productTagIds.get(String(tag.id))).filter(Boolean) });
      const created = existingProducts.get(sourceSlug) || (productSlugCounts.get(baseSlug) > 1 && existingProducts.get(baseSlug)) || await writers.createStoresProduct(wix, { name: text(row.name), slug: sourceSlug, description: composeProductDescription({ description: row.description, shortDescription: row.short_description }), productType: 'PHYSICAL', visible: true, tags: { publicTags: { tagIds: normalized.tagIds } }, media: { itemsInfo: { items: resolveImageUrls(row.images).map(({ url, altText, displayName }) => ({ url, ...(altText ? { altText } : {}), ...(displayName ? { displayName } : {}) })) } }, variantsInfo: { variants: [{ visible: true, sku: normalized.sku || undefined, inStock: row.is_in_stock !== false, price: { actualPrice: { amount: actualPrice }, ...(Number(regular) > Number(actualPrice) ? { compareAtPrice: { amount: regular } } : {}) } }] } });
      if (normalized.recoveries.length) { if (!dryRun) await recovery.appendRecoveries(projectDir, planResult.adapter, row.id, normalized.recoveries); recovery.recordRecoveredRecord(summary, normalized.recoveries); }
      const targets = (row.categories || []).map((category) => categoryIds.get(String(category.id))).filter(Boolean); if (targets.length) await writers.bulkAddItemToCategories(wix, { productId: created.id, categoryIds: targets }); remember('product', row.id, created.id); if (!dryRun) catalogBaseline.clearCollision(projectDir, 'stores/product', row.id); products += 1;
    }
    summary.imported.product = products;
    capabilities.finishQuickEntity(quickRun, 'product');
  }
  if (!dryRun) await writeJson(path.join(projectDir, 'state', 'crosswalk', 'quick-mode.json'), crosswalk);
  // A refusal means the run did not import everything: it is never reported completed (as in Shopify's quick mode).
  if (!dryRun && Object.keys(regionalRefused).length) summary.status = 'partial';
  Object.assign(summary, await sourceCoverage.quickSummaryCoverage(projectDir));
  catalogBaseline.quickReport(projectDir, summary);
  await recovery.writeFinalReport(projectDir, summary, { dryRun });
  await writeJson(path.join(projectDir, 'execution', dryRun ? 'dry-run-completion-report.json' : 'completion-report.json'), summary);
  return summary;
}
async function main() {
  const [command, projectDirArg] = process.argv.slice(2);
  if (!command || !projectDirArg || !['preflight', 'plan', 'setup-author', 'extract', 'import'].includes(command)) throw new Error('Usage: quick-mode.js <preflight|plan|setup-author|extract|import> <projectDir>');
  const result = await ({ preflight, plan, 'setup-author': ensureFallbackAuthor, extract, import: importData })[command](path.resolve(projectDirArg));
  process.stdout.write(`${JSON.stringify({ ok: true, command, status: result.status || 'completed' })}\n`);
}
main().catch(async (error) => { process.stderr.write(`${error.message}\n`); process.exit(await wixAuth.stopExitCode(error, process.argv[3] && path.resolve(process.argv[3]))); });

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Node.js

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Wix
Publisher
Wix (Official)
Source
wix/skills · skills/wix-replatform/resources/rp-quick-woocommerce · commit d168bc2
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Wix.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download rp-quick-woocommerce.zip