raycast-extension

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Raycast Extension Development

Raycast 扩展开发

Quick Start

快速开始

  1. Create project structure
  2. Write package.json with extension config
  3. Implement command in src/
  4. Run
    npm install && npm run dev
  1. 创建项目结构
  2. 编写包含扩展配置的package.json
  3. 在src/中实现命令
  4. 运行
    npm install && npm run dev

Project Structure

项目结构

my-extension/
├── package.json          # Extension manifest + dependencies
├── tsconfig.json         # TypeScript config
├── .eslintrc.json        # ESLint config
├── raycast-env.d.ts      # Type definitions (auto-generated)
├── assets/
│   └── extension-icon.png  # 512x512 PNG icon
└── src/
    └── command-name.tsx    # Command implementation
my-extension/
├── package.json          # Extension manifest + dependencies
├── tsconfig.json         # TypeScript config
├── .eslintrc.json        # ESLint config
├── raycast-env.d.ts      # Type definitions (auto-generated)
├── assets/
│   └── extension-icon.png  # 512x512 PNG icon
└── src/
    └── command-name.tsx    # Command implementation

package.json Template

package.json 模板

json
{
  "name": "extension-name",
  "title": "Extension Title",
  "description": "What this extension does",
  "icon": "extension-icon.png",
  "author": "author-name",
  "categories": ["Productivity", "Developer Tools"],
  "license": "MIT",
  "commands": [
    {
      "name": "command-name",
      "title": "Command Title",
      "description": "What this command does",
      "mode": "view",
      "keywords": ["keyword1", "keyword2"]
    }
  ],
  "dependencies": {
    "@raycast/api": "^1.83.1",
    "@raycast/utils": "^1.17.0"
  },
  "devDependencies": {
    "@raycast/eslint-config": "^1.0.11",
    "@types/node": "22.5.4",
    "@types/react": "18.3.3",
    "eslint": "^8.57.0",
    "prettier": "^3.3.3",
    "typescript": "^5.5.4"
  },
  "scripts": {
    "build": "ray build --skip-types -e dist -o dist",
    "dev": "ray develop",
    "fix-lint": "ray lint --fix",
    "lint": "ray lint"
  }
}
json
{
  "name": "extension-name",
  "title": "Extension Title",
  "description": "What this extension does",
  "icon": "extension-icon.png",
  "author": "author-name",
  "categories": ["Productivity", "Developer Tools"],
  "license": "MIT",
  "commands": [
    {
      "name": "command-name",
      "title": "Command Title",
      "description": "What this command does",
      "mode": "view",
      "keywords": ["keyword1", "keyword2"]
    }
  ],
  "dependencies": {
    "@raycast/api": "^1.83.1",
    "@raycast/utils": "^1.17.0"
  },
  "devDependencies": {
    "@raycast/eslint-config": "^1.0.11",
    "@types/node": "22.5.4",
    "@types/react": "18.3.3",
    "eslint": "^8.57.0",
    "prettier": "^3.3.3",
    "typescript": "^5.5.4"
  },
  "scripts": {
    "build": "ray build --skip-types -e dist -o dist",
    "dev": "ray develop",
    "fix-lint": "ray lint --fix",
    "lint": "ray lint"
  }
}

Command Modes

命令模式

ModeUse Case
view
Show UI with Detail, List, Form, Grid
no-view
Background task, clipboard, notifications only
menu-bar
Menu bar icon with dropdown
模式使用场景
view
显示带有详情、列表、表单、网格的UI
no-view
仅执行后台任务、剪贴板操作、通知
menu-bar
带下拉菜单的菜单栏图标

Hotkey Configuration

快捷键配置

Add to command in package.json:
json
"hotkey": {
  "modifiers": ["opt"],
  "key": "m"
}
Modifiers:
cmd
,
opt
,
ctrl
,
shift
Note: Hotkeys in package.json are suggestions. Users set them in Raycast Preferences → Extensions.
在package.json的命令中添加:
json
"hotkey": {
  "modifiers": ["opt"],
  "key": "m"
}
修饰符:
cmd
,
opt
,
ctrl
,
shift
注意:package.json中的快捷键为建议设置。用户可在Raycast偏好设置→扩展中自行设置。

tsconfig.json

tsconfig.json

