feature: add @kksh/script-command package

Implement script command parser, support existing raycast script commands
This commit is contained in:
Huakun Shen 2025-02-26 04:51:50 -05:00
parent 97cd20906f
commit 0fb76d3575
No known key found for this signature in database
19 changed files with 513 additions and 5 deletions

View File

@ -19,6 +19,7 @@
"@inlang/paraglide-sveltekit": "0.15.5",
"@kksh/extension": "workspace:*",
"@kksh/supabase": "workspace:*",
"@kksh/script-command": "workspace:*",
"@kksh/svelte5": "^0.1.15",
"@kksh/ui": "workspace:*",
"@kksh/utils": "workspace:*",

175
packages/script-command/.gitignore vendored Normal file
View File

@ -0,0 +1,175 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View File

@ -0,0 +1 @@
# script-command

View File

@ -0,0 +1,2 @@
export * from "./src/parser"
export * from "./src/models"

View File

@ -0,0 +1,14 @@
{
"name": "@kksh/script-command",
"module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"valibot": "1.0.0-rc.2"
}
}

View File

@ -0,0 +1,16 @@
#!/bin/bash
# Required parameters:
# @kunkun.schemaVersion 1
# @kunkun.title bash script cmd
# @kunkun.mode fullOutput
# Optional parameters:
# @kunkun.icon 🤖
# Documentation:
# @kunkun.author huakunshen
# @kunkun.authorURL https://raycast.com/huakunshen
echo "Hello World!"

View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
// Required parameters:
// @kunkun.schemaVersion 1
// @kunkun.title Test Script cmd
// @kunkun.mode compact
// Optional parameters:
// @kunkun.icon 🤖
// @kunkun.argument1 { "type": "text", "placeholder": "Placeholder" }
// @kunkun.packageName hahaha
// @kunkun.needsConfirmation true
// Documentation:
// @kunkun.description describe
// @kunkun.author huakunshen
// @kunkun.authorURL https://raycast.com/huakunshen
console.log("Hello World! Argument1 value: " + process.argv.slice(2)[0])

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
# Required parameters:
# @kunkun.schemaVersion 1
# @kunkun.title Test Py Script
# @kunkun.mode compact
# Optional parameters:
# @kunkun.icon 🤖
# @kunkun.packageName pypy
# Documentation:
# @kunkun.description py py py
# @kunkun.author huakunshen
# @kunkun.authorURL https://raycast.com/huakunshen
print("Hello World!")

View File

@ -0,0 +1,16 @@
#!/usr/bin/env ruby
# Required parameters:
# @kunkun.schemaVersion 1
# @kunkun.title ruby cmd
# @kunkun.mode fullOutput
# Optional parameters:
# @kunkun.icon 🤖
# Documentation:
# @kunkun.author huakunshen
# @kunkun.authorURL https://raycast.com/huakunshen
puts "Hello World!"

View File

@ -0,0 +1,16 @@
#!/bin/bash
# Required parameters:
# @raycast.schemaVersion 1
# @raycast.title bash script cmd
# @raycast.mode fullOutput
# Optional parameters:
# @raycast.icon 🤖
# Documentation:
# @raycast.author huakunshen
# @raycast.authorURL https://raycast.com/huakunshen
echo "Hello World!"

View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
// Required parameters:
// @raycast.schemaVersion 1
// @raycast.title Test Script cmd
// @raycast.mode compact
// Optional parameters:
// @raycast.icon 🤖
// @raycast.argument1 { "type": "text", "placeholder": "Placeholder" }
// @raycast.packageName hahaha
// @raycast.needsConfirmation true
// Documentation:
// @raycast.description describe
// @raycast.author huakunshen
// @raycast.authorURL https://raycast.com/huakunshen
console.log("Hello World! Argument1 value: " + process.argv.slice(2)[0])

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
# Required parameters:
# @raycast.schemaVersion 1
# @raycast.title Test Py Script
# @raycast.mode compact
# Optional parameters:
# @raycast.icon 🤖
# @raycast.packageName pypy
# Documentation:
# @raycast.description py py py
# @raycast.author huakunshen
# @raycast.authorURL https://raycast.com/huakunshen
print("Hello World!")

View File

@ -0,0 +1,16 @@
#!/usr/bin/env ruby
# Required parameters:
# @raycast.schemaVersion 1
# @raycast.title ruby cmd
# @raycast.mode fullOutput
# Optional parameters:
# @raycast.icon 🤖
# Documentation:
# @raycast.author huakunshen
# @raycast.authorURL https://raycast.com/huakunshen
puts "Hello World!"

View File

@ -0,0 +1,27 @@
import { describe, expect, test } from "bun:test"
import * as v from "valibot"
import { ScriptCommandConfig } from "../models"
import { parseScriptCommand } from "../parser"
describe("parseScriptCommand", () => {
test("should parse script command", async () => {
for (const filePath of [
"./samples/raycast/python.py",
"./samples/raycast/node.js",
"./samples/raycast/ruby.rb",
"./samples/raycast/bash.sh",
// kunkun
"./samples/kunkun/python.py",
"./samples/kunkun/node.js",
"./samples/kunkun/ruby.rb",
"./samples/kunkun/bash.sh"
]) {
const content = await Bun.file(filePath).text()
const scriptCommand = parseScriptCommand(
content,
filePath.split(".").pop() as "py" | "js" | "ts" | "swift" | "scpt"
)
expect(v.is(ScriptCommandConfig, scriptCommand)).toBe(true)
}
})
})

