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:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
@@ -0,0 +1,288 @@
#!/usr/bin/env node
// Validate that unchanged-region rail arrows reveal only the targeted region
// and reveal exactly five lines in the expected direction.
import net from 'node:net'
import path from 'node:path'
import process from 'node:process'
import { spawn } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { chromium } from 'playwright'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const repoRoot = path.resolve(__dirname, '..')
const demoDir = path.join(repoRoot, 'examples', 'streaming-demo')
function isPortOpen(port, host = '127.0.0.1') {
return new Promise((resolve) => {
const sock = net.createConnection({ host, port })
sock.on('connect', () => {
sock.end()
resolve(true)
})
sock.on('error', () => {
sock.destroy()
resolve(false)
})
})
}
async function findFreePort(start = 4205, end = 4215) {
for (let port = start; port <= end; port++) {
// eslint-disable-next-line no-await-in-loop
const open = await isPortOpen(port)
if (!open) return port
}
throw new Error(`No free port found in ${start}-${end}`)
}
async function waitForPort(port, ms = 20000) {
const start = Date.now()
while (true) {
// eslint-disable-next-line no-await-in-loop
const open = await isPortOpen(port)
if (open) return
if (Date.now() - start > ms)
throw new Error(`Timed out waiting for port ${port}`)
// eslint-disable-next-line no-await-in-loop
await new Promise(resolve => setTimeout(resolve, 150))
}
}
function killProcessTree(child) {
if (!child || child.killed) return
try {
child.kill('SIGTERM')
}
catch {}
setTimeout(() => {
try {
if (!child.killed) child.kill('SIGKILL')
}
catch {}
}, 3000).unref?.()
}
function parseCount(label) {
const match = label.match(/\d+/)
return match ? Number.parseInt(match[0], 10) : Number.NaN
}
function formatCountLabel(count) {
return `${count} unmodified ${count === 1 ? 'line' : 'lines'}`
}
function buildExpectedCounts(beforeLabels, targetLabel, delta) {
const counts = new Map()
beforeLabels.forEach((label) => {
counts.set(label, (counts.get(label) || 0) + 1)
})
counts.set(targetLabel, (counts.get(targetLabel) || 0) - 1)
if (counts.get(targetLabel) === 0) counts.delete(targetLabel)
const nextLabel = formatCountLabel(parseCount(targetLabel) - delta)
counts.set(nextLabel, (counts.get(nextLabel) || 0) + 1)
return Object.fromEntries(Array.from(counts.entries()).sort())
}
async function collectState(page, targetLabel) {
return page.evaluate((label) => {
const bridgeSelector =
'.stream-monaco-diff-unchanged-overlay .stream-monaco-diff-unchanged-bridge:not([hidden])'
const bridges = Array.from(document.querySelectorAll(bridgeSelector))
const modifiedEditor =
document.querySelector('.editor.modified') ||
document.querySelector('.monaco-editor.modified') ||
document.querySelector('.monaco-editor')
const findNeighbors = (bridge) => {
const rect = bridge.getBoundingClientRect()
let previousVisibleLine = null
let nextVisibleLine = null
const lineNumbers = modifiedEditor?.querySelectorAll('.line-numbers') || []
lineNumbers.forEach((node) => {
const lineNumber = Number.parseInt(node.textContent?.trim() || '', 10)
if (!Number.isFinite(lineNumber)) return
const top = node.getBoundingClientRect().top
if (top < rect.top - 1) {
previousVisibleLine =
previousVisibleLine == null
? lineNumber
: Math.max(previousVisibleLine, lineNumber)
}
else if (top > rect.bottom + 1) {
nextVisibleLine =
nextVisibleLine == null
? lineNumber
: Math.min(nextVisibleLine, lineNumber)
}
})
return { previousVisibleLine, nextVisibleLine }
}
const items = bridges.map((bridge) => {
const summary = bridge.querySelector('.stream-monaco-unchanged-summary')
const labelText = summary?.textContent?.trim() || ''
const rect = bridge.getBoundingClientRect()
const { previousVisibleLine, nextVisibleLine } = findNeighbors(bridge)
return {
label: labelText,
top: +rect.top.toFixed(2),
previousVisibleLine,
nextVisibleLine,
}
})
const counts = {}
items.forEach((item) => {
counts[item.label] = (counts[item.label] || 0) + 1
})
const target = items.find(item => item.label === label) || null
return {
items,
labels: items.map(item => item.label),
counts,
target,
}
}, targetLabel)
}
async function runDirectionCase(page, targetLabel, direction) {
const before = await collectState(page, targetLabel)
if (!before.target)
throw new Error(`Could not find target unchanged block: ${targetLabel}`)
const button = page
.locator('.stream-monaco-diff-unchanged-bridge:not([hidden])')
.filter({ has: page.locator('.stream-monaco-unchanged-summary', { hasText: targetLabel }) })
.locator(`.stream-monaco-unchanged-reveal[data-direction="${direction}"]`)
.first()
await button.click()
await page.waitForTimeout(350)
const after = await collectState(page, targetLabel)
const revealCount = 5
const targetAfterLabel = formatCountLabel(
parseCount(targetLabel) - revealCount,
)
const expectedCounts = buildExpectedCounts(
before.labels,
targetLabel,
revealCount,
)
const expectedPreviousVisibleLine =
direction === 'down'
? before.target.previousVisibleLine + revealCount
: before.target.previousVisibleLine
const expectedNextVisibleLine =
direction === 'up'
? before.target.nextVisibleLine - revealCount
: before.target.nextVisibleLine
const matchingTarget = after.items.find(
item =>
item.label === targetAfterLabel &&
item.previousVisibleLine === expectedPreviousVisibleLine &&
item.nextVisibleLine === expectedNextVisibleLine,
)
const normalizedAfterCounts = Object.fromEntries(
Object.entries(after.counts).sort(([left], [right]) =>
left.localeCompare(right),
),
)
const ok =
before.target.previousVisibleLine != null &&
before.target.nextVisibleLine != null &&
Boolean(matchingTarget) &&
JSON.stringify(normalizedAfterCounts) === JSON.stringify(expectedCounts)
return {
ok,
direction,
targetLabel,
targetAfterLabel,
before,
after,
expectedCounts,
expectedPreviousVisibleLine,
expectedNextVisibleLine,
matchingTarget,
}
}
async function main() {
const targetLabel = process.argv[2] || '71 unmodified lines'
const scenario = process.argv[3] || 'streaming'
const style = process.argv[4] || 'background'
const theme = process.argv[5] || 'snazzy-light'
const unchangedStyle = process.argv[6] || 'line-info'
const port = await findFreePort()
const vite = spawn(
process.execPath,
['./node_modules/vite/bin/vite.js', '--host', '127.0.0.1', '--port', String(port)],
{
cwd: demoDir,
stdio: ['ignore', 'pipe', 'pipe'],
},
)
const logs = []
vite.stdout.on('data', data => logs.push(String(data)))
vite.stderr.on('data', data => logs.push(String(data)))
const onExit = () => killProcessTree(vite)
process.on('exit', onExit)
process.on('SIGINT', () => process.exit(130))
process.on('SIGTERM', () => process.exit(143))
try {
await waitForPort(port)
const browser = await chromium.launch({ headless: true })
const runCase = async (direction) => {
const page = await browser.newPage({ viewport: { width: 1465, height: 1100 } })
const url =
`http://127.0.0.1:${port}/diff-ux?style=${style}&scenario=${scenario}` +
`&theme=${encodeURIComponent(theme)}&unchangedStyle=${encodeURIComponent(unchangedStyle)}`
await page.goto(url, { waitUntil: 'networkidle' })
await page.waitForSelector(
'.stream-monaco-diff-unchanged-overlay .stream-monaco-diff-unchanged-bridge',
{ timeout: 20000 },
)
const result = await runDirectionCase(page, targetLabel, direction)
await page.close()
return result
}
const down = await runCase('down')
const up = await runCase('up')
await browser.close()
const ok = down.ok && up.ok
console.log(
JSON.stringify(
{
ok,
targetLabel,
down,
up,
},
null,
2,
),
)
if (!ok) process.exitCode = 1
}
catch (error) {
console.error(String(error))
if (logs.length > 0) console.error(logs.join(''))
process.exitCode = 1
}
finally {
killProcessTree(vite)
}
}
main()