json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "lib": ["ES2022"],
    "module": "ES2022",
    "moduleResolution": "bundler",
    "noEmit": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "ES2022"
  },
  "include": ["src/**/*", "raycast-env.d.ts"]
}
json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "lib": ["ES2022"],
    "module": "ES2022",
    "moduleResolution": "bundler",
    "noEmit": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "ES2022"
  },
  "include": ["src/**/*", "raycast-env.d.ts"]
}

.eslintrc.json

.eslintrc.json

json
{
  "root": true,
  "extends": ["@raycast"]
}
json
{
  "root": true,
  "extends": ["@raycast"]
}

Command Patterns

命令模式示例

No-View Command (Background Task)

无界面命令(后台任务)

tsx
import { showHUD, Clipboard, showToast, Toast } from "@raycast/api";

export default async function Command() {
  const toast = await showToast({
    style: Toast.Style.Animated,
    title: "Working...",
  });

  try {
    // Do work
    const result = await doSomething();

    await Clipboard.copy(result);
    await showHUD("✅ Done!");
  } catch (error) {
    toast.style = Toast.Style.Failure;
    toast.title = "Failed";
    toast.message = error instanceof Error ? error.message : "Unknown error";
  }
}
tsx
import { showHUD, Clipboard, showToast, Toast } from "@raycast/api";

export default async function Command() {
  const toast = await showToast({
    style: Toast.Style.Animated,
    title: "Working...",
  });

  try {
    // Do work
    const result = await doSomething();

    await Clipboard.copy(result);
    await showHUD("✅ Done!");
  } catch (error) {
    toast.style = Toast.Style.Failure;
    toast.title = "Failed";
    toast.message = error instanceof Error ? error.message : "Unknown error";
  }
}

View Command (List)

界面命令(列表)

tsx
import { List, ActionPanel, Action } from "@raycast/api";

export default function Command() {
  return (
    <List>
      <List.Item
        title="Item"
        actions={
          <ActionPanel>
            <Action.CopyToClipboard content="text" />
          </ActionPanel>
        }
      />
    </List>
  );
}
tsx
import { List, ActionPanel, Action } from "@raycast/api";

export default function Command() {
  return (
    <List>
      <List.Item
        title="Item"
        actions={
          <ActionPanel>
            <Action.CopyToClipboard content="text" />
          </ActionPanel>
        }
      />
    </List>
  );
}

View Command (Detail)

界面命令(详情)

tsx
import { Detail } from "@raycast/api";

export default function Command() {
  const markdown = `# Hello World`;
  return <Detail markdown={markdown} />;
}
tsx
import { Detail } from "@raycast/api";

export default function Command() {
  const markdown = `# Hello World`;
  return <Detail markdown={markdown} />;
}

Performance & Caching

性能与缓存

Instant Load Pattern (No Empty Flash)

即时加载模式(避免空白闪烁)

Use synchronous cache read + async refresh for instant perceived load:
tsx
import { List, Cache } from "@raycast/api";
import { useCachedPromise, withCache } from "@raycast/utils";

const cache = new Cache();
const CACHE_KEY = "myData";

// Read cache synchronously at module load (before React renders)
function getInitialData(): MyData[] {
  const cached = cache.get(CACHE_KEY);
  if (cached) {
    try {
      return JSON.parse(cached);
    } catch {
      return [];
    }
  }
  return [];
}

// Expensive async operation wrapped with withCache (5 min TTL)
const fetchExpensiveData = withCache(
  async () => {
    // Your expensive operation here
    return await someSlowOperation();
  },
  { maxAge: 5 * 60 * 1000 }
);

async function fetchAllData(): Promise<MyData[]> {
  const data = await fetchExpensiveData();
  // Update cache for next launch
  cache.set(CACHE_KEY, JSON.stringify(data));
  return data;
}

export default function Command() {
  const { data, isLoading } = useCachedPromise(fetchAllData, [], {
    initialData: getInitialData(), // Sync read - instant render!
    keepPreviousData: true,
  });

  return (
    <List isLoading={isLoading && !data?.length}>
      {data?.map(item => <List.Item key={item.id} title={item.name} />)}
    </List>
  );
}
使用同步缓存读取 + 异步刷新实现即时感知加载:
tsx
import { List, Cache } from "@raycast/api";
import { useCachedPromise, withCache } from "@raycast/utils";

