diff --git a/extensions/snippetslab/package-lock.json b/extensions/snippetslab/package-lock.json index 88c3e373762ed..1e6cee60139c0 100644 --- a/extensions/snippetslab/package-lock.json +++ b/extensions/snippetslab/package-lock.json @@ -9,11 +9,14 @@ "dependencies": { "@raycast/api": "^1.79.1", "execa": "^9.3.0", - "p-queue": "^8.0.1" + "p-queue": "^8.0.1", + "plist": "^3.1.0", + "semver": "^7.6.3" }, "devDependencies": { "@raycast/eslint-config": "^1.0.8", "@types/node": "20.8.10", + "@types/plist": "^3.0.5", "@types/react": "18.3.3", "eslint": "^8.57.0", "prettier": "^3.2.5", @@ -286,6 +289,16 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", @@ -627,6 +640,14 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -709,6 +730,25 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -1809,6 +1849,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -1949,7 +2002,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -2185,6 +2237,14 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "engines": { + "node": ">=8.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/extensions/snippetslab/package.json b/extensions/snippetslab/package.json index 87b6eabb33d1e..368154e008f4d 100644 --- a/extensions/snippetslab/package.json +++ b/extensions/snippetslab/package.json @@ -67,10 +67,22 @@ "title": "Snippet Action: Primary", "description": "The first Snippet action shown in the action menu.", "data": [ - {"title": "Show Snippet Details", "value": "push"}, - {"title": "Copy to Clipboard", "value": "copy"}, - {"title": "Open in SnippetsLab", "value": "open"}, - {"title": "Paste to Active App", "value": "paste"} + { + "title": "Show Snippet Details", + "value": "push" + }, + { + "title": "Copy to Clipboard", + "value": "copy" + }, + { + "title": "Open in SnippetsLab", + "value": "open" + }, + { + "title": "Paste to Active App", + "value": "paste" + } ], "default": "push", "required": false @@ -81,10 +93,22 @@ "title": "Snippet Action: Secondary", "description": "The second Snippet action shown in the action menu.", "data": [ - {"title": "Show Snippet Details", "value": "push"}, - {"title": "Copy to Clipboard", "value": "copy"}, - {"title": "Open in SnippetsLab", "value": "open"}, - {"title": "Paste to Active App", "value": "paste"} + { + "title": "Show Snippet Details", + "value": "push" + }, + { + "title": "Copy to Clipboard", + "value": "copy" + }, + { + "title": "Open in SnippetsLab", + "value": "open" + }, + { + "title": "Paste to Active App", + "value": "paste" + } ], "default": "copy", "required": false @@ -101,11 +125,14 @@ "dependencies": { "@raycast/api": "^1.79.1", "execa": "^9.3.0", - "p-queue": "^8.0.1" + "p-queue": "^8.0.1", + "plist": "^3.1.0", + "semver": "^7.6.3" }, "devDependencies": { "@raycast/eslint-config": "^1.0.8", "@types/node": "20.8.10", + "@types/plist": "^3.0.5", "@types/react": "18.3.3", "eslint": "^8.57.0", "prettier": "^3.2.5", diff --git a/extensions/snippetslab/src/components/init_error.tsx b/extensions/snippetslab/src/components/init_error.tsx index 5d5a9496c0010..bcc9d1d9736ca 100644 --- a/extensions/snippetslab/src/components/init_error.tsx +++ b/extensions/snippetslab/src/components/init_error.tsx @@ -1,11 +1,21 @@ import { Action, ActionPanel, Detail, Icon } from "@raycast/api"; +import semver from "semver"; + +/** The minimum SnippetsLab app version. Must be valid semver. */ +const MIN_APP_VERSION = "2.5.0"; interface InitErrorProps { error: Error; + appVersion?: string; } /** Error screen for when the command utility cannot be found or used. */ -export function InitError({ error }: InitErrorProps) { +export function InitError({ error, appVersion }: InitErrorProps) { + const version = semver.coerce(appVersion); + const isUnsupportedAppVersion = version && semver.lt(version, MIN_APP_VERSION); + const unsupportedMessage = `It looks like you are using SnippetsLab ${appVersion}, but + version **${MIN_APP_VERSION} or later** is required.`; + const markdown = ` # An error occurred @@ -14,11 +24,13 @@ permissions. **Details** +${isUnsupportedAppVersion ? unsupportedMessage : ""} + ${error.message} **Troubleshooting Tips** -1. Ensure that **SnippetsLab 2.5 or later** is installed. +1. Ensure that **SnippetsLab ${MIN_APP_VERSION} or later** is installed. 2. The extension should typically locate the \`lab\` utility bundled with the app automatically. You can also manually specify the path in extension preferences. 3. If using a custom path, it must be an absolute path pointing directly to the \`lab\` binary. diff --git a/extensions/snippetslab/src/hooks/useSnippetsLab.ts b/extensions/snippetslab/src/hooks/useSnippetsLab.ts index f7d1cc1a96755..1ba286aab5623 100644 --- a/extensions/snippetslab/src/hooks/useSnippetsLab.ts +++ b/extensions/snippetslab/src/hooks/useSnippetsLab.ts @@ -3,12 +3,14 @@ import { execa } from "execa"; import fs from "fs"; import PQueue from "p-queue"; import path from "path"; +import plist from "plist"; import { useEffect, useState } from "react"; import { getPreferences } from "../models/preferences"; const BUNDLE_ID_APP_STORE = "com.renfei.SnippetsLab"; const BUNDLE_ID_SETAPP = "com.renfei.snippetslab-setapp"; const CLI_RELPATH = "Contents/Helpers/lab"; +const INFO_PLIST_RELPATH = "Contents/Info.plist"; /** * A serial queue for all operations using the command-line utility. @@ -43,6 +45,7 @@ export function useSnippetsLab() { const [isInitializing, setInitializing] = useState(true); const [initializationError, setInitializationError] = useState(); const [resolvedCliPath, setResolvedCliPath] = useState(); + const [appVersion, setAppVersion] = useState(); useEffect(() => { const checkCli = async () => { @@ -77,6 +80,11 @@ export function useSnippetsLab() { throw new Error("SnippetsLab.app not found. Is it installed?"); } + const infoPlistPath = path.join(app.path, INFO_PLIST_RELPATH); + const infoPlistContents = fs.readFileSync(infoPlistPath, "utf8"); + const infoDictionary = plist.parse(infoPlistContents) as Readonly; + setAppVersion(infoDictionary.CFBundleShortVersionString as string); + return path.join(app.path, CLI_RELPATH); }; @@ -133,6 +141,7 @@ export function useSnippetsLab() { return { isInitializing, initializationError, + appVersion, // undefined when using a custom CLI path in preferences. app: { run, open }, }; } diff --git a/extensions/snippetslab/src/index.tsx b/extensions/snippetslab/src/index.tsx index e68aa85019f74..a652e66d55dde 100644 --- a/extensions/snippetslab/src/index.tsx +++ b/extensions/snippetslab/src/index.tsx @@ -5,12 +5,12 @@ import { SnippetsSearch } from "./search"; /** Entry point for the Search in SnippetsLab command. */ export default function Command({ fallbackText }: LaunchProps) { - const { isInitializing, initializationError, app } = useSnippetsLab(); + const { isInitializing, initializationError, appVersion, app } = useSnippetsLab(); if (isInitializing) { return ; } else if (initializationError) { - return ; + return ; } else { return ; }