salesive-dev-tools
v0.3.0
Published
Salesive development tools — Vite plugin, React hooks, and the App Bridge for embedded Salesive apps
Maintainers
Readme
Salesive Development Tools
A powerful CLI tool and development toolkit for building, managing, and deploying Salesive templates.
Features
- 🎨 Template Management: Create, validate, and deploy Salesive templates
- 🔐 API Authentication: Secure authentication with Salesive Themes API
- ✅ Configuration Validation: Validate your template configuration files
- 🚀 One-Command Deploy: Package and deploy templates with
cook - 🔄 Auto-Update Check: Automatically notifies you of new versions
- 🔧 Vite Plugin: Inject configuration into your React applications
- 🪝 React Hooks: Easy access to configuration data in components
- 🔑 App Bridge: Runtime permission requests and lifecycle events for embedded Salesive apps
Installation
Global Installation (Recommended for CLI)
npm install -g salesive-dev-tools
# or
yarn global add salesive-dev-tools
# or
pnpm add -g salesive-dev-tools
# or
bun add -g salesive-dev-toolsLocal Installation (For Vite Plugin)
npm install salesive-dev-tools
# or
yarn add salesive-dev-tools
# or
pnpm add salesive-dev-toolsUsage
Setup in Vite Configuration
// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { salesiveConfigPlugin } from "salesive-dev-tools";
export default defineConfig({
plugins: [react(), salesiveConfigPlugin()],
});Create a Salesive Config File
Create a salesive.config.json file in the root of your project:
{
"name": "my-project",
"version": "1.0.0",
"description": "My Project Description",
"variables": {
"color-brand-primary": "#0d65d9",
"color-brand-primary-x": "#e6f6ff",
"font-brand-space": "Space Grotesk",
"app-name": "My App",
"app-description": "My awesome application",
"app-logo": "https://example.com/logo.png",
"app-favicon": "https://example.com/favicon.ico"
}
}Automatic Routes and routeOverrides
Some paths are automatic routes — Salesive serves them itself, so the dev server hands them to the platform app instead of your theme:
/cart /checkout /account /orders /payment /invoice /settings
/login /logout /privacy-policy /terms /about-us /return-policyA theme can claim one of these and render it itself by listing it in
routeOverrides. The dev server honours this, so what you see locally matches what
the platform serves after salesive cook:
{
"name": "my-theme",
"version": "1.0.0",
"routeOverrides": ["/blog", "/orders"]
}An override owns the whole subtree — declaring /orders also gives you
/orders/:id. Only automatic routes can be overridden; any other value is ignored.
Once you claim a route, that path no longer falls back to Salesive's built-in page,
so you must implement it (and its data fetching) in your own router.
Using the React Hook
import { useSalesiveConfig } from "salesive-dev-tools";
function MyComponent() {
// Get a specific variable value
const appName = useSalesiveConfig("app-name");
const brandColor = useSalesiveConfig("color-brand-primary");
// Or get all variables
const allVariables = useSalesiveConfig();
return (
<div style={{ color: brandColor }}>
<h1>{appName}</h1>
</div>
);
}Dynamic Configuration Updates
You can update the Salesive configuration at runtime and all components using the configuration will automatically update:
import { updateSalesiveConfig, setSalesiveConfig } from "salesive-dev-tools";
// Update a specific variable
setSalesiveConfig("app-name", "My Updated App Name");
// Update multiple variables at once (merges with existing config)
updateSalesiveConfig({
variables: {
"color-brand-primary": "#ff0000",
"app-description": "Updated description",
},
});
// Use a function to update based on previous state
updateSalesiveConfig((prevConfig) => ({
...prevConfig,
variables: {
...prevConfig.variables,
"app-name": `${prevConfig.variables["app-name"]} - Updated`,
},
}));
// Replace the entire config (no merging)
updateSalesiveConfig(newConfig, { merge: false });
// Advanced usage with options
updateSalesiveConfig(newConfig, {
merge: true, // Whether to merge with existing config (default: true)
notify: true, // Whether to notify components to update (default: true)
});Integration with React Router
You can use dynamic configuration with React Router to create route-specific themes:
import { Outlet, useLocation } from "react-router-dom";
import { setSalesiveConfig } from "salesive-dev-tools";
// Route-specific theme switcher
function ThemeManager() {
const location = useLocation();
useEffect(() => {
// Update theme based on current route
if (location.pathname.startsWith("/admin")) {
setSalesiveConfig("color-brand-primary", "#2d3748");
} else if (location.pathname.startsWith("/marketing")) {
setSalesiveConfig("color-brand-primary", "#e53e3e");
} else {
setSalesiveConfig("color-brand-primary", "#0d65d9");
}
}, [location.pathname]);
return <Outlet />;
}Using the Helper Function (for non-component code)
import { getSalesiveConfig } from "salesive-dev-tools";
// Get a specific variable value
const appName = getSalesiveConfig("app-name");
// Or get all variables
const allVariables = getSalesiveConfig();What Gets Injected?
The plugin performs several injections into your HTML:
- Page Title: From
variables.app-name - Meta Description: From
variables.app-description - Favicon: From
variables.app-favicon - CSS Variables: All entries in
variablesare injected as CSS custom properties - Global JavaScript Object: The entire config is available at
window.SALESIVE_CONFIG
Integration with React Router
For projects using React Router, you can combine this plugin with a routing setup. This works well with the nested route structure pattern where components are organized into a layout hierarchy.
Example routing structure:
// router.jsx
import { createBrowserRouter } from "react-router-dom";
import App from "./App";
import HomePage from "./pages/Home";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{ index: true, element: <HomePage /> },
// ...other routes
],
},
]);
export default router;Options
The salesiveConfigPlugin accepts the following options:
salesiveConfigPlugin({
// Custom path to your config file (default: 'salesive.config.json' in project root)
configPath: "./path/to/config.json",
});Development Mode Only
The plugin only runs during development (vite dev) and is automatically disabled for production builds to keep your production code clean.
CLI Commands
Quick Start
# 1. Set up authentication
salesive auth set-token
# 2. Initialize a new template (or use existing)
salesive init
# 3. Validate your configuration
salesive validate
# 4. Deploy your template
salesive cookAuthentication
Manage your Salesive API credentials for deploying templates.
# Set your Salesive API token
salesive auth set-token
# Check authentication status
salesive auth status
# Verify API key with server
salesive auth verify
# Clear your token if needed
salesive auth clear-tokenCreating New Projects
Two starters, two commands: init scaffolds a storefront theme (React + Vite,
deployed with salesive cook); create-app clones the embedded app starter
(React + Express on one port — OAuth 2.1 + PKCE, signed session, socket.io, a notes
CRUD proxy).
Storefront theme (salesive init):
# Initialize a new Salesive theme with interactive prompts
# (Select Build Tool, Framework, and Package Manager)
salesive init
# Initialize in the current directory (scaffolding)
salesive init --current
# Create a project with specific options (skips prompts)
salesive init --name my-project --build-tool vite --framework react
# Force creation even if directory exists
salesive init --name existing-dir --forceEmbedded app (salesive create-app):
# Clone the app starter with interactive prompts (App name, Package Manager)
salesive create-app
# Skip prompts
salesive create-app --name my-app --package-manager npm
# Set up in the current directory (won't overwrite existing files)
salesive create-app --current
# Force creation even if the directory exists
salesive create-app --name existing-dir --forcecreate-app clones the starter, seeds .env from .env.example, and prints the
next steps. Fill in SALESIVE_CLIENT_ID / SALESIVE_CLIENT_SECRET /
SALESIVE_WEBHOOK_SECRET (Dashboard → Apps → Developer), then npm run dev.
Optionally set MONGODB_URI to persist installs.
Development
# Start development server with config file watching
salesive dev
# Specify a custom project path
salesive dev --path ./my-project
# Specify a custom config file
salesive dev --config ./custom-config.jsonThe development server provides:
- Hot Reloading: Automatically restarts when configuration changes
- Beautiful Logs: Clean, colored output
- Keyboard Shortcuts:
[r]: Restart the server[q]: Quit the CLICtrl+C: Exit gracefully
Template Deployment (Cook)
The cook command packages and deploys your Salesive template to the themes API. It automatically runs your project build script (e.g., npm run build) before packaging.
# Deploy template from current directory (builds first)
salesive cook
# Skip the automatic build step
salesive cook --no-build
# Deploy from a specific path
salesive cook --path ./my-template
# Use a custom config file
salesive cook --config ./custom-config.json
# Keep temporary files for debugging
salesive cook --keep-temp
# Show detailed error information
salesive cook --verboseWhat gets deployed:
- All template files (HTML, CSS, JS, images, etc.)
salesive.config.json(required)salesive.form.json(optional, but recommended)
Automatic exclusions:
node_modules/.git/.salesive-temp/*.log.DS_Store
Generate Form Configuration:
If you don't have a salesive.form.json, generate one at: https://template-form.salesive.com
Configuration Validation
Validate your salesive.config.json before deployment:
# Validate current project
salesive validate
# Validate specific project
salesive validate --path ./my-project
# Validate specific config file
salesive validate --config ./custom-config.json
# Show verbose output
salesive validate --verboseValidation checks:
- Required fields:
name,version,description - Name format: lowercase, no spaces (use hyphens)
- Version format: semantic versioning (x.x.x)
- Variables structure
Configuration File Format
Your salesive.config.json must follow this structure:
{
"name": "my-template",
"version": "1.0.0",
"description": "My awesome template",
"author": "Your Name",
"license": "MIT",
"website": "https://example.com",
"icon": "https://example.com/icon.png",
"email": "[email protected]",
"variables": {
"primary-color": "#0d65d9",
"secondary-color": "#e6f6ff",
"font-family": "Space Grotesk"
}
}Available Options
Cook Options
| Option | Description |
| ------------------------ | ------------------------------------------------------- |
| -p, --path <path> | Path to template directory (default: current directory) |
| -c, --config <path> | Path to salesive.config.json file |
| --no-build | Skip the automatic build step |
| -b, --build-dir <path> | Alias for --out-dir to specify build directory |
| -k, --keep-temp | Keep temporary files after cooking |
| -v, --verbose | Show verbose output and detailed error information |
Auth Options
| Option | Description |
| --------------- | ---------------------------------------------------- |
| -v, --verbose | Show detailed error information (for verify command) |
Init Options
| Option | Description |
| --------------------------------- | -------------------------------------- |
| -n, --name <name> | Project name |
| -c, --current | Initialize in current directory |
| -b, --build-tool <tool> | Build tool (vite) |
| --framework <framework> | Framework (react) |
| -p, --package-manager <manager> | Package manager (npm, yarn, bun, pnpm) |
| -f, --force | Overwrite existing directory |
| -v, --verbose | Show verbose output |
Validate Options
| Option | Description |
| --------------------- | --------------------------------- |
| -p, --path <path> | Path to project directory |
| -c, --config <path> | Path to salesive.config.json file |
| -v, --verbose | Show verbose output |
Automatic Updates
The CLI automatically checks for updates once every 24 hours and notifies you when a new version is available.
┌──────────────────────────────────────────────────────────┐
│ Update available! 0.0.1 → 1.0.0 │
│ Run npm install -g salesive-dev-tools to update │
└──────────────────────────────────────────────────────────┘Error Handling
The CLI provides detailed error messages with actionable suggestions:
✖ API key verification failed
✗ Error 401: Unauthorized
Invalid API key format
💡 Suggestion:
• Invalid or expired API key
• Run salesive auth set-token to update your API key
• Ensure the API key starts with sk-Use the --verbose flag with any command to see detailed error information.
Troubleshooting
Common Issues
Authentication Errors:
# Verify your API key is valid
salesive auth verify
# Set a new API key if needed
salesive auth set-tokenConfiguration Errors:
# Validate your config file
salesive validate
# Check for common issues:
# - Name must be lowercase with no spaces
# - Version must follow x.x.x format
# - All required fields must be presentApp Bridge — Permissions & Lifecycle
Embedded Salesive apps run inside an iframe. The App Bridge lets your app communicate with the merchant's dashboard to request runtime permissions and react to open/close events.
Installation
npm install salesive-dev-toolsImport
// Subpath import (recommended — tree-shakes everything else out):
import {
PERMISSIONS,
DURATIONS,
APP_EVENTS,
requestPermission,
getGrantedPermissions,
onLaunch,
onAppWake,
onAppOpened,
onAppMinimized,
onAppClosed,
onLogout,
onStoreChanged,
onEvent,
} from "salesive-dev-tools/permissions";
// Or from the main package:
import { requestPermission, getGrantedPermissions } from "salesive-dev-tools";TypeScript types are included — no separate @types/ package needed.
Available permissions
| Constant | String value | What it enables |
|---|---|---|
| PERMISSIONS.AUTO_LAUNCH | "AUTO_LAUNCH" | App can call onLaunch() to un-minimize itself |
| PERMISSIONS.WRITE_DOMAINS | "WRITE_DOMAINS" | App can add / remove custom domains |
| PERMISSIONS.CLIPBOARD_READ | "CLIPBOARD_READ" | App can read from the system clipboard |
| PERMISSIONS.WRITE_ORDERS_CANCEL | "WRITE_ORDERS_CANCEL" | App can cancel orders on the merchant's behalf |
requestPermission(permission, options?, timeoutMs?)
Shows a merchant-facing approval modal in the dashboard. The merchant picks a duration
and clicks Allow or Deny. Returns a promise that resolves to true (approved) or false
(denied or timed out).
function requestPermission(
permission : Permission,
options? : DurationOption[], // duration choices shown in the modal; omit = all five
timeoutMs? : number, // default 30 000 ms
): Promise<boolean>options — which duration choices to offer. The first element is selected by default.
| Constant | String value | Lasts |
|---|---|---|
| DURATIONS.ONCE | "once" | One API call, then consumed |
| DURATIONS.ONE_DAY | "1day" | 1 day |
| DURATIONS.SEVEN_DAYS | "7days" | 7 days |
| DURATIONS.FOURTEEN_DAYS | "14days" | 14 days |
| DURATIONS.THIRTY_ONE_DAYS | "31days" | 31 days |
// Request AUTO_LAUNCH — offer 7-day and 31-day choices:
const granted = await requestPermission(
PERMISSIONS.AUTO_LAUNCH,
[DURATIONS.SEVEN_DAYS, DURATIONS.THIRTY_ONE_DAYS]
);
if (granted) {
console.log("Permission granted!");
}The dashboard modal has a 10-second auto-deny countdown. Keep timeoutMs above 10 000 ms
(the default 30 000 ms is fine).
getGrantedPermissions(timeoutMs?)
Silently query which permissions are currently active — no modal is shown.
function getGrantedPermissions(timeoutMs?: number): Promise<PermissionGrantMap>Returns a PermissionGrantMap — a partial map of Permission → GrantRecord:
interface GrantRecord {
mode : DurationOption;
grantedAt : string; // ISO-8601
expiresAt : string | null; // null for "once" grants
usedAt : string | null; // set after a "once" grant is consumed
}// Check on mount before requesting:
const grants = await getGrantedPermissions();
if (PERMISSIONS.AUTO_LAUNCH in grants) {
console.log("AUTO_LAUNCH already granted until", grants.AUTO_LAUNCH.expiresAt);
} else {
const ok = await requestPermission(PERMISSIONS.AUTO_LAUNCH);
}onLaunch()
Ask the dashboard to expand (un-minimize) this app. The dashboard silently ignores the
call if AUTO_LAUNCH hasn't been granted.
onLaunch(); // fire-and-forgetonAppWake(callback)
Fires when the dashboard first loads your app in the background — before the merchant has ever opened it. The Salesive dashboard pre-loads every installed app at startup in a hidden, full-size iframe so apps warm up instantly. This is the earliest point your JavaScript executes.
function onAppWake(callback: () => void): () => void // returns unsubscribeimport { onAppWake, getGrantedPermissions, PERMISSIONS } from "salesive-dev-tools/permissions";
onAppWake(async () => {
// Called as soon as the dashboard loads; merchant hasn't opened the app yet
connectWebSocket();
prefetchCriticalData();
// Check grants silently on wake
const grants = await getGrantedPermissions();
if (PERMISSIONS.AUTO_LAUNCH in grants) scheduleAutoLaunchTimer();
});onAppOpened(callback) / onAppMinimized(callback) / onAppClosed(callback)
Subscribe to foreground lifecycle events. All three return an unsubscribe function.
function onAppOpened(callback: () => void): () => void
function onAppMinimized(callback: () => void): () => void
function onAppClosed(callback: () => void): () => void| Event | When it fires |
|---|---|
| onAppOpened | App expands to full view (first open or restored from minimized) |
| onAppMinimized | Merchant presses − (minimize); app is hidden but still running |
| onAppClosed | Merchant presses × (close/dismiss); app keeps running in the background |
// Refresh data when the merchant opens the app:
const unsubOpen = onAppOpened(() => refreshDashboard());
// Save unsaved state when dismissed:
const unsubClose = onAppClosed(() => saveDraft());
// Auto-reopen 30 seconds after minimize:
const unsubMin = onAppMinimized(() => {
const timer = setTimeout(onLaunch, 30_000);
const unsubNext = onAppOpened(() => { clearTimeout(timer); unsubNext(); });
});
// Clean up in React:
useEffect(() => {
const unOpen = onAppOpened(() => { /* ... */ });
const unMin = onAppMinimized(() => { /* ... */ });
const unCls = onAppClosed(() => { /* ... */ });
return () => { unOpen(); unMin(); unCls(); };
}, []);onLogout(callback) / onStoreChanged(callback)
Session-boundary events: the merchant is no longer the same person, or is no longer looking at the same store. Both are broadcast to every installed app, background or foreground — a backgrounded app holds the departing merchant's data just as wrongly as the visible one. Both return an unsubscribe function.
function onLogout(callback: () => void): () => void
function onStoreChanged(callback: (store: {
shopId : string; // now-active store; matches the new `shop` param
shopName? : string; // display name, when the dashboard knows it
previousShopId : string | null; // the store just left, or null on a cold switch
}) => void): () => void| Event | When it fires |
|---|---|
| onLogout | Merchant logs out of Salesive; your iframe is destroyed moments later |
| onStoreChanged | Merchant switches active store; your iframe reloads with the new shop param |
// The dashboard clears only ITS origin on logout — your app is on another one,
// so its token survives unless you drop it here.
onLogout(() => {
localStorage.removeItem("my-app-token");
sessionStorage.clear();
});
// The reload (with the new ?shop=) is what boots you under the new store, so
// only clear what would otherwise SURVIVE it.
onStoreChanged(({ shopId, previousShopId }) => {
if (previousShopId) caches.delete(`orders-${previousShopId}`);
console.log("now operating on store", shopId);
});⚠️ Keep both handlers synchronous. Each announces that your iframe is about to be torn down, and the dashboard pauses only briefly (~150 ms on logout) before doing it. An
awaiton a network round trip will not finish — usenavigator.sendBeacon()if your server must be told too.
Full React example
import { useEffect, useState } from "react";
import {
PERMISSIONS, DURATIONS,
requestPermission, getGrantedPermissions,
onLaunch, onAppWake, onAppOpened, onAppMinimized, onAppClosed,
} from "salesive-dev-tools/permissions";
export default function App() {
const [autoLaunchGranted, setAutoLaunchGranted] = useState(false);
// Background init — fires before the merchant opens the app
useEffect(() => {
return onAppWake(async () => {
connectWebSocket();
const grants = await getGrantedPermissions();
setAutoLaunchGranted(PERMISSIONS.AUTO_LAUNCH in grants);
});
}, []);
// Foreground lifecycle
useEffect(() => {
const unOpen = onAppOpened(() => refreshDashboard());
const unMin = onAppMinimized(() => pauseHeavyWork());
const unCls = onAppClosed(() => saveDraft());
return () => { unOpen(); unMin(); unCls(); };
}, []);
// Auto-reopen 30 s after minimize (only when granted)
useEffect(() => {
if (!autoLaunchGranted) return;
return onAppMinimized(() => {
const timer = setTimeout(onLaunch, 30_000);
const unsub = onAppOpened(() => { clearTimeout(timer); unsub(); });
});
}, [autoLaunchGranted]);
async function handleRequestAutoLaunch() {
const ok = await requestPermission(
PERMISSIONS.AUTO_LAUNCH,
[DURATIONS.SEVEN_DAYS, DURATIONS.THIRTY_ONE_DAYS]
);
setAutoLaunchGranted(ok);
}
return (
<div>
{autoLaunchGranted
? <p>✓ Auto-launch active — app will reopen when minimized.</p>
: <button onClick={handleRequestAutoLaunch}>Enable auto-reopen</button>
}
</div>
);
}Environment notes
- Browser only — the bridge is a no-op in Node.js / SSR environments (all functions
return
false/{}/ unsubscribe no-ops). Safe to import in Next.js or Remix. - No dependencies — the permissions module has no runtime dependencies.
- Single listener — one
window.addEventListener("message", …)is registered per browser context when the module is first imported. Multiple calls torequestPermissionfor different permissions run concurrently without conflict.
WebMCP — AI tools for Ola
Register tools from your app's page that Ola (the merchant's AI assistant in the Salesive dashboard) can discover and call — each call gated by a merchant approval card in the chat showing your app's logo, with an Always allow option per tool.
The API follows the WebMCP proposal
(navigator.modelContext). No native browser support is required — this
module is a complete implementation on its own: in browsers without WebMCP it
does everything itself and also installs the standard navigator.modelContext
/ document.modelContext aliases; in browsers with native WebMCP it mirrors
your registrations into the native registry too. You never feature-detect.
Import
// Subpath import (recommended):
import { modelContext, installWebMCP } from "salesive-dev-tools/webmcp";
// Or from the main package:
import { modelContext } from "salesive-dev-tools";modelContext.registerTool(tool, options?)
modelContext.registerTool({
name: "track_delivery", // [a-zA-Z0-9_], ≤ 30 chars
title: "Track delivery", // shown on the approval card
description: "Track a delivery by order id and return its live status.",
inputSchema: { // JSON Schema, type "object"
type: "object",
properties: { orderId: { type: "string" } },
required: ["orderId"],
},
annotations: { readOnlyHint: true },
async execute({ orderId }) { // runs IN YOUR PAGE after approval
const status = await fetchStatus(orderId);
return { content: [{ type: "text", text: `Delivery: ${status}` }] };
},
});execute may return MCP-style {content:[{type:"text",text}]}, a plain
string, or any JSON value. Throw to report an error. Unregister with
modelContext.unregisterTool(name) or spec-style via an AbortSignal in
options.signal. Registry changes fire a toolchange event
(modelContext.ontoolchange / addEventListener).
| Fact | Value | |---|---| | Tools per app | 32 | | Approval | Per call, or "Always allow" per tool per installation | | Call timeout | ~2 min including merchant approval | | Execution | Client-side in your iframe — no scopes, no tokens | | Result cap | 16 KB |
Register at startup (e.g. in
onAppWake): your app is pre-loaded in the background, so tools are available to Ola before the merchant opens the app. Full guide: the "AI tools (WebMCP)" page in the Salesive docs.
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
License
MIT