const cache = new Cache();
const CACHE_KEY = "myData";

// 在模块加载时同步读取缓存(React渲染前)
function getInitialData(): MyData[] {
  const cached = cache.get(CACHE_KEY);
  if (cached) {
    try {
      return JSON.parse(cached);
    } catch {
      return [];
    }
  }
  return [];
}

// 用withCache包装耗时异步操作(5分钟TTL)
const fetchExpensiveData = withCache(
  async () => {
    // 此处为耗时操作
    return await someSlowOperation();
  },
  { maxAge: 5 * 60 * 1000 }
);

async function fetchAllData(): Promise<MyData[]> {
  const data = await fetchExpensiveData();
  // 更新缓存供下次启动使用
  cache.set(CACHE_KEY, JSON.stringify(data));
  return data;
}

export default function Command() {
  const { data, isLoading } = useCachedPromise(fetchAllData, [], {
    initialData: getInitialData(), // 同步读取 - 即时渲染!
    keepPreviousData: true,
  });

  return (
    <List isLoading={isLoading && !data?.length}>
      {data?.map(item => <List.Item key={item.id} title={item.name} />)}
    </List>
  );
}

Key Caching Utilities

核心缓存工具

UtilityPurpose
Cache
Persistent disk cache, sync read/write
withCache(fn, {maxAge})
Wrap async functions with TTL cache
useCachedPromise
Stale-while-revalidate pattern
LocalStorage
Async key-value storage
工具用途
Cache
持久化磁盘缓存,同步读写
withCache(fn, {maxAge})
为异步函数添加TTL缓存
useCachedPromise
缓存优先、后台更新模式
LocalStorage
异步键值对存储

Avoiding CLS (Content Layout Shift)

避免内容布局偏移(CLS)

Load all data in ONE async function:
tsx
// BAD - causes layout shift
const [customData, setCustomData] = useState([]);
useEffect(() => {
  loadCustomData().then(setCustomData); // Second render!
}, []);

// GOOD - single fetch, no shift
async function fetchAllData() {
  const [dataA, dataB] = await Promise.all([
    fetchDataA(),
    fetchDataB(),
  ]);
  return combineData(dataA, dataB);
}
在一个异步函数中加载所有数据:
tsx
// 错误示例 - 会导致布局偏移
const [customData, setCustomData] = useState([]);
useEffect(() => {
  loadCustomData().then(setCustomData); // 二次渲染!
}, []);

// 正确示例 - 单次获取,无偏移
async function fetchAllData() {
  const [dataA, dataB] = await Promise.all([
    fetchDataA(),
    fetchDataB(),
  ]);
  return combineData(dataA, dataB);
}

Non-Blocking Operations (Prevent UI Freeze)

非阻塞操作(防止UI冻结)

Root cause of "tiny delay": Sync operations (
execSync
,
statSync
,
readdirSync
) block the event loop during revalidation, freezing the UI even with cached data displayed.
tsx
// BAD - blocks event loop, UI freezes during revalidation
import { execSync } from "child_process";
import { statSync, readdirSync, copyFileSync } from "fs";

function fetchData() {
  copyFileSync(src, dest);                    // Blocks!
  const result = execSync("sqlite3 query");   // Blocks!
  const entries = readdirSync(dir);           // Blocks!
  for (const entry of entries) {
    statSync(join(dir, entry));               // Blocks N times!
  }
}

// GOOD - fully async, UI renders cached data while refreshing
import { exec } from "child_process";
import { promisify } from "util";
import { stat, readdir, copyFile, access } from "fs/promises";

const execAsync = promisify(exec);

async function fetchData() {
  await copyFile(src, dest);                         // Non-blocking
  const { stdout } = await execAsync("sqlite3...");  // Non-blocking

  // Use withFileTypes to avoid extra stat calls
  const entries = await readdir(dir, { withFileTypes: true });
  const results = entries
    .filter(e => e.isDirectory())  // No stat needed!
    .map(e => ({ path: join(dir, e.name), name: e.name }));
}
Key optimizations:
  1. Replace
    execSync
    with
    promisify(exec)
    for shell commands
  2. Replace
    existsSync
    with
    access()
    from
    fs/promises
  3. Replace
    readdirSync
    +
    statSync
    loop with
    readdir(dir, { withFileTypes: true })
  4. Run all path validations in parallel with
    Promise.all
  5. Use SQLite URI mode for direct read-only access (no file copy needed)
