feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
+116
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import process from 'node:process'
|
||||
import { defaultPromptsRoot, listPromptNames, readPrompt } from './prompts-lib.mjs'
|
||||
import {
|
||||
listSkillNames,
|
||||
parseInstallArgs,
|
||||
printInstallHelp,
|
||||
printInstallSummary,
|
||||
runSkillsInstall,
|
||||
} from './skills-lib.mjs'
|
||||
|
||||
function printCliHelp() {
|
||||
console.log(`Usage: markstream-vue <command>
|
||||
|
||||
Commands:
|
||||
skills list List bundled skills
|
||||
skills install [options] Install bundled skills
|
||||
prompts list List bundled prompt templates
|
||||
prompts dir Print bundled prompt directory
|
||||
prompts show <name> Print a bundled prompt template
|
||||
|
||||
Examples:
|
||||
markstream-vue skills list
|
||||
markstream-vue skills install
|
||||
markstream-vue skills install --target codex
|
||||
markstream-vue skills install --mode copy --force
|
||||
markstream-vue prompts list
|
||||
markstream-vue prompts show install-markstream
|
||||
`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const argv = process.argv.slice(2)
|
||||
const [namespace, action, ...rest] = argv
|
||||
|
||||
if (!namespace || namespace === '--help' || namespace === '-h' || namespace === 'help') {
|
||||
printCliHelp()
|
||||
return
|
||||
}
|
||||
|
||||
if (namespace === 'prompts') {
|
||||
if (!action || action === 'help') {
|
||||
console.log(`Usage: markstream-vue prompts <list|dir|show> [name]
|
||||
|
||||
Examples:
|
||||
markstream-vue prompts list
|
||||
markstream-vue prompts dir
|
||||
markstream-vue prompts show install-markstream
|
||||
`)
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'list') {
|
||||
const promptNames = await listPromptNames()
|
||||
console.log(`Bundled prompts (${promptNames.length}):`)
|
||||
for (const promptName of promptNames)
|
||||
console.log(`- ${promptName}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'dir') {
|
||||
console.log(defaultPromptsRoot)
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'show') {
|
||||
const [promptName] = rest
|
||||
const { content } = await readPrompt(promptName)
|
||||
process.stdout.write(content)
|
||||
if (!content.endsWith('\n'))
|
||||
process.stdout.write('\n')
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(`Unknown prompts action: ${action}`)
|
||||
}
|
||||
|
||||
if (namespace !== 'skills') {
|
||||
throw new Error(`Unknown command namespace: ${namespace}`)
|
||||
}
|
||||
|
||||
if (!action || action === 'help') {
|
||||
printInstallHelp('markstream-vue skills install', 'copy')
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'list') {
|
||||
const skillNames = await listSkillNames()
|
||||
console.log(`Bundled skills (${skillNames.length}):`)
|
||||
for (const skillName of skillNames)
|
||||
console.log(`- ${skillName}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'install') {
|
||||
const args = parseInstallArgs(rest, {
|
||||
target: 'agents',
|
||||
mode: 'copy',
|
||||
})
|
||||
if (args.help) {
|
||||
printInstallHelp('markstream-vue skills install', 'copy')
|
||||
return
|
||||
}
|
||||
const result = await runSkillsInstall(args)
|
||||
printInstallSummary(result)
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(`Unknown skills action: ${action}`)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
export const packageRoot = path.resolve(__dirname, '..')
|
||||
export const defaultPromptsRoot = path.join(packageRoot, 'prompts')
|
||||
|
||||
export async function listPromptFiles(promptsRoot = defaultPromptsRoot) {
|
||||
const dirents = await fs.readdir(promptsRoot, { withFileTypes: true })
|
||||
return dirents
|
||||
.filter(dirent => dirent.isFile() && dirent.name.endsWith('.md'))
|
||||
.map(dirent => dirent.name)
|
||||
.sort()
|
||||
}
|
||||
|
||||
export async function listPromptNames(promptsRoot = defaultPromptsRoot) {
|
||||
return (await listPromptFiles(promptsRoot)).map(file => file.replace(/\.md$/i, ''))
|
||||
}
|
||||
|
||||
export async function resolvePromptPath(name, promptsRoot = defaultPromptsRoot) {
|
||||
const normalized = String(name ?? '').trim()
|
||||
if (!normalized)
|
||||
throw new Error('Prompt name is required')
|
||||
|
||||
const candidates = normalized.endsWith('.md')
|
||||
? [normalized]
|
||||
: [`${normalized}.md`, normalized]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const fullPath = path.join(promptsRoot, candidate)
|
||||
try {
|
||||
await fs.access(fullPath)
|
||||
return fullPath
|
||||
}
|
||||
catch {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
|
||||
const available = await listPromptNames(promptsRoot)
|
||||
throw new Error(`Unknown prompt: ${normalized}. Available prompts: ${available.join(', ')}`)
|
||||
}
|
||||
|
||||
export async function readPrompt(name, promptsRoot = defaultPromptsRoot) {
|
||||
const fullPath = await resolvePromptPath(name, promptsRoot)
|
||||
const content = await fs.readFile(fullPath, 'utf8')
|
||||
return { fullPath, content }
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
export const packageRoot = path.resolve(__dirname, '..')
|
||||
export const defaultSourceRoot = path.join(packageRoot, '.agents', 'skills')
|
||||
|
||||
export function printInstallHelp(command = 'markstream-vue skills install', defaultMode = 'copy') {
|
||||
console.log(`Usage: ${command} [options]
|
||||
|
||||
Install bundled Markstream skills from .agents/skills into a local agent skill directory.
|
||||
|
||||
Options:
|
||||
--target <agents|codex|path> Target directory preset or custom path.
|
||||
Default: agents
|
||||
--mode <symlink|copy> Install mode. Default: ${defaultMode}
|
||||
--force Replace existing non-symlink targets
|
||||
--dry-run Print actions without writing anything
|
||||
--help Show this help
|
||||
|
||||
Examples:
|
||||
${command}
|
||||
${command} --mode copy
|
||||
${command} --target codex
|
||||
${command} --target ./tmp/skills-test --mode copy --force
|
||||
`)
|
||||
}
|
||||
|
||||
export function parseInstallArgs(argv, defaults = {}) {
|
||||
const args = {
|
||||
target: defaults.target ?? 'agents',
|
||||
mode: defaults.mode ?? 'copy',
|
||||
force: false,
|
||||
dryRun: false,
|
||||
help: false,
|
||||
}
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i]
|
||||
if (arg === '--')
|
||||
continue
|
||||
if (arg === '--target') {
|
||||
args.target = argv[++i] ?? ''
|
||||
}
|
||||
else if (arg === '--mode') {
|
||||
args.mode = argv[++i] ?? ''
|
||||
}
|
||||
else if (arg === '--force') {
|
||||
args.force = true
|
||||
}
|
||||
else if (arg === '--dry-run') {
|
||||
args.dryRun = true
|
||||
}
|
||||
else if (arg === '--help' || arg === '-h') {
|
||||
args.help = true
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unknown argument: ${arg}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!['symlink', 'copy'].includes(args.mode))
|
||||
throw new Error(`Invalid --mode value: ${args.mode}`)
|
||||
|
||||
if (!args.target)
|
||||
throw new Error('Missing --target value')
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
export function resolveTargetRoot(target) {
|
||||
const home = os.homedir()
|
||||
if (target === 'agents')
|
||||
return path.join(home, '.agents', 'skills')
|
||||
if (target === 'codex') {
|
||||
const codexHome = process.env.CODEX_HOME || path.join(home, '.codex')
|
||||
return path.join(codexHome, 'skills')
|
||||
}
|
||||
return path.resolve(process.cwd(), target)
|
||||
}
|
||||
|
||||
async function exists(filePath) {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function listSkillDirs(sourceRoot = defaultSourceRoot) {
|
||||
const dirents = await fs.readdir(sourceRoot, { withFileTypes: true })
|
||||
const skillDirs = []
|
||||
for (const dirent of dirents) {
|
||||
if (!dirent.isDirectory())
|
||||
continue
|
||||
const fullPath = path.join(sourceRoot, dirent.name)
|
||||
if (await exists(path.join(fullPath, 'SKILL.md')))
|
||||
skillDirs.push(fullPath)
|
||||
}
|
||||
return skillDirs.sort()
|
||||
}
|
||||
|
||||
export async function listSkillNames(sourceRoot = defaultSourceRoot) {
|
||||
return (await listSkillDirs(sourceRoot)).map(dir => path.basename(dir))
|
||||
}
|
||||
|
||||
async function ensureDir(dirPath, dryRun) {
|
||||
if (dryRun)
|
||||
return
|
||||
await fs.mkdir(dirPath, { recursive: true })
|
||||
}
|
||||
|
||||
async function readLinkedTarget(destPath) {
|
||||
try {
|
||||
const rawTarget = await fs.readlink(destPath)
|
||||
return path.resolve(path.dirname(destPath), rawTarget)
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function removeExisting(destPath, force, dryRun) {
|
||||
const stat = await fs.lstat(destPath)
|
||||
if (stat.isSymbolicLink()) {
|
||||
if (!dryRun)
|
||||
await fs.rm(destPath, { recursive: true, force: true })
|
||||
return 'replaced-symlink'
|
||||
}
|
||||
if (!force)
|
||||
throw new Error(`Target already exists and is not a symlink: ${destPath}. Re-run with --force to replace it.`)
|
||||
if (!dryRun)
|
||||
await fs.rm(destPath, { recursive: true, force: true })
|
||||
return 'replaced-existing'
|
||||
}
|
||||
|
||||
async function installSkill({ srcPath, destPath, mode, force, dryRun }) {
|
||||
if (await exists(destPath)) {
|
||||
if (mode === 'symlink') {
|
||||
const linkedTarget = await readLinkedTarget(destPath)
|
||||
if (linkedTarget === srcPath)
|
||||
return { status: 'skipped', detail: 'already-linked' }
|
||||
}
|
||||
const detail = await removeExisting(destPath, force, dryRun)
|
||||
if (mode === 'copy') {
|
||||
if (!dryRun)
|
||||
await fs.cp(srcPath, destPath, { recursive: true })
|
||||
return { status: 'installed', detail }
|
||||
}
|
||||
if (!dryRun)
|
||||
await fs.symlink(srcPath, destPath, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
return { status: 'installed', detail }
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
if (mode === 'copy')
|
||||
await fs.cp(srcPath, destPath, { recursive: true })
|
||||
else
|
||||
await fs.symlink(srcPath, destPath, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
}
|
||||
|
||||
return { status: 'installed', detail: 'new' }
|
||||
}
|
||||
|
||||
export async function runSkillsInstall({ sourceRoot = defaultSourceRoot, target, mode, force = false, dryRun = false }) {
|
||||
const targetRoot = resolveTargetRoot(target)
|
||||
const skillDirs = await listSkillDirs(sourceRoot)
|
||||
|
||||
if (skillDirs.length === 0)
|
||||
throw new Error(`No skill directories with SKILL.md found under ${sourceRoot}`)
|
||||
|
||||
await ensureDir(targetRoot, dryRun)
|
||||
|
||||
const summary = []
|
||||
for (const srcPath of skillDirs) {
|
||||
const skillName = path.basename(srcPath)
|
||||
const destPath = path.join(targetRoot, skillName)
|
||||
const result = await installSkill({
|
||||
srcPath,
|
||||
destPath,
|
||||
mode,
|
||||
force,
|
||||
dryRun,
|
||||
})
|
||||
summary.push({ skillName, destPath, ...result })
|
||||
}
|
||||
|
||||
return {
|
||||
sourceRoot,
|
||||
targetRoot,
|
||||
mode,
|
||||
dryRun,
|
||||
summary,
|
||||
}
|
||||
}
|
||||
|
||||
export function printInstallSummary(result) {
|
||||
const label = result.dryRun ? 'Dry run complete' : 'Skill install complete'
|
||||
console.log(`${label}: ${result.summary.length} skill(s) processed.`)
|
||||
console.log(`Source: ${result.sourceRoot}`)
|
||||
console.log(`Target: ${result.targetRoot}`)
|
||||
console.log(`Mode: ${result.mode}`)
|
||||
|
||||
for (const item of result.summary)
|
||||
console.log(`- ${item.skillName}: ${item.status} (${item.detail}) -> ${item.destPath}`)
|
||||
|
||||
if (!result.dryRun && result.mode === 'symlink')
|
||||
console.log('These installs are symlinks, so editing the source skills updates the installed versions too.')
|
||||
}
|
||||
Reference in New Issue
Block a user