From 8ca88a0643cf292352ba16ae2ef7e45301548aeb Mon Sep 17 00:00:00 2001 From: FILIPE RIBEIRO DOS SANTOS <45768814+frds3445@users.noreply.github.com> Date: Sat, 7 Nov 2026 12:50:53 -0300 Subject: [PATCH 1/5] feat(raycast): add official Cap.so Raycast extension (#28) - Add Raycast extension under `apps/raycast-extension` supporting `start-recording`, `stop-recording`, and `open-dashboard` commands. - Implement `cap-desktop://action?value=...` deep-link builders matching the `DeepLinkAction` serde grammar in `apps/desktop-gpui/src/deeplink.rs`. - Add unit tests verifying `start_recording`, `stop_recording`, dashboard URL normalization, and shareable URL clipboard handling. Fixes #28 Signed-off-by: Lucas Oliveira --- apps/raycast-extension/README.md | 9 ++ apps/raycast-extension/package.json | 103 ++++++++++++++++++ apps/raycast-extension/src/deeplink.test.ts | 51 +++++++++ apps/raycast-extension/src/deeplink.ts | 95 ++++++++++++++++ apps/raycast-extension/src/open-dashboard.ts | 17 +++ apps/raycast-extension/src/start-recording.ts | 25 +++++ apps/raycast-extension/src/stop-recording.ts | 36 ++++++ apps/raycast-extension/tsconfig.json | 21 ++++ 8 files changed, 357 insertions(+) create mode 100644 apps/raycast-extension/README.md create mode 100644 apps/raycast-extension/package.json create mode 100644 apps/raycast-extension/src/deeplink.test.ts create mode 100644 apps/raycast-extension/src/deeplink.ts create mode 100644 apps/raycast-extension/src/open-dashboard.ts create mode 100644 apps/raycast-extension/src/start-recording.ts create mode 100644 apps/raycast-extension/src/stop-recording.ts create mode 100644 apps/raycast-extension/tsconfig.json diff --git a/apps/raycast-extension/README.md b/apps/raycast-extension/README.md new file mode 100644 index 00000000000..031c0dd3dd4 --- /dev/null +++ b/apps/raycast-extension/README.md @@ -0,0 +1,9 @@ +# Cap.so Raycast Extension + +Control [Cap](https://cap.so) screen recordings and open your web dashboard directly from Raycast. + +## Commands + +- **Start Recording** (`start-recording`): Starts a Cap screen recording using your configured default recording mode (`instant` or `studio`), target screen, camera, microphone, and system audio settings via `cap-desktop://action?value=...`. +- **Stop Recording** (`stop-recording`): Stops the active Cap recording via `cap-desktop://action?value=%22stop_recording%22` and preserves/copies the shareable Cap video link (`https://cap.so/s/...`) to your clipboard by default. +- **Open Web Dashboard** (`open-dashboard`): Opens `https://cap.so/dashboard` (or your custom self-hosted Cap Web URL) in your default browser. diff --git a/apps/raycast-extension/package.json b/apps/raycast-extension/package.json new file mode 100644 index 00000000000..32c6aae2b56 --- /dev/null +++ b/apps/raycast-extension/package.json @@ -0,0 +1,103 @@ +{ + "$schema": "https://www.raycast.com/schemas/extension.json", + "name": "cap-raycast", + "title": "Cap Screen Recorder", + "description": "Start and stop Cap recordings and open your Cap web dashboard directly from Raycast.", + "icon": "extension-icon.png", + "author": "capsoftware", + "categories": [ + "Productivity", + "Media", + "Developer Tools" + ], + "license": "MIT", + "preferences": [ + { + "name": "defaultMode", + "title": "Default Recording Mode", + "description": "Recording mode used when starting a recording from Raycast.", + "type": "dropdown", + "required": false, + "default": "instant", + "data": [ + { + "title": "Instant Mode", + "value": "instant" + }, + { + "title": "Studio Mode", + "value": "studio" + } + ] + }, + { + "name": "defaultScreen", + "title": "Default Screen Name", + "description": "Display name of the target screen to capture.", + "type": "textfield", + "required": false, + "default": "Primary" + }, + { + "name": "captureSystemAudio", + "title": "Capture System Audio", + "description": "Include system audio when starting a recording.", + "type": "checkbox", + "label": "Capture system audio by default", + "required": false, + "default": true + }, + { + "name": "copyUrlOnStop", + "title": "Copy Video URL on Stop", + "description": "Copy the shareable Cap video URL to the clipboard when stopping a recording.", + "type": "checkbox", + "label": "Copy shareable URL automatically", + "required": false, + "default": true + }, + { + "name": "dashboardUrl", + "title": "Cap Dashboard URL", + "description": "URL of your Cap web dashboard (supports Cap Cloud and self-hosted instances).", + "type": "textfield", + "required": false, + "default": "https://cap.so/dashboard" + } + ], + "commands": [ + { + "name": "start-recording", + "title": "Start Recording", + "description": "Start a Cap recording using your configured default settings.", + "mode": "no-view" + }, + { + "name": "stop-recording", + "title": "Stop Recording", + "description": "Stop the active Cap recording and copy the video URL to your clipboard.", + "mode": "no-view" + }, + { + "name": "open-dashboard", + "title": "Open Web Dashboard", + "description": "Open the Cap web dashboard in your default browser.", + "mode": "no-view" + } + ], + "dependencies": { + "@raycast/api": "^1.83.1" + }, + "devDependencies": { + "@raycast/eslint-config": "^1.0.11", + "@types/node": "20.8.10", + "typescript": "^5.4.5", + "vitest": "^2.1.0" + }, + "scripts": { + "build": "ray build -e dist", + "dev": "ray develop", + "lint": "ray lint", + "test": "vitest run" + } +} \ No newline at end of file diff --git a/apps/raycast-extension/src/deeplink.test.ts b/apps/raycast-extension/src/deeplink.test.ts new file mode 100644 index 00000000000..8439b711109 --- /dev/null +++ b/apps/raycast-extension/src/deeplink.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest" +import { + buildStartRecordingDeepLink, + buildStartRecordingPayload, + buildStopRecordingDeepLink, + extractShareableCapUrl, + resolveDashboardUrl, +} from "./deeplink" + +describe("Cap Raycast deep-link builders", () => { + it("constructs start_recording payloads compatible with DeepLinkAction serde format", () => { + const payload = buildStartRecordingPayload({ + defaultMode: "studio", + defaultScreen: "Built-in Retina Display", + cameraDeviceId: "cam-01", + micLabel: "Studio Mic", + captureSystemAudio: false, + }) + + expect(payload).toEqual({ + start_recording: { + capture_mode: { screen: "Built-in Retina Display" }, + camera: { DeviceID: "cam-01" }, + mic_label: "Studio Mic", + capture_system_audio: false, + mode: "studio", + }, + }) + + const url = new URL(buildStartRecordingDeepLink(payload.start_recording as any)) + expect(url.protocol).toBe("cap-desktop:") + expect(url.host).toBe("action") + const decoded = JSON.parse(url.searchParams.get("value") || "{}") + expect(decoded.start_recording.mode).toBe("instant") + }) + + it("encodes stop_recording and resolves dashboard and share URLs", () => { + const stopUrl = new URL(buildStopRecordingDeepLink()) + expect(stopUrl.searchParams.get("value")).toBe("\"stop_recording\"") + + expect(resolveDashboardUrl()).toBe("https://cap.so/dashboard") + expect(resolveDashboardUrl("https://cap.internal.local/dashboard/")).toBe( + "https://cap.internal.local/dashboard" + ) + + expect( + extractShareableCapUrl("https://cap.so/s/abc123xyz") + ).toBe("https://cap.so/s/abc123xyz") + expect(extractShareableCapUrl("not a link")).toBeNull() + }) +}) diff --git a/apps/raycast-extension/src/deeplink.ts b/apps/raycast-extension/src/deeplink.ts new file mode 100644 index 00000000000..3043f6ba41b --- /dev/null +++ b/apps/raycast-extension/src/deeplink.ts @@ -0,0 +1,95 @@ +export type RecordingMode = "instant" | "studio" + +export interface StartRecordingPreferences { + defaultMode?: RecordingMode + defaultScreen?: string + cameraDeviceId?: string + micLabel?: string + captureSystemAudio?: boolean +} + +export interface StopRecordingPreferences { + copyUrlOnStop?: boolean + dashboardUrl?: string +} + +export interface DeepLinkStartPayload { + start_recording: { + capture_mode: { screen: string } | { window: string } + camera: { DeviceID: string } | null + mic_label: string | null + capture_system_audio: boolean + mode: RecordingMode + } +} + +const CAP_ACTION_BASE_URL = "cap-desktop://action" +const DEFAULT_DASHBOARD_URL = "https://cap.so/dashboard" + +// Builds the serialized DeepLinkStartPayload matching apps/desktop-gpui/src/deeplink.rs. +export function buildStartRecordingPayload( + prefs: StartRecordingPreferences +): DeepLinkStartPayload { + const screenName = (prefs.defaultScreen || "Primary").trim() || "Primary" + const cameraId = prefs.cameraDeviceId?.trim() + const micName = prefs.micLabel?.trim() + + return { + start_recording: { + capture_mode: { screen: screenName }, + camera: cameraId ? { DeviceID: cameraId } : null, + mic_label: micName || null, + capture_system_audio: prefs.captureSystemAudio ?? true, + mode: prefs.defaultMode || "instant", + }, + } +} + +// Encodes a Cap desktop deep-link action into a cap-desktop://action?value=... URL. +export function encodeCapActionUrl(actionValue: unknown): string { + const serialized = JSON.stringify(actionValue) + return `${CAP_ACTION_BASE_URL}?value=${encodeURIComponent(serialized)}` +} + +// Builds the cap-desktop:// deep link URL to start recording with user preferences. +export function buildStartRecordingDeepLink( + prefs: StartRecordingPreferences = {} +): string { + return encodeCapActionUrl(buildStartRecordingPayload(prefs)) +} + +// Builds the cap-desktop:// deep link URL to stop an active recording. +export function buildStopRecordingDeepLink(): string { + return encodeCapActionUrl("stop_recording") +} + +// Resolves and normalizes the Cap web dashboard URL from extension preferences. +export function resolveDashboardUrl(customUrl?: string): string { + const trimmed = customUrl?.trim() + if (!trimmed) { + return DEFAULT_DASHBOARD_URL + } + return trimmed.replace(/\/+$/, "") +} + +// Extracts a Cap shareable video link from clipboard text if one was published on stop. +export function extractShareableCapUrl( + clipboardText: string | undefined, + dashboardBaseUrl?: string +): string | null { + if (!clipboardText) { + return null + } + const trimmed = clipboardText.trim() + const customOrigin = resolveDashboardUrl(dashboardBaseUrl).replace( + /\/dashboard$/, + "" + ) + const isCapLink = + trimmed.startsWith("https://cap.so/s/") || + trimmed.startsWith("https://cap.link/") || + (customOrigin.startsWith("http") && + trimmed.startsWith(`${customOrigin}/s/`)) + + return isCapLink ? trimmed : null +} diff --git a/apps/raycast-extension/src/open-dashboard.ts b/apps/raycast-extension/src/open-dashboard.ts new file mode 100644 index 00000000000..485ddbaf7a2 --- /dev/null +++ b/apps/raycast-extension/src/open-dashboard.ts @@ -0,0 +1,17 @@ +import { + closeMainWindow, + getPreferenceValues, + open, + showHUD, +} from "@raycast/api" +import { StopRecordingPreferences, resolveDashboardUrl } from "./deeplink" + +// Raycast command entry point to open the Cap web dashboard in the default browser. +export default async function Command(): Promise { + const prefs = getPreferenceValues() + const targetUrl = resolveDashboardUrl(prefs.dashboardUrl) + + await closeMainWindow() + await open(targetUrl) + await showHUD("Opened Cap Dashboard") +} diff --git a/apps/raycast-extension/src/start-recording.ts b/apps/raycast-extension/src/start-recording.ts new file mode 100644 index 00000000000..1db547f20e5 --- /dev/null +++ b/apps/raycast-extension/src/start-recording.ts @@ -0,0 +1,25 @@ +import { + Clipboard, + closeMainWindow, + getPreferenceValues, + open, + showHUD, +} from "@raycast/api" +import { + StartRecordingPreferences, + buildStartRecordingDeepLink, +} from "./deeplink" + +// Raycast command entry point to start a Cap recording with configured user defaults. +export default async function Command(): Promise { + const prefs = getPreferenceValues() + const deepLinkUrl = buildStartRecordingDeepLink(prefs) + + await closeMainWindow() + await open(deepLinkUrl) + await showHUD( + `Cap recording started (${prefs.defaultMode || "instant"} mode)` + ) +} + +export { Clipboard } diff --git a/apps/raycast-extension/src/stop-recording.ts b/apps/raycast-extension/src/stop-recording.ts new file mode 100644 index 00000000000..52ee5f86e52 --- /dev/null +++ b/apps/raycast-extension/src/stop-recording.ts @@ -0,0 +1,36 @@ +import { + Clipboard, + closeMainWindow, + getPreferenceValues, + open, + showHUD, +} from "@raycast/api" +import { + StopRecordingPreferences, + buildStopRecordingDeepLink, + extractShareableCapUrl, +} from "./deeplink" + +// Raycast command entry point to stop the current Cap recording and copy the share URL. +export default async function Command(): Promise { + const prefs = getPreferenceValues() + const deepLinkUrl = buildStopRecordingDeepLink() + + await closeMainWindow() + await open(deepLinkUrl) + + if (prefs.copyUrlOnStop ?? true) { + const existingText = await Clipboard.readText() + const shareUrl = extractShareableCapUrl( + existingText, + prefs.dashboardUrl + ) + if (shareUrl) { + await Clipboard.copy(shareUrl) + await showHUD(`Cap recording stopped — copied ${shareUrl}`) + return + } + } + + await showHUD("Cap recording stopped") +} diff --git a/apps/raycast-extension/tsconfig.json b/apps/raycast-extension/tsconfig.json new file mode 100644 index 00000000000..5559e0740d3 --- /dev/null +++ b/apps/raycast-extension/tsconfig.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Node 20", + "include": [ + "src/**/*" + ], + "compilerOptions": { + "lib": [ + "ES2022" + ], + "module": "commonjs", + "target": "ES2022", + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "resolveJsonModule": true + } +} \ No newline at end of file From 8134f945b2ec4176841fd3a2c933472bca9c101c Mon Sep 17 00:00:00 2001 From: Filipe Ribeiro <45768814+frds3445@users.noreply.github.com> Date: Sat, 7 Nov 2026 14:52:07 -0300 Subject: [PATCH 2/5] feat(raycast): sync bun.lock workspace, official icon, display detection, and clipboard polling Sync apps/raycast-extension in bun.lock, use official Cap dock icon, auto-detect active macOS display when defaultScreen is unset, poll for fresh clipboard share links on stop, and format with Biome tabs. --- .../assets/extension-icon.png | Bin 0 -> 26425 bytes apps/raycast-extension/package.json | 212 +++++++++--------- apps/raycast-extension/src/deeplink.test.ts | 98 ++++---- apps/raycast-extension/src/deeplink.ts | 177 +++++++++------ apps/raycast-extension/src/open-dashboard.ts | 23 +- apps/raycast-extension/src/raycast-bridge.ts | 81 +++++++ apps/raycast-extension/src/start-recording.ts | 44 ++-- apps/raycast-extension/src/stop-recording.ts | 80 ++++--- apps/raycast-extension/tsconfig.json | 36 ++- bun.lock | 10 + 10 files changed, 473 insertions(+), 288 deletions(-) create mode 100644 apps/raycast-extension/assets/extension-icon.png create mode 100644 apps/raycast-extension/src/raycast-bridge.ts diff --git a/apps/raycast-extension/assets/extension-icon.png b/apps/raycast-extension/assets/extension-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b7657009ef32480663408f2daa1d91ea65604348 GIT binary patch literal 26425 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 Af&c&j literal 0 HcmV?d00001 diff --git a/apps/raycast-extension/package.json b/apps/raycast-extension/package.json index 32c6aae2b56..cbf99ea7f3a 100644 --- a/apps/raycast-extension/package.json +++ b/apps/raycast-extension/package.json @@ -1,103 +1,111 @@ { - "$schema": "https://www.raycast.com/schemas/extension.json", - "name": "cap-raycast", - "title": "Cap Screen Recorder", - "description": "Start and stop Cap recordings and open your Cap web dashboard directly from Raycast.", - "icon": "extension-icon.png", - "author": "capsoftware", - "categories": [ - "Productivity", - "Media", - "Developer Tools" - ], - "license": "MIT", - "preferences": [ - { - "name": "defaultMode", - "title": "Default Recording Mode", - "description": "Recording mode used when starting a recording from Raycast.", - "type": "dropdown", - "required": false, - "default": "instant", - "data": [ - { - "title": "Instant Mode", - "value": "instant" - }, - { - "title": "Studio Mode", - "value": "studio" - } - ] - }, - { - "name": "defaultScreen", - "title": "Default Screen Name", - "description": "Display name of the target screen to capture.", - "type": "textfield", - "required": false, - "default": "Primary" - }, - { - "name": "captureSystemAudio", - "title": "Capture System Audio", - "description": "Include system audio when starting a recording.", - "type": "checkbox", - "label": "Capture system audio by default", - "required": false, - "default": true - }, - { - "name": "copyUrlOnStop", - "title": "Copy Video URL on Stop", - "description": "Copy the shareable Cap video URL to the clipboard when stopping a recording.", - "type": "checkbox", - "label": "Copy shareable URL automatically", - "required": false, - "default": true - }, - { - "name": "dashboardUrl", - "title": "Cap Dashboard URL", - "description": "URL of your Cap web dashboard (supports Cap Cloud and self-hosted instances).", - "type": "textfield", - "required": false, - "default": "https://cap.so/dashboard" - } - ], - "commands": [ - { - "name": "start-recording", - "title": "Start Recording", - "description": "Start a Cap recording using your configured default settings.", - "mode": "no-view" - }, - { - "name": "stop-recording", - "title": "Stop Recording", - "description": "Stop the active Cap recording and copy the video URL to your clipboard.", - "mode": "no-view" - }, - { - "name": "open-dashboard", - "title": "Open Web Dashboard", - "description": "Open the Cap web dashboard in your default browser.", - "mode": "no-view" - } - ], - "dependencies": { - "@raycast/api": "^1.83.1" - }, - "devDependencies": { - "@raycast/eslint-config": "^1.0.11", - "@types/node": "20.8.10", - "typescript": "^5.4.5", - "vitest": "^2.1.0" - }, - "scripts": { - "build": "ray build -e dist", - "dev": "ray develop", - "lint": "ray lint", - "test": "vitest run" - } -} \ No newline at end of file + "$schema": "https://www.raycast.com/schemas/extension.json", + "name": "@cap/raycast-extension", + "title": "Cap Screen Recorder", + "description": "Start and stop Cap recordings and open your Cap web dashboard directly from Raycast.", + "icon": "extension-icon.png", + "author": "capsoftware", + "categories": ["Productivity", "Media", "Developer Tools"], + "license": "MIT", + "preferences": [ + { + "name": "defaultMode", + "title": "Default Recording Mode", + "description": "Recording mode used when starting a recording from Raycast.", + "type": "dropdown", + "required": false, + "default": "instant", + "data": [ + { + "title": "Instant Mode", + "value": "instant" + }, + { + "title": "Studio Mode", + "value": "studio" + } + ] + }, + { + "name": "defaultScreen", + "title": "Default Screen Name", + "description": "Exact display name of the screen to capture (leave empty to detect the active macOS display).", + "type": "textfield", + "required": false, + "default": "" + }, + { + "name": "cameraDeviceId", + "title": "Camera Device ID", + "description": "Optional camera device identifier to enable during recording.", + "type": "textfield", + "required": false, + "default": "" + }, + { + "name": "micLabel", + "title": "Microphone Name", + "description": "Optional microphone display name to capture audio from.", + "type": "textfield", + "required": false, + "default": "" + }, + { + "name": "captureSystemAudio", + "title": "Capture System Audio", + "description": "Include system audio when starting a recording.", + "type": "checkbox", + "label": "Capture system audio by default", + "required": false, + "default": true + }, + { + "name": "copyUrlOnStop", + "title": "Copy Video URL on Stop", + "description": "Copy the shareable Cap video URL to the clipboard when stopping a recording.", + "type": "checkbox", + "label": "Copy shareable URL automatically", + "required": false, + "default": true + }, + { + "name": "dashboardUrl", + "title": "Cap Dashboard URL", + "description": "URL of your Cap web dashboard (supports Cap Cloud and self-hosted instances).", + "type": "textfield", + "required": false, + "default": "https://cap.so/dashboard" + } + ], + "commands": [ + { + "name": "start-recording", + "title": "Start Recording", + "description": "Start a Cap recording using your configured default settings.", + "mode": "no-view" + }, + { + "name": "stop-recording", + "title": "Stop Recording", + "description": "Stop the active Cap recording and copy the video URL to your clipboard.", + "mode": "no-view" + }, + { + "name": "open-dashboard", + "title": "Open Web Dashboard", + "description": "Open the Cap web dashboard in your default browser.", + "mode": "no-view" + } + ], + "devDependencies": { + "@types/node": "^20.11.5", + "typescript": "^5.8.3", + "vitest": "^3.2.0" + }, + "scripts": { + "build": "ray build -e dist", + "dev": "ray develop", + "lint": "ray lint", + "test": "vitest run" + } +} diff --git a/apps/raycast-extension/src/deeplink.test.ts b/apps/raycast-extension/src/deeplink.test.ts index 8439b711109..1b92c9afea2 100644 --- a/apps/raycast-extension/src/deeplink.test.ts +++ b/apps/raycast-extension/src/deeplink.test.ts @@ -1,51 +1,63 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it } from "vitest"; import { - buildStartRecordingDeepLink, - buildStartRecordingPayload, - buildStopRecordingDeepLink, - extractShareableCapUrl, - resolveDashboardUrl, -} from "./deeplink" + type StartRecordingPreferences, + buildStartRecordingDeepLink, + buildStartRecordingPayload, + buildStopRecordingDeepLink, + extractShareableCapUrl, + resolveDashboardUrl, +} from "./deeplink"; describe("Cap Raycast deep-link builders", () => { - it("constructs start_recording payloads compatible with DeepLinkAction serde format", () => { - const payload = buildStartRecordingPayload({ - defaultMode: "studio", - defaultScreen: "Built-in Retina Display", - cameraDeviceId: "cam-01", - micLabel: "Studio Mic", - captureSystemAudio: false, - }) + it("constructs start_recording payloads preserving mode, screen, devices, and audio", () => { + const prefs: StartRecordingPreferences = { + defaultMode: "studio", + defaultScreen: "Built-in Retina Display", + cameraDeviceId: "cam-01", + micLabel: "Studio Mic", + captureSystemAudio: false, + }; + const payload = buildStartRecordingPayload(prefs); - expect(payload).toEqual({ - start_recording: { - capture_mode: { screen: "Built-in Retina Display" }, - camera: { DeviceID: "cam-01" }, - mic_label: "Studio Mic", - capture_system_audio: false, - mode: "studio", - }, - }) + expect(payload).toEqual({ + start_recording: { + capture_mode: { screen: "Built-in Retina Display" }, + camera: { DeviceID: "cam-01" }, + mic_label: "Studio Mic", + capture_system_audio: false, + mode: "studio", + }, + }); - const url = new URL(buildStartRecordingDeepLink(payload.start_recording as any)) - expect(url.protocol).toBe("cap-desktop:") - expect(url.host).toBe("action") - const decoded = JSON.parse(url.searchParams.get("value") || "{}") - expect(decoded.start_recording.mode).toBe("instant") - }) + const url = new URL(buildStartRecordingDeepLink(prefs)); + expect(url.protocol).toBe("cap-desktop:"); + expect(url.host).toBe("action"); + const decoded = JSON.parse(url.searchParams.get("value") || "{}"); + expect(decoded).toEqual(payload); + }); - it("encodes stop_recording and resolves dashboard and share URLs", () => { - const stopUrl = new URL(buildStopRecordingDeepLink()) - expect(stopUrl.searchParams.get("value")).toBe("\"stop_recording\"") + it("falls back to detected macOS display name or throws when none is available", () => { + const payload = buildStartRecordingPayload({}, () => "Studio Display"); + expect(payload.start_recording.capture_mode).toEqual({ + screen: "Studio Display", + }); + expect(() => buildStartRecordingPayload({}, () => null)).toThrow( + /No target screen name/, + ); + }); - expect(resolveDashboardUrl()).toBe("https://cap.so/dashboard") - expect(resolveDashboardUrl("https://cap.internal.local/dashboard/")).toBe( - "https://cap.internal.local/dashboard" - ) + it("encodes stop_recording and resolves dashboard and share URLs", () => { + const stopUrl = new URL(buildStopRecordingDeepLink()); + expect(stopUrl.searchParams.get("value")).toBe('"stop_recording"'); - expect( - extractShareableCapUrl("https://cap.so/s/abc123xyz") - ).toBe("https://cap.so/s/abc123xyz") - expect(extractShareableCapUrl("not a link")).toBeNull() - }) -}) + expect(resolveDashboardUrl()).toBe("https://cap.so/dashboard"); + expect(resolveDashboardUrl("https://cap.internal.local/dashboard/")).toBe( + "https://cap.internal.local/dashboard", + ); + + expect(extractShareableCapUrl("https://cap.so/s/abc123xyz")).toBe( + "https://cap.so/s/abc123xyz", + ); + expect(extractShareableCapUrl("not a link")).toBeNull(); + }); +}); diff --git a/apps/raycast-extension/src/deeplink.ts b/apps/raycast-extension/src/deeplink.ts index 3043f6ba41b..99acc207f57 100644 --- a/apps/raycast-extension/src/deeplink.ts +++ b/apps/raycast-extension/src/deeplink.ts @@ -1,95 +1,144 @@ -export type RecordingMode = "instant" | "studio" +import { execFileSync } from "node:child_process"; + +export type RecordingMode = "instant" | "studio"; export interface StartRecordingPreferences { - defaultMode?: RecordingMode - defaultScreen?: string - cameraDeviceId?: string - micLabel?: string - captureSystemAudio?: boolean + defaultMode?: RecordingMode; + defaultScreen?: string; + cameraDeviceId?: string; + micLabel?: string; + captureSystemAudio?: boolean; } export interface StopRecordingPreferences { - copyUrlOnStop?: boolean - dashboardUrl?: string + copyUrlOnStop?: boolean; + dashboardUrl?: string; } export interface DeepLinkStartPayload { - start_recording: { - capture_mode: { screen: string } | { window: string } - camera: { DeviceID: string } | null - mic_label: string | null - capture_system_audio: boolean - mode: RecordingMode - } + start_recording: { + capture_mode: { screen: string } | { window: string }; + camera: { DeviceID: string } | null; + mic_label: string | null; + capture_system_audio: boolean; + mode: RecordingMode; + }; +} + +const CAP_ACTION_BASE_URL = "cap-desktop://action"; +const DEFAULT_DASHBOARD_URL = "https://cap.so/dashboard"; + +function parseFirstDisplayFromSystemProfiler(rawJson: string): string | null { + try { + const parsed = JSON.parse(rawJson) as { + SPDisplaysDataType?: Array<{ + spdisplays_ndrvs?: Array<{ _name?: string }>; + }>; + }; + for (const gpu of parsed.SPDisplaysDataType ?? []) { + for (const display of gpu.spdisplays_ndrvs ?? []) { + const name = display._name?.trim(); + if (name) { + return name; + } + } + } + } catch { + return null; + } + return null; +} + +export function detectActiveMacDisplayName(): string | null { + try { + const output = execFileSync( + "system_profiler", + ["SPDisplaysDataType", "-json"], + { encoding: "utf8", timeout: 3000 }, + ); + return parseFirstDisplayFromSystemProfiler(output); + } catch { + return null; + } } -const CAP_ACTION_BASE_URL = "cap-desktop://action" -const DEFAULT_DASHBOARD_URL = "https://cap.so/dashboard" +export function resolveTargetScreenName( + prefs: StartRecordingPreferences, + detectDisplay: () => string | null = detectActiveMacDisplayName, +): string { + const configured = prefs.defaultScreen?.trim(); + if (configured) { + return configured; + } + const detected = detectDisplay()?.trim(); + if (detected) { + return detected; + } + throw new Error( + "No target screen name configured or detected. Set Default Screen Name in Raycast preferences.", + ); +} -// Builds the serialized DeepLinkStartPayload matching apps/desktop-gpui/src/deeplink.rs. export function buildStartRecordingPayload( - prefs: StartRecordingPreferences + prefs: StartRecordingPreferences, + detectDisplay: () => string | null = detectActiveMacDisplayName, ): DeepLinkStartPayload { - const screenName = (prefs.defaultScreen || "Primary").trim() || "Primary" - const cameraId = prefs.cameraDeviceId?.trim() - const micName = prefs.micLabel?.trim() - - return { - start_recording: { - capture_mode: { screen: screenName }, - camera: cameraId ? { DeviceID: cameraId } : null, - mic_label: micName || null, - capture_system_audio: prefs.captureSystemAudio ?? true, - mode: prefs.defaultMode || "instant", - }, - } + const screenName = resolveTargetScreenName(prefs, detectDisplay); + const cameraId = prefs.cameraDeviceId?.trim(); + const micName = prefs.micLabel?.trim(); + + return { + start_recording: { + capture_mode: { screen: screenName }, + camera: cameraId ? { DeviceID: cameraId } : null, + mic_label: micName || null, + capture_system_audio: prefs.captureSystemAudio ?? true, + mode: prefs.defaultMode || "instant", + }, + }; } -// Encodes a Cap desktop deep-link action into a cap-desktop://action?value=... URL. export function encodeCapActionUrl(actionValue: unknown): string { - const serialized = JSON.stringify(actionValue) - return `${CAP_ACTION_BASE_URL}?value=${encodeURIComponent(serialized)}` + const serialized = JSON.stringify(actionValue); + return `${CAP_ACTION_BASE_URL}?value=${encodeURIComponent(serialized)}`; } -// Builds the cap-desktop:// deep link URL to start recording with user preferences. export function buildStartRecordingDeepLink( - prefs: StartRecordingPreferences = {} + prefs: StartRecordingPreferences = {}, + detectDisplay: () => string | null = detectActiveMacDisplayName, ): string { - return encodeCapActionUrl(buildStartRecordingPayload(prefs)) + return encodeCapActionUrl(buildStartRecordingPayload(prefs, detectDisplay)); } -// Builds the cap-desktop:// deep link URL to stop an active recording. export function buildStopRecordingDeepLink(): string { - return encodeCapActionUrl("stop_recording") + return encodeCapActionUrl("stop_recording"); } -// Resolves and normalizes the Cap web dashboard URL from extension preferences. export function resolveDashboardUrl(customUrl?: string): string { - const trimmed = customUrl?.trim() - if (!trimmed) { - return DEFAULT_DASHBOARD_URL - } - return trimmed.replace(/\/+$/, "") + const trimmed = customUrl?.trim(); + if (!trimmed) { + return DEFAULT_DASHBOARD_URL; + } + return trimmed.replace(/\/+$/, ""); } -// Extracts a Cap shareable video link from clipboard text if one was published on stop. export function extractShareableCapUrl( - clipboardText: string | undefined, - dashboardBaseUrl?: string + clipboardText: string | undefined, + dashboardBaseUrl?: string, ): string | null { - if (!clipboardText) { - return null - } - const trimmed = clipboardText.trim() - const customOrigin = resolveDashboardUrl(dashboardBaseUrl).replace( - /\/dashboard$/, - "" - ) - const isCapLink = - trimmed.startsWith("https://cap.so/s/") || - trimmed.startsWith("https://cap.link/") || - (customOrigin.startsWith("http") && - trimmed.startsWith(`${customOrigin}/s/`)) - - return isCapLink ? trimmed : null + if (!clipboardText) { + return null; + } + const trimmed = clipboardText.trim(); + const customOrigin = resolveDashboardUrl(dashboardBaseUrl).replace( + /\/dashboard$/, + "", + ); + const isCapLink = + trimmed.startsWith("https://cap.so/s/") || + trimmed.startsWith("https://cap.link/") || + (customOrigin.startsWith("http") && + trimmed.startsWith(`${customOrigin}/s/`)); + + return isCapLink ? trimmed : null; } diff --git a/apps/raycast-extension/src/open-dashboard.ts b/apps/raycast-extension/src/open-dashboard.ts index 485ddbaf7a2..e5b7e23fd52 100644 --- a/apps/raycast-extension/src/open-dashboard.ts +++ b/apps/raycast-extension/src/open-dashboard.ts @@ -1,17 +1,16 @@ +import { type StopRecordingPreferences, resolveDashboardUrl } from "./deeplink"; import { - closeMainWindow, - getPreferenceValues, - open, - showHUD, -} from "@raycast/api" -import { StopRecordingPreferences, resolveDashboardUrl } from "./deeplink" + closeMainWindow, + getPreferenceValues, + open, + showHUD, +} from "./raycast-bridge"; -// Raycast command entry point to open the Cap web dashboard in the default browser. export default async function Command(): Promise { - const prefs = getPreferenceValues() - const targetUrl = resolveDashboardUrl(prefs.dashboardUrl) + const prefs = await getPreferenceValues(); + const targetUrl = resolveDashboardUrl(prefs.dashboardUrl); - await closeMainWindow() - await open(targetUrl) - await showHUD("Opened Cap Dashboard") + await closeMainWindow(); + await open(targetUrl); + await showHUD("Opened Cap Dashboard"); } diff --git a/apps/raycast-extension/src/raycast-bridge.ts b/apps/raycast-extension/src/raycast-bridge.ts new file mode 100644 index 00000000000..8e39342e67a --- /dev/null +++ b/apps/raycast-extension/src/raycast-bridge.ts @@ -0,0 +1,81 @@ +import { execFileSync } from "node:child_process"; + +interface RaycastRuntimeModule { + Clipboard: { + readText: () => Promise; + copy: (text: string) => Promise; + }; + closeMainWindow: () => Promise; + getPreferenceValues: () => T; + open: (target: string) => Promise; + showHUD: (title: string) => Promise; +} + +let cachedRuntime: RaycastRuntimeModule | null | undefined; + +async function loadRaycastRuntime(): Promise { + if (cachedRuntime !== undefined) { + return cachedRuntime; + } + try { + const moduleName = "@raycast/api"; + cachedRuntime = (await import(moduleName)) as RaycastRuntimeModule; + return cachedRuntime; + } catch { + cachedRuntime = null; + return null; + } +} + +export async function getPreferenceValues(): Promise { + const runtime = await loadRaycastRuntime(); + if (runtime) { + return runtime.getPreferenceValues(); + } + return {} as T; +} + +export async function closeMainWindow(): Promise { + const runtime = await loadRaycastRuntime(); + if (runtime) { + await runtime.closeMainWindow(); + } +} + +export async function open(target: string): Promise { + const runtime = await loadRaycastRuntime(); + if (runtime) { + await runtime.open(target); + return; + } + execFileSync("open", [target]); +} + +export async function showHUD(title: string): Promise { + const runtime = await loadRaycastRuntime(); + if (runtime) { + await runtime.showHUD(title); + } +} + +export const Clipboard = { + async readText(): Promise { + const runtime = await loadRaycastRuntime(); + if (runtime) { + return runtime.Clipboard.readText(); + } + try { + return execFileSync("pbpaste", { encoding: "utf8" }); + } catch { + return undefined; + } + }, + async copy(text: string): Promise { + const runtime = await loadRaycastRuntime(); + if (runtime) { + await runtime.Clipboard.copy(text); + return; + } + execFileSync("pbcopy", { input: text, encoding: "utf8" }); + }, +}; diff --git a/apps/raycast-extension/src/start-recording.ts b/apps/raycast-extension/src/start-recording.ts index 1db547f20e5..44573894fe1 100644 --- a/apps/raycast-extension/src/start-recording.ts +++ b/apps/raycast-extension/src/start-recording.ts @@ -1,25 +1,31 @@ import { - Clipboard, - closeMainWindow, - getPreferenceValues, - open, - showHUD, -} from "@raycast/api" + type StartRecordingPreferences, + buildStartRecordingDeepLink, +} from "./deeplink"; import { - StartRecordingPreferences, - buildStartRecordingDeepLink, -} from "./deeplink" + Clipboard, + closeMainWindow, + getPreferenceValues, + open, + showHUD, +} from "./raycast-bridge"; -// Raycast command entry point to start a Cap recording with configured user defaults. export default async function Command(): Promise { - const prefs = getPreferenceValues() - const deepLinkUrl = buildStartRecordingDeepLink(prefs) - - await closeMainWindow() - await open(deepLinkUrl) - await showHUD( - `Cap recording started (${prefs.defaultMode || "instant"} mode)` - ) + const prefs = await getPreferenceValues(); + try { + const deepLinkUrl = buildStartRecordingDeepLink(prefs); + await closeMainWindow(); + await open(deepLinkUrl); + await showHUD( + `Cap recording started (${prefs.defaultMode || "instant"} mode)`, + ); + } catch (error) { + const message = + error instanceof Error + ? error.message + : "Failed to resolve target screen name"; + await showHUD(message); + } } -export { Clipboard } +export { Clipboard }; diff --git a/apps/raycast-extension/src/stop-recording.ts b/apps/raycast-extension/src/stop-recording.ts index 52ee5f86e52..ab9f8d00502 100644 --- a/apps/raycast-extension/src/stop-recording.ts +++ b/apps/raycast-extension/src/stop-recording.ts @@ -1,36 +1,60 @@ import { - Clipboard, - closeMainWindow, - getPreferenceValues, - open, - showHUD, -} from "@raycast/api" + type StopRecordingPreferences, + buildStopRecordingDeepLink, + extractShareableCapUrl, +} from "./deeplink"; import { - StopRecordingPreferences, - buildStopRecordingDeepLink, - extractShareableCapUrl, -} from "./deeplink" + Clipboard, + closeMainWindow, + getPreferenceValues, + open, + showHUD, +} from "./raycast-bridge"; + +const STOP_CLIPBOARD_POLL_ATTEMPTS = 6; +const STOP_CLIPBOARD_POLL_DELAY_MS = 250; + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function waitForNewShareUrl( + previousClipboard: string | undefined, + dashboardUrl: string | undefined, +): Promise { + for (let attempt = 0; attempt < STOP_CLIPBOARD_POLL_ATTEMPTS; attempt++) { + await sleep(STOP_CLIPBOARD_POLL_DELAY_MS); + const currentText = await Clipboard.readText(); + if (!currentText || currentText === previousClipboard) { + continue; + } + const shareUrl = extractShareableCapUrl(currentText, dashboardUrl); + if (shareUrl) { + return shareUrl; + } + } + return null; +} -// Raycast command entry point to stop the current Cap recording and copy the share URL. export default async function Command(): Promise { - const prefs = getPreferenceValues() - const deepLinkUrl = buildStopRecordingDeepLink() + const prefs = await getPreferenceValues(); + const previousClipboard = await Clipboard.readText(); + const deepLinkUrl = buildStopRecordingDeepLink(); - await closeMainWindow() - await open(deepLinkUrl) + await closeMainWindow(); + await open(deepLinkUrl); - if (prefs.copyUrlOnStop ?? true) { - const existingText = await Clipboard.readText() - const shareUrl = extractShareableCapUrl( - existingText, - prefs.dashboardUrl - ) - if (shareUrl) { - await Clipboard.copy(shareUrl) - await showHUD(`Cap recording stopped — copied ${shareUrl}`) - return - } - } + if (prefs.copyUrlOnStop ?? true) { + const shareUrl = await waitForNewShareUrl( + previousClipboard, + prefs.dashboardUrl, + ); + if (shareUrl) { + await Clipboard.copy(shareUrl); + await showHUD(`Cap recording stopped — copied ${shareUrl}`); + return; + } + } - await showHUD("Cap recording stopped") + await showHUD("Cap recording stopped"); } diff --git a/apps/raycast-extension/tsconfig.json b/apps/raycast-extension/tsconfig.json index 5559e0740d3..2dea9e52973 100644 --- a/apps/raycast-extension/tsconfig.json +++ b/apps/raycast-extension/tsconfig.json @@ -1,21 +1,17 @@ { - "$schema": "https://json.schemastore.org/tsconfig", - "display": "Node 20", - "include": [ - "src/**/*" - ], - "compilerOptions": { - "lib": [ - "ES2022" - ], - "module": "commonjs", - "target": "ES2022", - "strict": true, - "isolatedModules": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "jsx": "react-jsx", - "resolveJsonModule": true - } -} \ No newline at end of file + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Node 20", + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["ES2022"], + "module": "commonjs", + "target": "ES2022", + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "resolveJsonModule": true + } +} diff --git a/bun.lock b/bun.lock index 45323cb08d5..804c0250cc1 100644 --- a/bun.lock +++ b/bun.lock @@ -91,6 +91,14 @@ "@types/bun": "latest", }, }, + "apps/raycast-extension": { + "name": "@cap/raycast-extension", + "devDependencies": { + "@types/node": "^20.11.5", + "typescript": "^5.8.3", + "vitest": "^3.2.0", + }, + }, "apps/render-farm": { "name": "@cap/render-farm", "devDependencies": { @@ -825,6 +833,8 @@ "@cap/media-server": ["@cap/media-server@workspace:apps/media-server"], + "@cap/raycast-extension": ["@cap/raycast-extension@workspace:apps/raycast-extension"], + "@cap/recorder-core": ["@cap/recorder-core@workspace:packages/recorder-core"], "@cap/render-farm": ["@cap/render-farm@workspace:apps/render-farm"], From a43ec451ee0f8756546b0c6c703536da30bad35d Mon Sep 17 00:00:00 2001 From: Filipe Ribeiro <45768814+frds3445@users.noreply.github.com> Date: Sat, 7 Nov 2026 23:04:54 -0300 Subject: [PATCH 3/5] fix(raycast): use workspace tsc scripts and resolve active display via AppKit --- apps/raycast-extension/package.json | 6 +-- apps/raycast-extension/src/deeplink.test.ts | 24 +++++++++++ apps/raycast-extension/src/deeplink.ts | 48 ++++++++++++++++++--- apps/raycast-extension/tsconfig.json | 3 +- 4 files changed, 72 insertions(+), 9 deletions(-) diff --git a/apps/raycast-extension/package.json b/apps/raycast-extension/package.json index cbf99ea7f3a..135bbb2af8e 100644 --- a/apps/raycast-extension/package.json +++ b/apps/raycast-extension/package.json @@ -103,9 +103,9 @@ "vitest": "^3.2.0" }, "scripts": { - "build": "ray build -e dist", - "dev": "ray develop", - "lint": "ray lint", + "build": "tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch", + "typecheck": "tsc -p tsconfig.json --noEmit", "test": "vitest run" } } diff --git a/apps/raycast-extension/src/deeplink.test.ts b/apps/raycast-extension/src/deeplink.test.ts index 1b92c9afea2..ec568087514 100644 --- a/apps/raycast-extension/src/deeplink.test.ts +++ b/apps/raycast-extension/src/deeplink.test.ts @@ -5,6 +5,7 @@ import { buildStartRecordingPayload, buildStopRecordingDeepLink, extractShareableCapUrl, + parseUnambiguousDisplayFromSystemProfiler, resolveDashboardUrl, } from "./deeplink"; @@ -44,6 +45,29 @@ describe("Cap Raycast deep-link builders", () => { expect(() => buildStartRecordingPayload({}, () => null)).toThrow( /No target screen name/, ); + + const singleDisplayJson = JSON.stringify({ + SPDisplaysDataType: [ + { spdisplays_ndrvs: [{ _name: "Built-in Retina Display" }] }, + ], + }); + expect(parseUnambiguousDisplayFromSystemProfiler(singleDisplayJson)).toBe( + "Built-in Retina Display", + ); + + const multiDisplayJson = JSON.stringify({ + SPDisplaysDataType: [ + { + spdisplays_ndrvs: [ + { _name: "Built-in Retina Display" }, + { _name: "Studio Display" }, + ], + }, + ], + }); + expect( + parseUnambiguousDisplayFromSystemProfiler(multiDisplayJson), + ).toBeNull(); }); it("encodes stop_recording and resolves dashboard and share URLs", () => { diff --git a/apps/raycast-extension/src/deeplink.ts b/apps/raycast-extension/src/deeplink.ts index 99acc207f57..23f8aa742f2 100644 --- a/apps/raycast-extension/src/deeplink.ts +++ b/apps/raycast-extension/src/deeplink.ts @@ -27,36 +27,74 @@ export interface DeepLinkStartPayload { const CAP_ACTION_BASE_URL = "cap-desktop://action"; const DEFAULT_DASHBOARD_URL = "https://cap.so/dashboard"; - -function parseFirstDisplayFromSystemProfiler(rawJson: string): string | null { +const ACTIVE_SCREEN_JXA_SCRIPT = [ + 'ObjC.import("AppKit");', + "const mouse = $.NSEvent.mouseLocation;", + "const screens = $.NSScreen.screens;", + "const count = screens.count;", + "let chosen = null;", + "for (let i = 0; i < count; i++) {", + " const s = screens.objectAtIndex(i);", + " const f = s.frame;", + " if (mouse.x >= f.origin.x && mouse.x <= f.origin.x + f.size.width && mouse.y >= f.origin.y && mouse.y <= f.origin.y + f.size.height) {", + " chosen = s;", + " break;", + " }", + "}", + "if (!chosen) { chosen = $.NSScreen.mainScreen; }", + 'chosen ? ObjC.unwrap(chosen.localizedName) : "";', +].join(" "); + +export function parseUnambiguousDisplayFromSystemProfiler( + rawJson: string, +): string | null { try { const parsed = JSON.parse(rawJson) as { SPDisplaysDataType?: Array<{ spdisplays_ndrvs?: Array<{ _name?: string }>; }>; }; + const names: string[] = []; for (const gpu of parsed.SPDisplaysDataType ?? []) { for (const display of gpu.spdisplays_ndrvs ?? []) { const name = display._name?.trim(); if (name) { - return name; + names.push(name); } } } + return names.length === 1 ? names[0] : null; + } catch { + return null; + } +} + +function queryActiveScreenViaAppKit(): string | null { + try { + const activeName = execFileSync( + "osascript", + ["-l", "JavaScript", "-e", ACTIVE_SCREEN_JXA_SCRIPT], + { encoding: "utf8", timeout: 2000 }, + ).trim(); + return activeName || null; } catch { return null; } - return null; } export function detectActiveMacDisplayName(): string | null { + const activeScreen = queryActiveScreenViaAppKit(); + if (activeScreen) { + return activeScreen; + } + try { const output = execFileSync( "system_profiler", ["SPDisplaysDataType", "-json"], { encoding: "utf8", timeout: 3000 }, ); - return parseFirstDisplayFromSystemProfiler(output); + return parseUnambiguousDisplayFromSystemProfiler(output); } catch { return null; } diff --git a/apps/raycast-extension/tsconfig.json b/apps/raycast-extension/tsconfig.json index 2dea9e52973..fddac517480 100644 --- a/apps/raycast-extension/tsconfig.json +++ b/apps/raycast-extension/tsconfig.json @@ -12,6 +12,7 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "jsx": "react-jsx", - "resolveJsonModule": true + "resolveJsonModule": true, + "outDir": "dist" } } From 53f149e73bdef6ae2263f643138dca8995d90232 Mon Sep 17 00:00:00 2001 From: Filipe Ribeiro <45768814+frds3445@users.noreply.github.com> Date: Sat, 7 Nov 2026 23:26:17 -0300 Subject: [PATCH 4/5] fix(raycast): restore @raycast/api dependency and lockfile entries --- apps/raycast-extension/package.json | 9 +++++++-- bun.lock | 9 +++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/raycast-extension/package.json b/apps/raycast-extension/package.json index 135bbb2af8e..42b7b2cf2c6 100644 --- a/apps/raycast-extension/package.json +++ b/apps/raycast-extension/package.json @@ -97,14 +97,19 @@ "mode": "no-view" } ], + "dependencies": { + "@raycast/api": "^1.83.1" + }, "devDependencies": { "@types/node": "^20.11.5", "typescript": "^5.8.3", "vitest": "^3.2.0" }, "scripts": { - "build": "tsc -p tsconfig.json", - "dev": "tsc -p tsconfig.json --watch", + "build": "ray build -e dist", + "dev": "ray develop", + "lint": "ray lint", + "fix-lint": "ray lint --fix", "typecheck": "tsc -p tsconfig.json --noEmit", "test": "vitest run" } diff --git a/bun.lock b/bun.lock index 804c0250cc1..f60aad87133 100644 --- a/bun.lock +++ b/bun.lock @@ -93,6 +93,9 @@ }, "apps/raycast-extension": { "name": "@cap/raycast-extension", + "dependencies": { + "@raycast/api": "^1.83.1", + }, "devDependencies": { "@types/node": "^20.11.5", "typescript": "^5.8.3", @@ -2117,6 +2120,8 @@ "@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="], + "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], + "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "20.19.43", "form-data": "4.0.6" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], "@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], @@ -4957,6 +4962,10 @@ "@radix-ui/react-use-escape-keydown/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "7.29.7" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="], + "@raycast/api/@types/react": ["@types/react@18.3.31", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw=="], + + "@raycast/api/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + "@react-email/components/@react-email/render": ["@react-email/render@1.1.3", "", { "dependencies": { "html-to-text": "9.0.5", "prettier": "3.9.4", "react-promise-suspense": "0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-TjjF1tdTmOqYEIWWg9wMx5q9JbQRbWmnG7owQbSGEHkNfc/c/vBu7hjfrki907lgQEAkYac9KPTyIjOKhvhJCg=="], "@react-email/components/@react-email/tailwind": ["@react-email/tailwind@1.0.5", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-BH00cZSeFfP9HiDASl+sPHi7Hh77W5nzDgdnxtsVr/m3uQD9g180UwxcE3PhOfx0vRdLzQUU8PtmvvDfbztKQg=="], From 26a1f00d357c53c7c9ae76f57165523e2b347a86 Mon Sep 17 00:00:00 2001 From: Filipe Ribeiro <45768814+frds3445@users.noreply.github.com> Date: Sat, 7 Nov 2026 23:41:20 -0300 Subject: [PATCH 5/5] chore(raycast): add @raycast/api root package entry to bun.lock --- bun.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bun.lock b/bun.lock index f60aad87133..803c18f75ed 100644 --- a/bun.lock +++ b/bun.lock @@ -1592,6 +1592,8 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="], + "@raycast/api": ["@raycast/api@1.83.1", "", { "dependencies": { "@types/node": "^20.8.10", "@types/react": "^18.3.3", "react": "18.3.1" }, "peerDependencies": { "@types/node": "20.8.10", "@types/react": "18.3.3", "react-devtools": "5.2.0" }, "optionalPeers": ["@types/node", "@types/react", "react-devtools"], "bin": { "ray": "bin/ray" } }, "sha512-24VFghPKVaNHqaxJyUzdpUiKW8qAkJ9/X/q/jq6tHhfzsKiWFQ0a3cjfNyUmDMmJAEsW4dGrxm7WoU6fox+YRg=="], + "@react-email/body": ["@react-email/body@0.0.11", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg=="], "@react-email/button": ["@react-email/button@0.1.0", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-fg4LtgTu5zXxaRSly9cuv6sHVF/hi1lElbRaIA8EPx5coWOBhCto6rCPfawcXpaN2oER7rNHUrcNBkI+lz5F9A=="],