"微小延迟"的根本原因:同步操作(
execSync
,
statSync
,
readdirSync
)在重新验证时会阻塞事件循环,即使显示了缓存数据也会冻结UI。
tsx
// 错误示例 - 阻塞事件循环,重新验证时UI冻结
import { execSync } from "child_process";
import { statSync, readdirSync, copyFileSync } from "fs";

function fetchData() {
  copyFileSync(src, dest);                    // 阻塞!
  const result = execSync("sqlite3 query");   // 阻塞!
  const entries = readdirSync(dir);           // 阻塞!
  for (const entry of entries) {
    statSync(join(dir, entry));               // 阻塞N次!
  }
}

// 正确示例 - 完全异步,刷新时UI仍显示缓存数据
import { exec } from "child_process";
import { promisify } from "util";
import { stat, readdir, copyFile, access } from "fs/promises";

const execAsync = promisify(exec);

async function fetchData() {
  await copyFile(src, dest);                         // 非阻塞
  const { stdout } = await execAsync("sqlite3...");  // 非阻塞

  // 使用withFileTypes避免额外stat调用
  const entries = await readdir(dir, { withFileTypes: true });
  const results = entries
    .filter(e => e.isDirectory())  // 无需stat!
    .map(e => ({ path: join(dir, e.name), name: e.name }));
}
关键优化点
  1. promisify(exec)
    替代
    execSync
    执行shell命令
  2. fs/promises
    中的
    access()
    替代
    existsSync
  3. readdir(dir, { withFileTypes: true })
    替代
    readdirSync
    +
    statSync
    循环
  4. Promise.all
    并行执行所有路径验证
  5. 使用SQLite URI模式直接只读访问(无需复制文件)

SQLite Direct Access (Skip File Copy)

SQLite 直接访问(跳过文件复制)

When reading SQLite databases from other apps (like Zed, VS Code, etc.), avoid copying the database file. Use URI mode for direct read-only access:
tsx
// BAD - copies entire database file (slow, blocks)
import { copyFileSync, unlinkSync } from "fs";

const tempDb = `/tmp/copy-${Date.now()}.sqlite`;
copyFileSync(originalDb, tempDb);           // Expensive!
execSync(`sqlite3 "${tempDb}" "SELECT..."`);
unlinkSync(tempDb);                         // Cleanup

// GOOD - direct read-only access via URI mode
const uri = `file:${originalDb}?mode=ro&immutable=1`;
const { stdout } = await execAsync(`sqlite3 "${uri}" "SELECT..."`);
URI parameters:
  • mode=ro
    - Read-only mode, no write locks acquired
  • immutable=1
    - Skip WAL/lock checks, treat file as immutable
This eliminates the file copy entirely, saving significant I/O time.
读取其他应用(如Zed、VS Code等)的SQLite数据库时,避免复制数据库文件。使用URI模式直接只读访问:
tsx
// 错误示例 - 复制整个数据库文件(缓慢、阻塞)
import { copyFileSync, unlinkSync } from "fs";

const tempDb = `/tmp/copy-${Date.now()}.sqlite`;
copyFileSync(originalDb, tempDb);           // 高开销!
execSync(`sqlite3 "${tempDb}" "SELECT..."`);
unlinkSync(tempDb);                         // 清理

// 正确示例 - 通过URI模式直接只读访问
const uri = `file:${originalDb}?mode=ro&immutable=1`;
const { stdout } = await execAsync(`sqlite3 "${uri}" "SELECT..."`);
URI参数
  • mode=ro
    - 只读模式,不获取写锁
  • immutable=1
    - 跳过WAL/锁检查,将文件视为不可变
这完全消除了文件复制操作,节省大量I/O时间。

execFile vs exec (Bypass Shell)

execFile vs exec(绕过Shell)

exec
spawns a shell (~20ms overhead),
execFile
calls binary directly (~4ms):
tsx
// BAD - spawns shell, parses command string
import { exec } from "child_process";
const execAsync = promisify(exec);
await execAsync(`sqlite3 -separator '|||' "${db}" "${query}"`);