View File

@ -0,0 +1,22 @@
import * as v from "valibot"
export const ScriptCommandMode = v.union([
v.literal("compact"),
v.literal("fullOutput"),
v.literal("inline"),
v.literal("silent")
])
export const ScriptCommandConfig = v.object({
scriptPath: v.nullable(v.string()),
schemaVersion: v.optional(v.number(), 1),
title: v.string(),
mode: v.optional(ScriptCommandMode, "fullOutput"),
icon: v.optional(v.string()),
packageName: v.optional(v.string()),
needsConfirmation: v.optional(v.boolean(), false),
description: v.optional(v.string()),
author: v.optional(v.string()),
authorURL: v.optional(v.string())
})
export type ScriptCommandConfig = v.InferOutput<typeof ScriptCommandConfig>

View File

@ -0,0 +1,66 @@
import * as v from "valibot"
import { ScriptCommandConfig } from "./models"
/**
* Parse script command from comments of file content
* @param fileContent
* @param fileType
*/
export function parseScriptCommand(
fileContent: string,
fileType: "js" | "py" | "rb" | "sh" | "swift" | "ts" | "scpt"
): ScriptCommandConfig {
const config: any = { scriptPath: null }
// Split content into lines and process each line
const lines = fileContent.split("\n")
for (const line of lines) {
// Look for lines containing @raycast. or @kunkun. metadata
if (line.includes("@raycast.") || line.includes("@kunkun.")) {
const trimmedLine = line.trim()
// Remove any comment markers based on file type
const cleanedLine = trimmedLine
.replace(/^#\s*/, "") // Python/Ruby style comments
.replace(/^\/\/\s*/, "") // JavaScript/TypeScript style comments
.trim()
// Extract parameter name and value, supporting both @raycast and @kunkun
const match = cleanedLine.match(/@(raycast|kunkun)\.(\w+)\s+(.+)/)
if (match) {
const [, prefix, param, value] = match
switch (param) {
case "schemaVersion":
config.schemaVersion = parseInt(value, 10)
break
case "title":
config.title = value
break
case "mode":
config.mode = value as any
break
case "packageName":
config.packageName = value
break
case "icon":
config.icon = value
break
case "description":
config.description = value
break
case "author":
config.author = value
break
case "authorURL":
config.authorURL = value
break
case "needsConfirmation":
config.needsConfirmation = value.toLowerCase() === "true"
break
}
}
}
}
return v.parse(ScriptCommandConfig, config)
}

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

View File

@ -1,4 +1,3 @@
## Permission Table
<table>
@ -7,7 +6,6 @@
<th>Description</th>
</tr>
<tr>
<td>

41
pnpm-lock.yaml generated
View File

@ -335,7 +335,7 @@ importers:
version: 8.23.0(eslint@9.21.0(jiti@2.4.0))(typescript@5.6.3)
autoprefixer:
specifier: ^10.4.20
version: 10.4.20(postcss@8.4.49)
version: 10.4.20(postcss@8.5.1)
bits-ui:
specifier: 1.0.0-next.86
version: 1.0.0-next.86(svelte@5.16.6)
@ -769,6 +769,19 @@ importers:
specifier: 1.0.0-beta.4
version: 1.0.0-beta.4(valibot@1.0.0-beta.10(typescript@5.7.3))
packages/script-command:
dependencies:
typescript:
specifier: ^5.0.0
version: 5.7.3
valibot:
specifier: 1.0.0-rc.2
version: 1.0.0-rc.2(typescript@5.7.3)
devDependencies:
'@types/bun':
specifier: latest
version: 1.2.3
packages/supabase:
dependencies:
'@kksh/api':
@ -11386,6 +11399,14 @@ packages:
typescript:
optional: true
valibot@1.0.0-rc.2:
resolution: {integrity: sha512-Tnnp7dydpihvoUbJiaxuYfsCAgAFKuFMex7PTaI25XSjRWkU70DmJPlAO1W6sF1/WUx4RNWyM2hdmBSMIUSZFA==}
peerDependencies:
typescript: '>=5'
peerDependenciesMeta:
typescript:
optional: true
validate-npm-package-name@5.0.1:
resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@ -18156,6 +18177,16 @@ snapshots:
postcss: 8.4.49
postcss-value-parser: 4.2.0
autoprefixer@10.4.20(postcss@8.5.1):
dependencies:
browserslist: 4.24.2
caniuse-lite: 1.0.30001676
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.1.1
postcss: 8.5.1
postcss-value-parser: 4.2.0
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.0.0
@ -23678,14 +23709,14 @@ snapshots:
tauri-plugin-network-api@2.0.5(typescript@5.7.2):
dependencies:
'@tauri-apps/api': 2.2.0
valibot: 1.0.0-beta.11(typescript@5.7.2)
valibot: 1.0.0-beta.12(typescript@5.7.2)
transitivePeerDependencies:
- typescript
tauri-plugin-network-api@2.0.5(typescript@5.7.3):
dependencies:
'@tauri-apps/api': 2.2.0
valibot: 1.0.0-beta.11(typescript@5.7.3)
valibot: 1.0.0-beta.12(typescript@5.7.3)
transitivePeerDependencies:
- typescript
@ -24297,6 +24328,10 @@ snapshots:
optionalDependencies:
typescript: 5.6.3
valibot@1.0.0-rc.2(typescript@5.7.3):
optionalDependencies:
typescript: 5.7.3
validate-npm-package-name@5.0.1: {}
validator@13.12.0: