apprun-skills

End-to-end guidance for AppRun apps in TypeScript using MVU: component patterns, event handling, state management (including async generators), routing/navigation with params and guards, and testing with vitest. Use when designing or reviewing AppRun components, wiring routes, managing state flows, or writing AppRun tests.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "apprun-skills" with this command: npx skills add yysun/apprun-skills/yysun-apprun-skills-apprun-skills

AppRun Skills

Overview

  • Build AppRun apps with MVU (Model-View-Update) in TypeScript.
  • Prefer pure update functions for testability.
  • Use mounted() for components embedded in JSX.
  • Use state = async only for top-level routed pages that must load async data.

Project Setup

Recommended Project Structure

web/                        # Frontend application root
├── index.html              # Entry HTML file
├── package.json            # Dependencies and scripts
├── vite.config.js          # Vite configuration
├── src/
│   ├── main.tsx            # Application entry point (routes registration)
│   ├── api.ts              # REST API client (optional)
│   ├── styles.css          # Application styles
│   ├── tsconfig.json       # TypeScript configuration
│   ├── components/         # Reusable UI components
│   │   ├── Layout.tsx      # Root layout container
│   │   └── ...             # Other reusable components
│   ├── domain/             # Business logic modules (optional)
│   │   └── ...             # Pure functions and business logic
│   ├── pages/              # Top-level page components
│   │   ├── Home.tsx        # Example: Home page
│   │   └── ...             # Other route pages
│   ├── types/              # TypeScript type definitions
│   │   ├── index.ts        # Shared types
│   │   └── jsx.d.ts        # JSX type declarations
│   └── utils/              # Utility functions
└── public/                 # Static assets (optional)

Vite Configuration

import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    outDir: 'dist',
    emptyOutDir: true,
  },
  server: {
    port: 8080,
    open: true,
    historyApiFallback: true,  // SPA mode
    proxy: {
      // Proxy API requests to backend
      '/api': {
        target: 'http://127.0.0.1:3000',
        changeOrigin: true,
        secure: false
      }
    }
  }
})

Package.json

{
  "name": "my-apprun-app",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "check": "tsc --noEmit"
  },
  "devDependencies": {
    "apprun": "^3.38.0",
    "typescript": "^5.0.0",
    "vite": "^5.0.0"
  }
}

TypeScript Configuration

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "jsx": "react",
    "jsxFactory": "app.createElement",
    "jsxFragmentFactory": "app.Fragment",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Critical Settings for AppRun:

  • jsx: "react" - Enables JSX syntax
  • jsxFactory: "app.createElement" - Uses AppRun's JSX factory
  • jsxFragmentFactory: "app.Fragment" - Uses AppRun's Fragment support
  • moduleResolution: "bundler" - Optimized for Vite

Entry Points

HTML Entry (index.html):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My AppRun App</title>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="src/main.tsx"></script>
</body>
</html>

Application Entry (src/main.tsx):

import app from 'apprun';
import Layout from './components/Layout';
import Home from './pages/Home';
import About from './pages/About';
import './styles.css';

app.render('#root', <Layout />);

app.addComponents('#pages', {
  '/': Home,
  '/about': About,
});

Layout Component (src/components/Layout.tsx):

import app from 'apprun';

export default () => (
  <div id="app">
    <div id="pages"></div>
  </div>
);

Styling Options

Option 1: Vanilla CSS

/* src/styles.css */
:root {
  --color-primary: #007bff;
  --color-text: #333;
  --spacing-unit: 8px;
}

body {
  font-family: system-ui, -apple-system, sans-serif;
  color: var(--color-text);
  margin: 0;
  padding: 0;
}

Option 2: Tailwind CSS v4

Install Tailwind v4:

npm install -D tailwindcss@next @tailwindcss/vite@next

Update vite.config.js:

import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [tailwindcss()],
  // ... other config
})

Import in src/styles.css:

@import "tailwindcss";

Use in components:

<div className="flex items-center gap-4 p-4 bg-white rounded-lg shadow">
  <h1 className="text-2xl font-bold">Hello World</h1>