// GOOD - direct binary execution, ~16ms faster
import { execFile } from "child_process";
const execFileAsync = promisify(execFile);
await execFileAsync("sqlite3", ["-separator", "|||", db, query]);
exec
会启动Shell(约20ms开销),
execFile
直接调用二进制文件(约4ms):
tsx
// 错误示例 - 启动Shell,解析命令字符串
import { exec } from "child_process";
const execAsync = promisify(exec);
await execAsync(`sqlite3 -separator '|||' "${db}" "${query}"`);

// 正确示例 - 直接执行二进制文件,快约16ms
import { execFile } from "child_process";
const execFileAsync = promisify(execFile);
await execFileAsync("sqlite3", ["-separator", "|||", db, query]);

Sidecar Pattern (True Background Preloading)

辅助进程模式(真正的后台预加载)

For truly instant cold starts, use a background worker to pre-warm the cache before the user opens the extension.
The Problem:
view
commands cannot use
interval
(background scheduling). Only
no-view
and
menu-bar
modes support it.
The Solution: Create two commands that share the same cache:
json
// package.json
{
  "commands": [
    {
      "name": "main",
      "title": "My Extension",
      "mode": "view"
    },
    {
      "name": "background-sync",
      "title": "Background Sync",
      "mode": "no-view",
      "interval": "15m"
    }
  ]
}
tsx
// shared-cache.ts - both commands import this
import { Cache } from "@raycast/api";
export const sharedCache = new Cache(); // Shared across extension

// background-sync.tsx (no-view worker)
import { sharedCache } from "./shared-cache";
export default async function Command() {
  const data = await fetchExpensiveData();
  sharedCache.set("projects", JSON.stringify(data));
}

// main.tsx (view command)
import { sharedCache } from "./shared-cache";
function getInitialData() {
  const cached = sharedCache.get("projects");
  return cached ? JSON.parse(cached) : [];
}
export default function Command() {
  const { data } = useCachedPromise(fetchData, [], {
    initialData: getInitialData(), // Instant from pre-warmed cache!
  });
}
Key points:
  • Worker runs silently on interval, user never sees it
  • Both commands share the same
    Cache
    (scoped to extension, not command)
  • View command reads synchronously from pre-warmed cache
  • Use
    15m
    to
    1h
    intervals to avoid battery/rate-limit issues
实现真正的即时冷启动,使用后台工作进程在用户打开扩展前预热缓存。
问题
view
模式命令无法使用
interval
(后台调度)。仅
no-view
menu-bar
模式支持该配置。
解决方案:创建两个共享同一缓存的命令:
json
// package.json
{
  "commands": [
    {
      "name": "main",
      "title": "My Extension",
      "mode": "view"
    },
    {
      "name": "background-sync",
      "title": "Background Sync",
      "mode": "no-view",
      "interval": "15m"
    }
  ]
}
tsx
// shared-cache.ts - 两个命令均导入此文件
import { Cache } from "@raycast/api";
export const sharedCache = new Cache(); // 扩展内共享

// background-sync.tsx(无界面工作进程)
import { sharedCache } from "./shared-cache";
export default async function Command() {
  const data = await fetchExpensiveData();
  sharedCache.set("projects", JSON.stringify(data));
}

// main.tsx(界面命令)
import { sharedCache } from "./shared-cache";
function getInitialData() {
  const cached = sharedCache.get("projects");
  return cached ? JSON.parse(cached) : [];
}
export default function Command() {
  const { data } = useCachedPromise(fetchData, [], {
    initialData: getInitialData(), // 从预热缓存中即时读取!
  });
}
关键点
  • 工作进程按间隔静默运行,用户无感知
  • 两个命令共享同一
    Cache
    (作用域为扩展,而非单个命令)
  • 界面命令从预热缓存中同步读取
  • 使用15分钟到1小时的间隔,避免耗电或触发频率限制

Large Datasets: useSQL over JSON Cache

大型数据集:用useSQL替代JSON缓存

For >1,000 items, use SQLite instead of JSON cache for instant filtering:
tsx
// BAD - loads entire 10MB JSON into memory to filter
const allProjects = JSON.parse(cache.get("projects"));
const filtered = allProjects.filter(p => p.name.includes(query));

