feat(skill): ✨ add HuLa skill linking script and project context documentation
This commit is contained in:
2
package.json
vendored
2
package.json
vendored
@@ -71,6 +71,8 @@
|
||||
"test:ui": "vitest --ui --coverage.enabled=true",
|
||||
"========= 测试覆盖率 =========": "",
|
||||
"coverage": "vitest run --coverage",
|
||||
"========= 链接HuLa skill =========": "",
|
||||
"skill": "node scripts/link-skills.js",
|
||||
"========= 单独提交 =========": "",
|
||||
"gitcz": "git-cz"
|
||||
},
|
||||
|
||||
314
scripts/link-skills.js
vendored
Normal file
314
scripts/link-skills.js
vendored
Normal file
@@ -0,0 +1,314 @@
|
||||
import { checkbox } from '@inquirer/prompts'
|
||||
import { openSync, promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { ReadStream, WriteStream } from 'node:tty'
|
||||
import { styleText } from 'node:util'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..')
|
||||
const skillsRoot = path.join(repoRoot, 'skills')
|
||||
|
||||
async function main() {
|
||||
const shouldSkip = process.env.HULA_SKIP_SKILL_LINK || process.env.CI
|
||||
const isLifecycle = Boolean(process.env.npm_lifecycle_event)
|
||||
const canPrompt = Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
||||
const forcePrompt = process.env.HULA_FORCE_SKILL_LINK === '1'
|
||||
|
||||
if (shouldSkip) {
|
||||
console.log('已跳过技能链接:检测到 HULA_SKIP_SKILL_LINK 或 CI。')
|
||||
return
|
||||
}
|
||||
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE
|
||||
if (!homeDir) {
|
||||
console.log('已跳过技能链接:未检测到 HOME 或 USERPROFILE。')
|
||||
return
|
||||
}
|
||||
|
||||
let skillDirs = []
|
||||
try {
|
||||
const entries = await fs.readdir(skillsRoot, { withFileTypes: true })
|
||||
skillDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name)
|
||||
} catch {
|
||||
console.log(`已跳过技能链接:无法读取 ${skillsRoot}。`)
|
||||
return
|
||||
}
|
||||
|
||||
if (skillDirs.length === 0) {
|
||||
console.log(`已跳过技能链接:${skillsRoot} 下未找到技能目录。`)
|
||||
return
|
||||
}
|
||||
|
||||
const codexHome = process.env.CODEX_HOME || path.join(homeDir, '.codex')
|
||||
const claudeHome = process.env.CLAUDE_HOME || path.join(homeDir, '.claude')
|
||||
|
||||
const targets = [
|
||||
{
|
||||
id: 'codex',
|
||||
name: 'Codex',
|
||||
dir: path.join(codexHome, 'skills')
|
||||
},
|
||||
{
|
||||
id: 'claude',
|
||||
name: 'Claude',
|
||||
dir: path.join(claudeHome, 'skills')
|
||||
}
|
||||
]
|
||||
|
||||
const targetsWithStatus = []
|
||||
for (const target of targets) {
|
||||
const status = await getTargetLinkStatus(target.dir, skillsRoot, skillDirs)
|
||||
targetsWithStatus.push({ ...target, status })
|
||||
}
|
||||
|
||||
if (targetsWithStatus.every((target) => target.status.allLinked)) {
|
||||
console.log('已跳过技能链接:目标目录已全部链接。')
|
||||
return
|
||||
}
|
||||
|
||||
if (isLifecycle && !forcePrompt) {
|
||||
const pending = targetsWithStatus.filter((target) => !target.status.allLinked)
|
||||
const pendingNames = pending.map((target) => target.name).join(', ')
|
||||
console.log(`技能链接待处理:${pendingNames}。`)
|
||||
console.log('请运行 `node scripts/link-skills.js` 进行链接。')
|
||||
return
|
||||
}
|
||||
|
||||
if (!canPrompt && !forcePrompt) {
|
||||
console.log('已跳过技能链接:当前为非交互终端。')
|
||||
console.log('请在终端运行 `node scripts/link-skills.js` 进行链接。')
|
||||
return
|
||||
}
|
||||
|
||||
let promptContext = null
|
||||
let cleanupPromptContext = () => {}
|
||||
|
||||
if (!canPrompt) {
|
||||
const fallback = createConsolePromptContext()
|
||||
if (!fallback) {
|
||||
console.log('已跳过技能链接:无法打开控制台用于交互。')
|
||||
return
|
||||
}
|
||||
promptContext = fallback.context
|
||||
cleanupPromptContext = fallback.cleanup
|
||||
}
|
||||
|
||||
try {
|
||||
const useChineseHelp = isChineseLocale()
|
||||
const allTargetsValue = '__all__'
|
||||
const keyLabelMap = useChineseHelp
|
||||
? new Map([
|
||||
['space', '空格'],
|
||||
['⏎', '回车']
|
||||
])
|
||||
: null
|
||||
const actionLabelMap = useChineseHelp
|
||||
? new Map([
|
||||
['navigate', '移动'],
|
||||
['select', '选择'],
|
||||
['all', '全选'],
|
||||
['invert', '反选'],
|
||||
['submit', '提交']
|
||||
])
|
||||
: null
|
||||
const selectedTargets = await checkbox(
|
||||
{
|
||||
message: '请选择要链接的目标:',
|
||||
required: true,
|
||||
theme: {
|
||||
icon: {
|
||||
checked: '✓',
|
||||
unchecked: '○',
|
||||
cursor: '❯'
|
||||
},
|
||||
style: {
|
||||
keysHelpTip: (keys) =>
|
||||
keys
|
||||
.map(([key, action]) => {
|
||||
const keyLabel = keyLabelMap?.get(key) ?? key
|
||||
const actionLabel = actionLabelMap?.get(action) ?? action
|
||||
return `${styleText('bold', keyLabel)} ${styleText('dim', actionLabel)}`
|
||||
})
|
||||
.join(styleText('dim', ' · '))
|
||||
}
|
||||
},
|
||||
choices: [
|
||||
{
|
||||
name: '链接全部未链接目标',
|
||||
value: allTargetsValue,
|
||||
checked: false
|
||||
},
|
||||
...targetsWithStatus.map((target) => ({
|
||||
name: `${target.name} (${target.dir})`,
|
||||
value: target.id,
|
||||
checked: false,
|
||||
disabled: target.status.allLinked ? '已链接' : false
|
||||
}))
|
||||
]
|
||||
},
|
||||
promptContext ?? undefined
|
||||
)
|
||||
|
||||
const shouldLinkAll = selectedTargets.includes(allTargetsValue)
|
||||
const selectedIds = shouldLinkAll
|
||||
? targetsWithStatus.filter((target) => !target.status.allLinked).map((target) => target.id)
|
||||
: selectedTargets.filter((value) => value !== allTargetsValue)
|
||||
|
||||
if (selectedIds.length === 0) {
|
||||
console.log('已跳过技能链接:未选择目标。')
|
||||
return
|
||||
}
|
||||
|
||||
for (const target of targetsWithStatus) {
|
||||
if (!selectedIds.includes(target.id)) {
|
||||
continue
|
||||
}
|
||||
await linkSkillsToTarget(target.dir, skillsRoot, skillDirs)
|
||||
}
|
||||
} finally {
|
||||
cleanupPromptContext()
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.log('技能链接失败。')
|
||||
if (error instanceof Error) {
|
||||
console.log(error.message)
|
||||
}
|
||||
})
|
||||
|
||||
function isChineseLocale() {
|
||||
const candidates = [
|
||||
process.env.LC_ALL,
|
||||
process.env.LC_MESSAGES,
|
||||
process.env.LANG,
|
||||
Intl.DateTimeFormat().resolvedOptions().locale
|
||||
].filter(Boolean)
|
||||
|
||||
const locale = candidates.join(' ').toLowerCase()
|
||||
return locale.includes('zh')
|
||||
}
|
||||
|
||||
function createConsolePromptContext() {
|
||||
const isWindows = process.platform === 'win32'
|
||||
const candidates = isWindows
|
||||
? [
|
||||
{ input: '\\\\.\\CONIN$', output: '\\\\.\\CONOUT$' },
|
||||
{ input: 'CONIN$', output: 'CONOUT$' }
|
||||
]
|
||||
: [{ input: '/dev/tty', output: '/dev/tty' }]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const inputFd = openSync(candidate.input, 'r')
|
||||
const outputFd = openSync(candidate.output, 'w')
|
||||
const input = new ReadStream(inputFd)
|
||||
const output = new WriteStream(outputFd)
|
||||
return {
|
||||
context: { input, output },
|
||||
cleanup: () => {
|
||||
input.destroy()
|
||||
output.destroy()
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function getTargetLinkStatus(targetDir, sourceRoot, skillNames) {
|
||||
let linkedCount = 0
|
||||
|
||||
for (const skillName of skillNames) {
|
||||
const source = path.join(sourceRoot, skillName)
|
||||
const dest = path.join(targetDir, skillName)
|
||||
if (await isSkillLinked(source, dest)) {
|
||||
linkedCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allLinked: linkedCount === skillNames.length
|
||||
}
|
||||
}
|
||||
|
||||
async function isSkillLinked(source, dest) {
|
||||
let destStat = null
|
||||
try {
|
||||
destStat = await fs.lstat(dest)
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.log(`已跳过技能链接:无法检查 ${dest}。`)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (!destStat.isSymbolicLink()) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const [destResolved, sourceResolved] = await Promise.all([fs.realpath(dest), fs.realpath(source)])
|
||||
return destResolved === sourceResolved
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function linkSkillsToTarget(targetDir, sourceRoot, skillNames) {
|
||||
await fs.mkdir(targetDir, { recursive: true })
|
||||
const sourceRootResolved = await fs.realpath(sourceRoot)
|
||||
|
||||
for (const skillName of skillNames) {
|
||||
const source = path.join(sourceRootResolved, skillName)
|
||||
const dest = path.join(targetDir, skillName)
|
||||
await ensureLink(source, dest)
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureLink(source, dest) {
|
||||
const sourceResolved = await fs.realpath(source)
|
||||
let destStat = null
|
||||
|
||||
try {
|
||||
destStat = await fs.lstat(dest)
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.log(`已跳过技能链接:无法检查 ${dest}。`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (destStat) {
|
||||
if (destStat.isSymbolicLink()) {
|
||||
try {
|
||||
const destResolved = await fs.realpath(dest)
|
||||
if (destResolved === sourceResolved) {
|
||||
console.log(`技能链接已存在:${dest}`)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
console.log(`已跳过技能链接:${dest} 处的链接无效。`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`已跳过技能链接:${dest} 已存在。`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
await fs.symlink(sourceResolved, dest, 'junction')
|
||||
} else {
|
||||
await fs.symlink(sourceResolved, dest)
|
||||
}
|
||||
console.log(`已创建技能链接:${dest} -> ${sourceResolved}`)
|
||||
} catch {
|
||||
console.log(`技能链接失败:${dest}`)
|
||||
}
|
||||
}
|
||||
40
skills/hula-skill/SKILL.md
Normal file
40
skills/hula-skill/SKILL.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: hula-skill
|
||||
description: "HuLa project skill for frontend (Vue 3 + Vite + UnoCSS + Naive UI/Vant), backend (Tauri v2 + Rust + SeaORM/SQLite), full-stack flows, and build/release work. Use when the user mentions hula or HuLa or requests changes in this repository; after triggering, ask which scope (frontend/backend/fullstack/build-release) to enable."
|
||||
---
|
||||
|
||||
# HuLa Skill
|
||||
|
||||
## Overview
|
||||
|
||||
Enable consistent changes across the HuLa frontend, backend, full-stack flows, and build/release tasks with repo-specific conventions and resources.
|
||||
|
||||
## Activation Gate
|
||||
|
||||
Ask which scope to enable: frontend, backend, fullstack, or build-release.
|
||||
Confirm platform (desktop or mobile), target area (view/component/store/command), and any constraints before editing.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Identify scope and platform.
|
||||
2. Locate similar code paths and follow existing patterns.
|
||||
3. Apply changes using repo conventions and available templates.
|
||||
4. Update related layers (routes, stores, commands) when needed.
|
||||
5. Propose or run checks/tests only when requested.
|
||||
|
||||
## Scope Routing
|
||||
|
||||
- Frontend: read `references/frontend.md` and `references/overview.md`; use `assets/templates/view-desktop.vue`, `assets/templates/view-mobile.vue`, `assets/templates/pinia-store.ts` as starters.
|
||||
- Backend: read `references/backend.md` and `references/overview.md`; use `assets/templates/tauri-command.rs`.
|
||||
- Fullstack: read `references/fullstack.md` plus frontend/backend references; use `assets/templates/tauri-command.ts`.
|
||||
- Build/Release: read `references/build-release.md` and `references/checklists.md`.
|
||||
|
||||
## Scripts
|
||||
|
||||
Use `scripts/hula_summary.py` for quick repo context (views/stores counts and paths).
|
||||
Use `scripts/hula_tauri_map.py` to list Tauri commands and frontend invoke usage.
|
||||
|
||||
## References
|
||||
|
||||
Use `references/overview.md` for stack, directories, aliases, and global conventions.
|
||||
Use `references/checklists.md` for per-scope checklists.
|
||||
13
skills/hula-skill/assets/templates/pinia-store.ts
Normal file
13
skills/hula-skill/assets/templates/pinia-store.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { StoresEnum } from '@/enums'
|
||||
|
||||
// Add the enum entry in src/enums/index.ts before using this store.
|
||||
export const useExampleStore = defineStore(StoresEnum.EXAMPLE, () => {
|
||||
const items = ref<string[]>([])
|
||||
|
||||
const addItem = (value: string) => {
|
||||
items.value.push(value)
|
||||
}
|
||||
|
||||
return { items, addItem }
|
||||
})
|
||||
4
skills/hula-skill/assets/templates/tauri-command.rs
Normal file
4
skills/hula-skill/assets/templates/tauri-command.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
#[tauri::command]
|
||||
pub async fn example_command(payload: String) -> Result<String, String> {
|
||||
Ok(payload)
|
||||
}
|
||||
7
skills/hula-skill/assets/templates/tauri-command.ts
Normal file
7
skills/hula-skill/assets/templates/tauri-command.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { TauriCommand } from '@/enums'
|
||||
import { invokeWithErrorHandler } from '@/utils/TauriInvokeHandler'
|
||||
|
||||
// Add the enum entry in src/enums/index.ts before using this command.
|
||||
export const exampleCommand = async (payload: string): Promise<string> => {
|
||||
return await invokeWithErrorHandler<string>(TauriCommand.EXAMPLE_COMMAND, { payload })
|
||||
}
|
||||
15
skills/hula-skill/assets/templates/view-desktop.vue
Normal file
15
skills/hula-skill/assets/templates/view-desktop.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<section class="size-full p-16px">
|
||||
<n-flex vertical :size="12">
|
||||
<h1 class="text-18px font-600">{{ title }}</h1>
|
||||
<div class="text-14px text-[--text-color]">
|
||||
{{ description }}
|
||||
</div>
|
||||
</n-flex>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const title = 'Title'
|
||||
const description = 'Description'
|
||||
</script>
|
||||
13
skills/hula-skill/assets/templates/view-mobile.vue
Normal file
13
skills/hula-skill/assets/templates/view-mobile.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<section class="min-h-100vh p-16px">
|
||||
<div class="text-18px font-600">{{ title }}</div>
|
||||
<div class="text-14px text-[--text-color] mt-8px">
|
||||
{{ description }}
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const title = 'Title'
|
||||
const description = 'Description'
|
||||
</script>
|
||||
24
skills/hula-skill/references/backend.md
Normal file
24
skills/hula-skill/references/backend.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Backend Guide (Tauri + Rust)
|
||||
|
||||
## Command Locations
|
||||
|
||||
- Shared commands: `src-tauri/src/command/`
|
||||
- Desktop-specific: `src-tauri/src/desktops/`
|
||||
- Mobile-specific: `src-tauri/src/mobiles/`
|
||||
|
||||
## Add a New Command
|
||||
|
||||
1. Define a `#[tauri::command]` function in an existing module or a new module.
|
||||
2. If you add a new module, export it from `src-tauri/src/command/mod.rs`.
|
||||
3. Register the command in `tauri::generate_handler![...]` inside `src-tauri/src/lib.rs`.
|
||||
4. Keep signatures async when IO or DB is involved.
|
||||
|
||||
## Database
|
||||
|
||||
- Use SeaORM entities in `src-tauri/entity/`.
|
||||
- Add schema changes via migrations in `src-tauri/migration/`.
|
||||
|
||||
## Shared State
|
||||
|
||||
- Access shared state via `tauri::State<'_, AppData>`.
|
||||
- Keep long operations async to avoid blocking the runtime.
|
||||
17
skills/hula-skill/references/build-release.md
Normal file
17
skills/hula-skill/references/build-release.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Build and Release Guide
|
||||
|
||||
## Common Commands
|
||||
|
||||
- Install deps: `pnpm install`
|
||||
- Desktop dev: `pnpm tauri:dev`
|
||||
- Desktop build: `pnpm tauri:build`
|
||||
- Lint/format check: `pnpm check`
|
||||
- Auto-fix: `pnpm check:write`
|
||||
- Vue formatting: `pnpm format:vue` or `pnpm format:all`
|
||||
- Tests: `pnpm test:run`
|
||||
- Commit helper: `pnpm commit`
|
||||
|
||||
## Notes
|
||||
|
||||
- Use the registry configured in `.npmrc`. Override locally only if needed.
|
||||
- Avoid committing secrets; use `.env.local` for personal tokens.
|
||||
27
skills/hula-skill/references/checklists.md
Normal file
27
skills/hula-skill/references/checklists.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Checklists
|
||||
|
||||
## Frontend Changes
|
||||
|
||||
- Confirm platform (desktop/mobile) and target window/view.
|
||||
- Reuse existing view/component patterns from nearby files.
|
||||
- Update routes in `src/router/index.ts` when adding a new view.
|
||||
- Add i18n keys under `locales/` when new user-facing strings appear.
|
||||
- Use UnoCSS utilities and shared tokens from `src/styles/scss/global/variable.scss`.
|
||||
|
||||
## Backend Changes
|
||||
|
||||
- Add `#[tauri::command]` and register it in `src-tauri/src/lib.rs`.
|
||||
- Export new command modules in `src-tauri/src/command/mod.rs` if added.
|
||||
- Update SeaORM entities and migrations when schema changes.
|
||||
|
||||
## Fullstack Changes
|
||||
|
||||
- Add a `TauriCommand` enum entry if the frontend uses a named constant.
|
||||
- Add or update a frontend wrapper in `src/services/tauriCommand.ts`.
|
||||
- Use `invokeWithErrorHandler` for consistent error handling where needed.
|
||||
- Validate payload and response types end-to-end.
|
||||
|
||||
## Build/Release Work
|
||||
|
||||
- Use the existing `pnpm` scripts for checks and builds.
|
||||
- Avoid touching `.rules` unless explicitly asked.
|
||||
43
skills/hula-skill/references/frontend.md
Normal file
43
skills/hula-skill/references/frontend.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Frontend Guide
|
||||
|
||||
## Default Stack
|
||||
|
||||
- Vue 3 Composition API with `<script setup>`
|
||||
- Vite 7 + TypeScript
|
||||
- Pinia for global state
|
||||
- UnoCSS for utility styling
|
||||
- Naive UI on desktop, Vant on mobile
|
||||
- vue-i18n for translations
|
||||
|
||||
## File Placement
|
||||
|
||||
- Desktop views: `src/views/`
|
||||
- Mobile views: `src/mobile/views/`
|
||||
- Shared components: `src/components/`
|
||||
- Mobile components: `src/mobile/components/`
|
||||
- Layouts: `src/layout/` and `src/mobile/layout/`
|
||||
- Routes: `src/router/index.ts`
|
||||
- Stores: `src/stores/`
|
||||
- Services/hooks/utils: `src/services/`, `src/hooks/`, `src/utils/`
|
||||
|
||||
## Pinia Patterns
|
||||
|
||||
- Use setup-style stores: `defineStore(StoresEnum.X, () => { ... })`.
|
||||
- Keep imperative logic inside store actions.
|
||||
- Use `storeToRefs` when destructuring state in components.
|
||||
- Enable persistence per-store with `persist: true` only when needed.
|
||||
|
||||
## Styling
|
||||
|
||||
- Prefer UnoCSS utilities for simple styling.
|
||||
- Use `src/styles/scss/global/variable.scss` for shared tokens.
|
||||
- Consume tokens via `bg-[--token]`, `text-[--token]`, `border-[--token]`.
|
||||
|
||||
## Routing and Views
|
||||
|
||||
- Add new routes in `src/router/index.ts`.
|
||||
- Use `@/` for `src/` imports and `#/` for `src/mobile/`.
|
||||
|
||||
## i18n
|
||||
|
||||
- Add new strings under `locales/` and reference via `t(...)`.
|
||||
16
skills/hula-skill/references/fullstack.md
Normal file
16
skills/hula-skill/references/fullstack.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Fullstack Flow Guide
|
||||
|
||||
## Add or Update a Tauri Command End-to-End
|
||||
|
||||
1. Implement a `#[tauri::command]` in Rust and register it in `src-tauri/src/lib.rs`.
|
||||
2. Add or update the enum entry in `src/enums/index.ts` (`TauriCommand`) when a named constant is preferred.
|
||||
3. Add a typed wrapper in `src/services/tauriCommand.ts` or call `invoke` directly in a hook/service.
|
||||
4. Use `invokeWithErrorHandler` in `src/utils/TauriInvokeHandler.ts` when you need standardized error handling.
|
||||
5. Update any related types in `src/services/types` or local module types.
|
||||
|
||||
## Typical Touch Points
|
||||
|
||||
- Rust: `src-tauri/src/command/`, `src-tauri/src/lib.rs`
|
||||
- Frontend enums: `src/enums/index.ts`
|
||||
- Frontend wrappers: `src/services/tauriCommand.ts`
|
||||
- Invoke helpers: `src/utils/TauriInvokeHandler.ts`
|
||||
45
skills/hula-skill/references/overview.md
Normal file
45
skills/hula-skill/references/overview.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# HuLa Overview
|
||||
|
||||
## Stack
|
||||
|
||||
- Tauri v2 + Rust
|
||||
- Vue 3 (Composition API) + Vite 7 + TypeScript
|
||||
- Pinia + pinia-plugin-persistedstate + pinia-shared-state
|
||||
- UnoCSS + Sass
|
||||
- Naive UI (desktop), Vant (mobile)
|
||||
- SeaORM + SQLite (SQLCipher)
|
||||
- vue-i18n
|
||||
|
||||
## Repo Layout
|
||||
|
||||
- `src/` frontend source
|
||||
- `views/`, `components/`, `layout/`, `services/`, `stores/`, `hooks/`, `router/`, `utils/`
|
||||
- `mobile/` for mobile views/components/layout
|
||||
- `src-tauri/` Rust backend
|
||||
- `src/` application logic, commands, desktop/mobile modules
|
||||
- `entity/` SeaORM entities
|
||||
- `migration/` SeaORM migrations
|
||||
- `tauri-plugin-hula/` local Tauri plugin
|
||||
|
||||
## Aliases
|
||||
|
||||
- `@/` -> `src/`
|
||||
- `#/` -> `src/mobile/`
|
||||
- `~/` -> repo root
|
||||
|
||||
## Conventions
|
||||
|
||||
- Use 2-space indent and LF line endings.
|
||||
- Prefer `<script setup>` and the Composition API.
|
||||
- Prefer UnoCSS utilities; use `src/styles/scss/global/variable.scss` for shared tokens.
|
||||
- Use `storeToRefs` when destructuring Pinia state.
|
||||
- Do not change `.rules` unless asked.
|
||||
- Do not add secrets to tracked files; use `.env.local`.
|
||||
|
||||
## Common Files
|
||||
|
||||
- Router: `src/router/index.ts`
|
||||
- Tauri command enum: `src/enums/index.ts` (`TauriCommand`)
|
||||
- Frontend command wrappers: `src/services/tauriCommand.ts`
|
||||
- Tauri invoke helpers: `src/utils/TauriInvokeHandler.ts`
|
||||
- Theme tokens: `src/styles/scss/global/variable.scss`
|
||||
73
skills/hula-skill/scripts/hula_summary.py
vendored
Normal file
73
skills/hula-skill/scripts/hula_summary.py
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_repo_root(start: Path) -> Path | None:
|
||||
for path in [start] + list(start.parents):
|
||||
if (path / "package.json").exists() and (path / "src").exists():
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def list_views(root: Path) -> list[Path]:
|
||||
view_paths = []
|
||||
desktop = root / "src" / "views"
|
||||
mobile = root / "src" / "mobile" / "views"
|
||||
if desktop.exists():
|
||||
view_paths.extend(desktop.rglob("*.vue"))
|
||||
if mobile.exists():
|
||||
view_paths.extend(mobile.rglob("*.vue"))
|
||||
return sorted(view_paths, key=lambda path: str(path))
|
||||
|
||||
|
||||
def list_stores(root: Path) -> list[Path]:
|
||||
stores_dir = root / "src" / "stores"
|
||||
if not stores_dir.exists():
|
||||
return []
|
||||
return sorted(stores_dir.glob("*.ts"), key=lambda path: str(path))
|
||||
|
||||
|
||||
def print_paths(paths: list[Path], root: Path) -> None:
|
||||
for path in paths:
|
||||
print(path.relative_to(root))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Summarize HuLa frontend layout.")
|
||||
parser.add_argument("--root", help="Repo root (defaults to auto-detect).")
|
||||
parser.add_argument("--views", action="store_true", help="List view files.")
|
||||
parser.add_argument("--stores", action="store_true", help="List store files.")
|
||||
parser.add_argument("--summary", action="store_true", help="Print counts summary.")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.root).resolve() if args.root else find_repo_root(Path(__file__).resolve())
|
||||
if not root:
|
||||
print("ERROR: Could not locate repo root (missing package.json).", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if not (args.views or args.stores or args.summary):
|
||||
args.summary = True
|
||||
|
||||
views = list_views(root)
|
||||
stores = list_stores(root)
|
||||
|
||||
if args.summary:
|
||||
print(f"Repo: {root}")
|
||||
print(f"Views: {len(views)}")
|
||||
print(f"Stores: {len(stores)}")
|
||||
|
||||
if args.views:
|
||||
print("View files:")
|
||||
print_paths(views, root)
|
||||
|
||||
if args.stores:
|
||||
print("Store files:")
|
||||
print_paths(stores, root)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
103
skills/hula-skill/scripts/hula_tauri_map.py
vendored
Normal file
103
skills/hula-skill/scripts/hula_tauri_map.py
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
FN_RE = re.compile(r"\bfn\s+([A-Za-z0-9_]+)\b")
|
||||
INVOKE_STRING_RE = re.compile(r"\b(?:invoke|invokeWithErrorHandler)\s*\(\s*(['\"])([^'\"]+)\1")
|
||||
INVOKE_ENUM_RE = re.compile(r"\b(?:invoke|invokeWithErrorHandler)\s*\(\s*TauriCommand\.([A-Za-z0-9_]+)")
|
||||
|
||||
|
||||
def find_repo_root(start: Path) -> Path | None:
|
||||
for path in [start] + list(start.parents):
|
||||
if (path / "package.json").exists() and (path / "src").exists():
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def scan_rust_commands(root: Path) -> list[tuple[str, Path, int]]:
|
||||
src_dir = root / "src-tauri" / "src"
|
||||
if not src_dir.exists():
|
||||
return []
|
||||
results: list[tuple[str, Path, int]] = []
|
||||
for rust_file in src_dir.rglob("*.rs"):
|
||||
lines = rust_file.read_text(encoding="utf-8", errors="ignore").splitlines()
|
||||
idx = 0
|
||||
while idx < len(lines):
|
||||
if "#[tauri::command]" in lines[idx]:
|
||||
seek = idx + 1
|
||||
while seek < len(lines):
|
||||
match = FN_RE.search(lines[seek])
|
||||
if match:
|
||||
results.append((match.group(1), rust_file, seek + 1))
|
||||
break
|
||||
seek += 1
|
||||
idx = seek
|
||||
idx += 1
|
||||
return results
|
||||
|
||||
|
||||
def scan_invoke_usage(root: Path) -> tuple[list[tuple[str, Path, int]], list[tuple[str, Path, int]]]:
|
||||
src_dir = root / "src"
|
||||
if not src_dir.exists():
|
||||
return ([], [])
|
||||
string_hits: list[tuple[str, Path, int]] = []
|
||||
enum_hits: list[tuple[str, Path, int]] = []
|
||||
for src_file in src_dir.rglob("*"):
|
||||
if not src_file.is_file():
|
||||
continue
|
||||
if src_file.suffix not in {".ts", ".vue"}:
|
||||
continue
|
||||
lines = src_file.read_text(encoding="utf-8", errors="ignore").splitlines()
|
||||
for line_idx, line in enumerate(lines, start=1):
|
||||
for match in INVOKE_STRING_RE.finditer(line):
|
||||
string_hits.append((match.group(2), src_file, line_idx))
|
||||
for match in INVOKE_ENUM_RE.finditer(line):
|
||||
enum_hits.append((match.group(1), src_file, line_idx))
|
||||
return (string_hits, enum_hits)
|
||||
|
||||
|
||||
def print_hits(title: str, hits: list[tuple[str, Path, int]], root: Path, detail: bool) -> None:
|
||||
unique = sorted({name for name, _, _ in hits})
|
||||
print(f"{title}: {len(unique)}")
|
||||
if detail:
|
||||
for name, path, line in hits:
|
||||
print(f"{name}\t{path.relative_to(root)}:{line}")
|
||||
else:
|
||||
for name in unique:
|
||||
print(name)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="List Tauri commands and frontend invoke usage.")
|
||||
parser.add_argument("--root", help="Repo root (defaults to auto-detect).")
|
||||
parser.add_argument("--rust", action="store_true", help="List Rust #[tauri::command] functions.")
|
||||
parser.add_argument("--invoke", action="store_true", help="List frontend invoke usage.")
|
||||
parser.add_argument("--detail", action="store_true", help="Include file and line details.")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.root).resolve() if args.root else find_repo_root(Path(__file__).resolve())
|
||||
if not root:
|
||||
print("ERROR: Could not locate repo root (missing package.json).", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if not (args.rust or args.invoke):
|
||||
args.rust = True
|
||||
args.invoke = True
|
||||
|
||||
if args.rust:
|
||||
rust_hits = scan_rust_commands(root)
|
||||
print_hits("Rust commands", rust_hits, root, args.detail)
|
||||
|
||||
if args.invoke:
|
||||
string_hits, enum_hits = scan_invoke_usage(root)
|
||||
print_hits("Invoke string commands", string_hits, root, args.detail)
|
||||
print_hits("Invoke enum commands", enum_hits, root, args.detail)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user