</div>

Option 3: CSS Modules

import styles from './MyComponent.module.css';

export default () => (
  <div className={styles.container}>
    <h1 className={styles.title}>Hello</h1>
  </div>
);

API Client Pattern

// src/api.ts
const API_BASE_URL = '/api';

interface RequestOptions extends RequestInit {
  params?: Record<string, string>;
}

async function request<T>(
  endpoint: string,
  options: RequestOptions = {}
): Promise<T> {
  const { params, ...fetchOptions } = options;
  
  let url = `${API_BASE_URL}${endpoint}`;
  if (params) {
    const query = new URLSearchParams(params).toString();
    url += `?${query}`;
  }

  const response = await fetch(url, {
    headers: {
      'Content-Type': 'application/json',
      ...fetchOptions.headers,
    },
    ...fetchOptions,
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(error.message || `HTTP ${response.status}`);
  }

  return response.json();
}

export const api = {
  get: <T>(endpoint: string, params?: Record<string, string>) =>
    request<T>(endpoint, { method: 'GET', params }),
    
  post: <T>(endpoint: string, data?: unknown) =>
    request<T>(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    }),
    
  put: <T>(endpoint: string, data?: unknown) =>
    request<T>(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    }),
    
  delete: <T>(endpoint: string) =>
    request<T>(endpoint, { method: 'DELETE' }),
};

export default api;

Quick Start

# 1. Create project
npm create vite@latest my-apprun-app -- --template vanilla-ts
cd my-apprun-app

# 2. Install AppRun
npm install
npm install -D apprun

# 3. Configure TypeScript (update tsconfig.json with settings above)

# 4. Rename entry file
mv src/main.ts src/main.tsx

# 5. Create basic app structure
# (Add Layout, pages, components as shown above)

# 6. Run development server
npm run dev

# 7. Build for production
npm run build
npm run preview

Why Vite + AppRun?

Why Vite:

  • Fast development with instant HMR
  • Optimized builds with Rollup
  • First-class TypeScript support
  • Minimal configuration

Why AppRun:

  • Lightweight (~7KB gzipped)
  • Simple MVU pattern
  • Direct DOM updates (no virtual DOM)
  • Full TypeScript support
  • Built-in routing

Component Patterns - Decision Tree

  1. Manages state + user interactions? → Stateful Class Component
  2. Popup/modal/overlay? → Modal Component (use mounted())
  3. Display-only from props? → Functional Component
  4. 10+ events needing type safety? → Typed Events Pattern

Stateful Class Component

Structure Order: Imports → Interfaces → Helpers → Actions → Component

import { app, Component } from 'apprun';

interface Props { data?: any; }
export interface State { 
  loading: boolean; 
  error: string | null; 
  successMessage?: string;
  // ... specific fields
}

const getStateFromProps = (props: Props): State => ({ /* ... */ });

export const saveData = async function* (state: State): AsyncGenerator<State> {
  // Validation
  if (!state.data.name.trim()) {
    yield { ...state, error: 'Name required' };
    return;
  }
  
  // Loading
  yield { ...state, loading: true, error: null };
  
  // API call
  try {
    await api.save(state.data);
    yield { ...state, loading: false, successMessage: 'Saved!' };
    app.run('data-saved');
  } catch (error: any) {
    yield { ...state, loading: false, error: error.message };
  }
};

export default class MyComponent extends Component<State> {
  declare props: Readonly<Props>;
  
  mounted = (props: Props): State => getStateFromProps(props);
  
  view = (state: State) => {
    if (state.loading) return <div>Loading...</div>;
    if (state.error) return <div className="error">{state.error}</div>;
    
    return (
      <form>
        <input $bind="data.name" />
        <button $onclick={[saveData]} disabled={state.loading}>Save</button>
      </form>
    );
  };
}

View Pattern: Guard clauses → Early returns → Main content

Modal Component

CRITICAL: Must use mounted() (embedded in JSX), not state = async

export default class Modal extends Component<State> {
  declare props: Readonly<Props>;
  