// GOOD - SQLite queries only matching rows
import { useSQL } from "@raycast/utils";
const { data } = useSQL(dbPath, `SELECT * FROM projects WHERE name LIKE ?`, [`%${query}%`]);
对于超过1000条数据的场景,使用SQLite而非JSON缓存实现即时过滤:
tsx
// 错误示例 - 加载整个10MB JSON到内存中过滤
const allProjects = JSON.parse(cache.get("projects"));
const filtered = allProjects.filter(p => p.name.includes(query));

// 正确示例 - SQLite仅查询匹配的行
import { useSQL } from "@raycast/utils";
const { data } = useSQL(dbPath, `SELECT * FROM projects WHERE name LIKE ?`, [`%${query}%`]);

Optimistic UI (Instant Actions)

乐观UI(即时操作反馈)

For write operations, update UI immediately before API confirms:
tsx
const { mutate } = useCachedPromise(fetchItems);

async function deleteItem(id: string) {
  await mutate(deleteItemAPI(id), {
    optimisticUpdate: (current) => current.filter(i => i.id !== id),
    rollbackOnError: true, // Revert if API fails
  });
}
User sees instant feedback; rollback happens automatically on failure.
tsx
// BAD - sequential stat calls
const entries = readdirSync(dir);
for (const entry of entries) {
  const s = statSync(join(dir, entry));  // N blocking calls
}

// GOOD - parallel async checks
const checkPath = async (p: string) => {
  try {
    const s = await stat(p);
    return s.isDirectory() ? p : null;
  } catch { return null; }
};

const results = await Promise.all(paths.map(checkPath));
对于写入操作,在API确认前立即更新UI:
tsx
const { mutate } = useCachedPromise(fetchItems);

async function deleteItem(id: string) {
  await mutate(deleteItemAPI(id), {
    optimisticUpdate: (current) => current.filter(i => i.id !== id),
    rollbackOnError: true, // API失败时自动回滚
  });
}
用户会看到即时反馈;若API失败,UI会自动回滚。
tsx
// 错误示例 - 顺序stat调用
const entries = readdirSync(dir);
for (const entry of entries) {
  const s = statSync(join(dir, entry));  // N次阻塞调用
}

// 正确示例 - 并行异步检查
const checkPath = async (p: string) => {
  try {
    const s = await stat(p);
    return s.isDirectory() ? p : null;
  } catch { return null; }
};

const results = await Promise.all(paths.map(checkPath));

Common APIs

常用API

Clipboard

剪贴板

tsx
import { Clipboard } from "@raycast/api";

await Clipboard.copy("text");
await Clipboard.paste("text");
const text = await Clipboard.readText();
tsx
import { Clipboard } from "@raycast/api";

await Clipboard.copy("text");
await Clipboard.paste("text");
const text = await Clipboard.readText();

Notifications

通知

tsx
import { showHUD, showToast, Toast } from "@raycast/api";

// Quick notification (disappears)
await showHUD("Done!");

// Toast with progress
const toast = await showToast({
  style: Toast.Style.Animated,
  title: "Loading...",
});
toast.style = Toast.Style.Success;
toast.title = "Complete";
tsx
import { showHUD, showToast, Toast } from "@raycast/api";

// 快速通知(自动消失)
await showHUD("Done!");

// 带进度的提示框
const toast = await showToast({
  style: Toast.Style.Animated,
  title: "Loading...",
});
toast.style = Toast.Style.Success;
toast.title = "Complete";

AppleScript (macOS Integration)

AppleScript(macOS 集成)

tsx
import { runAppleScript } from "@raycast/utils";

// Get Chrome active tab URL
const url = await runAppleScript(`
  tell application "Google Chrome"
    return URL of active tab of front window
  end tell
`);

// Get Safari URL
const safariUrl = await runAppleScript(`
  tell application "Safari"
    return URL of current tab of front window
  end tell
`);

// Get frontmost app
const app = await runAppleScript(`
  tell application "System Events"
    return name of first application process whose frontmost is true
  end tell
`);
tsx
import { runAppleScript } from "@raycast/utils";