  mounted = (props: Props): State => getStateFromProps(props);
  
  view = (state: State) => (
    <div className="modal-backdrop" onclick={closeModal}>
      <div className="modal-content" onclick={(e) => e.stopPropagation()}>
        <button onclick={closeModal}>×</button>
        {/* content */}
      </div>
    </div>
  );
}

Requirements: Close button + backdrop click + stopPropagation

Functional Component

export interface Props {
  data: DataType[];
  onItemClick?: (item: DataType) => void;
}

export default function DisplayComponent({ data, onItemClick }: Props) {
  if (!data?.length) return <div>No items</div>;
  return (
    <ul>
      {data.map(item => (
        <li onclick={() => onItemClick?.(item)}>{item.name}</li>
      ))}
    </ul>
  );
}

Pattern: Destructure → Guard clauses → Main render

Typed Events Pattern

Payload Rules:

  1. Single value → payload: string | Call: $onclick={['delete', id]}
  2. Multiple values → payload: { id: string; name: string } | Call: $onclick={['edit', { id, name }]}
  3. No payload → payload: void | Call: $onclick="save"
  4. Input events → payload: { target: { value: string } }
// types/events.ts
export type MyEvents =
  | { name: 'save'; payload: void }
  | { name: 'delete'; payload: string }
  | { name: 'edit'; payload: { id: string; name: string } };

export type MyEventName = MyEvents['name'];

// Component
class MyComponent extends Component<State, MyEventName> {
  override update = myHandlers;
}

// Handlers (OBJECT format, not array)
export const myHandlers: Update<State, MyEventName> = {
  save: (state): State => ({ ...state, saved: true }),
  delete: (state, id: string): State => ({ 
    ...state, 
    items: state.items.filter(i => i.id !== id) 
  }),
  edit: (state, { id, name }: { id: string; name: string }): State => ({ 
    ...state, 
    editing: { id, name } 
  })
};

stopPropagation: Add event as last parameter

'click-item': (state, id: string, e?: Event): State => {
  e?.stopPropagation();
  return { ...state, selected: id };
}

Event Directives

AppRun Directives (Trigger Update Handlers)

DirectiveUse CaseExample
$bind="field"Two-way binding (PREFERRED for forms)<input $bind="name" />
$bind="nested.field"Nested property<input $bind="user.profile.name" />
$onclick="action"String action<button $onclick="save" />
$onclick={['action', data]}Action with params<button $onclick={['delete', id]} />
$onclick={[func]}Direct function<button $onclick={[saveData]} />
$oninput="handler"Custom input handling<input $oninput="validate" />

Other directives: $onchange, $onsubmit, $onfocus, $onblur, $onkeydown

Standard HTML Events (DOM Manipulation)

Use onclick, oninput, etc. for direct DOM manipulation only:

<div onclick={(e) => e.stopPropagation()}>Content</div>

When to Use What

  • $bind - Simple form fields (no handler needed)
  • $oninput - Validation, transformation, debouncing
  • $onclick - Trigger update handlers
  • Never - $onclick={() => app.run('action')}

Validation Example:

$oninput="validate-email"

'validate-email': (state, e: Event) => {
  const email = (e.target as HTMLInputElement).value;
  const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  return { ...state, email, emailError: valid ? null : 'Invalid' };
}

Update Handlers

Sync: Return new state

'increment': (state) => ({ ...state, count: state.count + 1 })

Async: Use async

'load': async (state) => {
  this.setState({ ...state, loading: true });
  const data = await api.fetch();
  return { ...state, data, loading: false };
}

Generator: Multi-step with intermediate renders (PREFERRED for complex flows)

'save': async function* (state) {
  yield { ...state, loading: true };
  await api.save(state.data);
  yield { ...state, loading: false, success: true };
}

Side Effects: No return = no re-render

'navigate': (state) => {
  window.location.href = '/path';
  // No return - no re-render
}

Component Communication

PatternUse CaseImplementation
PropsParent → ChildPass data via props
CallbacksChild → ParentPass function via props
Global EventsAny → Anyis_global_event = () => true

Global Events:

// Modal component
class Modal extends Component {
  is_global_event = () => true;
  
  update = {
    'open-modal': (state, data) => ({ ...state, visible: true, data }),
    'close-modal': (state) => ({ ...state, visible: false })
  };
}

// Any component can trigger
<button onclick={() => app.run('open-modal', data)}>Open</button>

Critical Rules

State Initialization

Component TypeUseExample
JSX Embeddedmounted()mounted = (props) => getStateFromProps(props)
Top-Level Routedstate = asyncstate = async () => { const data = await api.fetch(); return { data }; }

❌ NEVER mix both mounted() and state = async

State Updates

Returning state triggers re-render:

  • Immutable (recommended): return { ...state, field: value }
  • Mutable (allowed): state.field = value; return state
  • Side effects only: Don't return (no re-render)

Required State Properties

interface State {
  loading: boolean;        // For async operations
  error: string | null;    // For error messages
  successMessage?: string; // For success feedback
  // ... specific fields
}

Deep Cloning

// Nested object update
return { 
  ...state, 
  user: { 
    ...state.user, 
    profile: { 
      ...state.user.profile, 
      name 
    } 
  } 
};

Anti-Patterns

❌ DON'T:

// Don't use $onclick with arrow functions calling app.run
$onclick={() => app.run('action')}

// Don't forget error handling in async
async function save() { await api.save(); }  // No try-catch!

// Don't use manual input when $bind available
$oninput={(e) => setState({ ...state, field: e.target.value })}

// Don't use state = async for JSX embedded components
class Modal extends Component {
  state = async () => { /* WRONG */ };
}

// Don't forget defensive programming
messages.map()  // messages might be undefined - use messages?.map()

// Don't use array format for update handlers
update = [['event', handler]]  // WRONG - use object format

// Don't mutate state directly
state.count++;  // WRONG

Routing, Linking, and Component Registration

This section explains how AppRun applications handle routing, page navigation, and component registration.

Overview

The app uses AppRun's built-in routing system without any external router libraries. Routes are defined declaratively, and navigation uses standard HTML anchor tags or programmatic methods.

1. Component Registration

Routes are registered centrally in main.tsx:

import app from 'apprun';
import Layout from './components/Layout';
import Home from './pages/Home';
import World from './pages/World';

app.render('#root', <Layout />);
app.addComponents('#pages', {
  '/': Home,
  '/World': World,
  // '/Agent': Agent,      // commented out
  // '/Settings': Settings, // commented out
});

How It Works:

  • app.render('#root', <Layout />): Renders the top-level Layout component into the #root DOM element
  • app.addComponents('#pages', {...}): Registers route-to-component mappings
    • Key: Route path (e.g., '/', '/World')
    • Value: Component class (e.g., Home, World)
    • Components are rendered into the #pages container defined in Layout

2. Layout Container

The Layout component provides the rendering container for routed pages:

// web/src/components/Layout.tsx
export default () => <div id="main" className="w-full min-h-screen">
  <div id="pages"></div>
</div>
  • Minimal wrapper with full-width, full-height container
  • The #pages div is where route components are dynamically rendered
  • AppRun automatically swaps components based on the current route

3. Page Linking (Declarative Navigation)

The app uses standard HTML anchor tags for navigation:

Example from Home Component:

// Navigate to a specific world
<a href={'/World/' + worldName}>
  <button className="btn btn-primary">
    Enter {worldName}
  </button>
</a>

Example from World Component:

// Navigate back to home
<a href="/">
  <button className="back-button" title="Back to Worlds">
    <span className="world-back-icon">←</span>
  </button>
</a>

How It Works:

  • Standard <a href=""> links trigger AppRun's routing
  • AppRun intercepts link clicks and updates the route without full page reload
  • Route parameters (like world name) are included in the URL path
  • No special Link component required—just plain HTML

4. Programmatic Navigation

Components can navigate programmatically using window.location.href:

Example from Home Component Update Handler:

update = {
  'enter-world': (state: HomeState, world: World): void => {
    // Navigate to the world page
    window.location.href = '/World/' + world.name;
  }
}

When to Use:

  • Inside event handlers that need to navigate after logic
  • When navigation is a side effect (return void instead of new state)
  • For conditional navigation based on user actions

5. Route Parameters

Routes can include dynamic parameters in the path:

URL Pattern:

/World/:worldName

Parsing Parameters:

Components can access route parameters from the URL:

// Example: /World/MyWorld
const worldName = window.location.pathname.split('/')[2];  // "MyWorld"

Route Handler Pattern:

update = {
  '/World': async (state, worldName: string) => {
    // worldName is parsed from the URL
    return {
      ...state,
      worldName,
      // ... load world data
    };
  }
}

6. Component Architecture (MVU Pattern)

Page components follow AppRun's Model-View-Update pattern:

export default class PageComponent extends Component<StateType> {
  
  // 1. STATE: Initial data and loading states
  state = {
    loading: true,
    data: null,
    // ...
  };

  // 2. VIEW: Render function that returns JSX
  view = (state: StateType) => {
    return <div>
      {/* JSX markup */}
    </div>;
  };

  // 3. UPDATE: Event handlers
  update = {
    'event-name': (state, payload) => {
      // Return new state to trigger re-render
      return { ...state, newData: payload };
    },
    
    'navigation-event': (state) => {
      // Return void for side effects (no re-render)
      window.location.href = '/path';
    }
  };
}

Key Principles:

  • State: Plain object with component data
  • View: Pure function that converts state to JSX
  • Update: Event handlers that return new state or void
  • Immutability: Always return new state objects, never mutate

7. Event System

Local vs Global Events:

Components can be configured to listen to global events:

export default class WorldComponent extends Component {
  // Make all events global (visible across components)
  override is_global_event = () => true;
}

Event Propagation:

  • Local events: Only visible within the component
  • Global events: Can be triggered from child components or other parts of the app
  • Use app.run('event-name', payload) to trigger events programmatically

Event Handler Types:

update = {
  // Returns new state → triggers re-render
  'update-data': (state, newData) => ({
    ...state,
    data: newData
  }),
  
  // Returns void → no re-render (side effect only)
  'navigate': (state) => {
    window.location.href = '/path';
  }
}

8. Best Practices

Navigation:

  • ✅ Use <a href=""> for simple links
  • ✅ Use window.location.href for programmatic navigation
  • ✅ Include route parameters in the path: /World/${name}
  • ❌ Don't use client-side routing for external URLs

Component Registration:

  • ✅ Register all routes in a single place (main.tsx)
  • ✅ Use clear, semantic route paths
  • ✅ Keep the route structure flat and simple
  • ❌ Don't nest routes deeply

Event Handling:

  • ✅ Return new state to trigger re-render
  • ✅ Return void for navigation or side effects
  • ✅ Use descriptive event names: 'load-world', 'delete-chat'
  • ❌ Don't mutate state directly

URL Structure:

/                      → Home page (world selection)
/World/:name          → World page (chat interface)
/Agent/:id            → Agent page (currently disabled)
/Settings             → Settings page (currently disabled)

9. Example Flow: Entering a World

Step 1: User clicks "Enter World" button on Home page

// Home.tsx
<a href={'/World/' + world.name}>
  <button className="btn btn-primary">
    Enter {world.name}
  </button>
</a>

Step 2: AppRun intercepts the link and updates route

  • URL changes to /World/MyWorld
  • AppRun's router detects the route change
  • Router looks up the registered component for /World

Step 3: World component is mounted and initialized

// World.tsx
update = {
  '/World': async (state, worldName: string) => {
    // Load world data from API
    const world = await api.getWorld(worldName);
    const messages = await api.getMessages(worldName);
    
    return {
      ...state,
      worldName,
      world,
      messages,
      loading: false
    };
  }
}

Step 4: World component renders with loaded data

  • View function receives the updated state
  • Chat interface displays with agents and messages
  • Component is now interactive and listening for events