// 获取Chrome当前标签页URL
const url = await runAppleScript(`
  tell application "Google Chrome"
    return URL of active tab of front window
  end tell
`);

// 获取Safari URL
const safariUrl = await runAppleScript(`
  tell application "Safari"
    return URL of current tab of front window
  end tell
`);

// 获取当前活跃应用
const app = await runAppleScript(`
  tell application "System Events"
    return name of first application process whose frontmost is true
  end tell
`);

Fetch Data

获取数据

tsx
// Native fetch works
const response = await fetch("https://api.example.com/data");
const data = await response.json();
tsx
// 原生fetch可直接使用
const response = await fetch("https://api.example.com/data");
const data = await response.json();

Preferences

偏好设置

In package.json:
json
"preferences": [
  {
    "name": "apiKey",
    "type": "password",
    "required": true,
    "title": "API Key",
    "description": "Your API key"
  }
]
In code:
tsx
import { getPreferenceValues } from "@raycast/api";

interface Preferences {
  apiKey: string;
}

const { apiKey } = getPreferenceValues<Preferences>();
在package.json中配置:
json
"preferences": [
  {
    "name": "apiKey",
    "type": "password",
    "required": true,
    "title": "API Key",
    "description": "Your API key"
  }
]
在代码中调用:
tsx
import { getPreferenceValues } from "@raycast/api";

interface Preferences {
  apiKey: string;
}

const { apiKey } = getPreferenceValues<Preferences>();

Creating Extension Icon

创建扩展图标

Use ImageMagick:
bash
convert -size 512x512 xc:'#6366F1' -fill white -gravity center \
  -font Helvetica-Bold -pointsize 280 -annotate +0+20 'M' \
  assets/extension-icon.png
使用ImageMagick:
bash
convert -size 512x512 xc:'#6366F1' -fill white -gravity center \
  -font Helvetica-Bold -pointsize 280 -annotate +0+20 'M' \
  assets/extension-icon.png

Development Workflow

开发工作流

bash
undefined
bash
undefined

Install dependencies

安装依赖

npm install
npm install

Start dev server (hot reload)

启动开发服务器(热重载)

npm run dev
npm run dev

Lint and fix

检查并修复代码规范

npm run fix-lint
npm run fix-lint

Build for production

生产环境构建

npm run build
undefined
npm run build
undefined

Raycast Deeplinks

Raycast 深度链接

Trigger Raycast commands programmatically via URL scheme:
bash
undefined
通过URL scheme以编程方式触发Raycast命令:
bash
undefined

Reload all extensions

重载所有扩展

open "raycast://extensions/raycast/raycast/reload-extensions"
open "raycast://extensions/raycast/raycast/reload-extensions"

Open Raycast

打开Raycast

open "raycast://focus"
open "raycast://focus"

Run any extension command

运行任意扩展命令

open "raycast://extensions/{author}/{extension}/{command}"
undefined
open "raycast://extensions/{author}/{extension}/{command}"
undefined

Auto-reload after build

构建后自动重载

Add to package.json scripts:
json
"build": "ray build --skip-types -e dist -o dist && open raycast://extensions/raycast/raycast/reload-extensions"
Or create a reload script:
bash
#!/bin/bash
npm run build && open "raycast://extensions/raycast/raycast/reload-extensions"
在package.json的scripts中添加:
json
"build": "ray build --skip-types -e dist -o dist && open raycast://extensions/raycast/raycast/reload-extensions"
或创建重载脚本:
bash
#!/bin/bash
npm run build && open "raycast://extensions/raycast/raycast/reload-extensions"

Testing in Raycast

在Raycast中测试

  1. Run
    npm run dev
    (provides hot reload)
  2. Open Raycast
  3. Search for your command name
  4. Press Enter to run
Without dev server running, use deeplink to reload after changes:
bash
npm run build && open "raycast://extensions/raycast/raycast/reload-extensions"
  1. 运行
    npm run dev
    (支持热重载)
  2. 打开Raycast
  3. 搜索你的命令名称
  4. 按Enter运行
若未运行开发服务器,可使用深度链接在修改后重载:
bash
npm run build && open "raycast://extensions/raycast/raycast/reload-extensions"

Publishing

发布

bash
npm run publish
Submits to Raycast Store for review.
bash
npm run publish
提交至Raycast商店等待审核。