10. Debugging Tips

Check Current Route:

console.log(window.location.pathname);  // "/World/MyWorld"

Monitor Route Changes:

app.on('//', (route) => {
  console.log('Route changed to:', route);
});

Verify Component Registration:

// Check if component is registered for a route
// Look for the component rendering in #pages container
console.log(document.querySelector('#pages').innerHTML);

Summary

  • Registration: app.addComponents('#pages', { path: Component })
  • Navigation: Use <a href=""> or window.location.href
  • Route Params: Parsed from URL path in route handlers
  • Component Pattern: MVU (Model-View-Update)
  • Events: Local by default, can be made global with is_global_event()
  • No Router Library: AppRun's built-in routing handles everything

Testing (Vitest)

  • Unit test pure update functions.
  • Iterate async generators to capture each yield.
  • Mock APIs with vi.mock.
import { describe, it, expect, vi } from 'vitest';
import { save } from './Form';
import api from '../api';

vi.mock('../api');

describe('save', () => {
  it('yields validation then stops', async () => {
    const state = { loading: false, error: null, form: { name: '' } } as State;
    const gen = save(state);
    const first = await gen.next();
    expect(first.value?.error).toBe('Name is required');
  });
});

Development Checklist

Component Structure

  • Imports at top
  • Props interface with ? for optional
  • State interface (exported)
  • Helper functions
  • Action functions (exported for $onclick and testing)
  • Component class with mounted or state = async

TypeScript Types

  • Props interface
  • State interface exported
  • Event types for 10+ events (discriminated union)
  • Generic types: Component<State, EventName>
  • Async generators: AsyncGenerator<State>

View Method

  • Guard clauses first (loading, error, success)
  • Early returns for special states
  • Main content last
  • Defensive programming (data?.map(), defaults)

State Management

  • Include loading, error, successMessage?
  • Return new state to re-render
  • Use mounted() for JSX embedded
  • Use state = async only for routed pages
  • Never mix both

Event Handling

  • Use $bind for simple forms
  • Use $onclick (not onclick={() => app.run()})
  • Export action functions for reusability
  • Use async generators for multi-step
  • Add try-catch in async functions

Error Handling

  • Try-catch in async operations
  • Error state in interface
  • Error display in view
  • Loading states during async
  • Success messages

Best Practices

  • Keep update logic pure when possible
  • Use global events for cross-component
  • Add catch-all route for 404
  • Test update logic and error paths
  • Use descriptive event names

Quick Reference

Component Selection:

  • State + interactions → Stateful Class
  • Modal/popup → Modal Component (mounted())
  • Display only → Functional
  • 10+ events → Typed Events

State Init:

  • JSX embedded → mounted()
  • Routed page → state = async

Events:

  • $bind for forms (preferred)
  • $onclick for actions
  • Typed for large components

Updates:

  • Return state → re-render
  • No return → side effect
  • Generators → multi-step

Communication:

  • Props: parent → child
  • Callbacks: child → parent
  • Global: any → any

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

rpd

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

openclaw-version-monitor

监控 OpenClaw GitHub 版本更新,获取最新版本发布说明,翻译成中文, 并推送到 Telegram 和 Feishu。用于:(1) 定时检查版本更新 (2) 推送版本更新通知 (3) 生成中文版发布说明

Archived SourceRecently Updated
Coding

ask-claude

Delegate a task to Claude Code CLI and immediately report the result back in chat. Supports persistent sessions with full context memory. Safe execution: no data exfiltration, no external calls, file operations confined to workspace. Use when the user asks to run Claude, delegate a coding task, continue a previous Claude session, or any task benefiting from Claude Code's tools (file editing, code analysis, bash, etc.).

Archived SourceRecently Updated
Coding

ai-dating

This skill enables dating and matchmaking workflows. Use it when a user asks to make friends, find a partner, run matchmaking, or provide dating preferences/profile updates. The skill should execute `dating-cli` commands to complete profile setup, task creation/update, match checking, contact reveal, and review.

Archived SourceRecently Updated
apprun-skills | V50.AI