feat: 支持 Bun standalone 单文件打包
- core: 工具描述从文件系统加载改为编译时内联生成 - core: 添加 WASM 加载器支持嵌入式 WASM 数据 - core: bash-parser 使用动态导入 web-tree-sitter - server: 添加静态文件托管支持 (--static 参数) - server: 新增 standalone 入口点 (嵌入 Web UI + WASM) - scripts: 添加 build-standalone.ts 构建脚本 - 更新 .gitignore 忽略生成文件
This commit is contained in:
@@ -40,7 +40,8 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && cp -r src/tools/descriptions dist/tools/",
|
||||
"prebuild": "bun run scripts/generate-descriptions.ts",
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Generate Tool Descriptions
|
||||
*
|
||||
* 将 descriptions 目录下的 .txt 文件转换为 TypeScript 模块
|
||||
* 用于 Bun 打包时内联工具描述
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const DESCRIPTIONS_DIR = path.join(__dirname, '../src/tools/descriptions');
|
||||
const OUTPUT_FILE = path.join(__dirname, '../src/tools/descriptions.generated.ts');
|
||||
|
||||
interface DescriptionEntry {
|
||||
category: string;
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
function collectDescriptions(): DescriptionEntry[] {
|
||||
const entries: DescriptionEntry[] = [];
|
||||
|
||||
const categories = fs.readdirSync(DESCRIPTIONS_DIR);
|
||||
for (const category of categories) {
|
||||
const categoryPath = path.join(DESCRIPTIONS_DIR, category);
|
||||
const stat = fs.statSync(categoryPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
const files = fs.readdirSync(categoryPath);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.txt')) {
|
||||
const name = file.replace('.txt', '');
|
||||
const content = fs.readFileSync(path.join(categoryPath, file), 'utf-8').trim();
|
||||
entries.push({ category, name, content });
|
||||
}
|
||||
}
|
||||
} else if (category.endsWith('.txt')) {
|
||||
const name = category.replace('.txt', '');
|
||||
const content = fs.readFileSync(categoryPath, 'utf-8').trim();
|
||||
entries.push({ category: '', name, content });
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function escapeString(str: string): string {
|
||||
return str
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/`/g, '\\`')
|
||||
.replace(/\$/g, '\\$');
|
||||
}
|
||||
|
||||
function generateCode(entries: DescriptionEntry[]): string {
|
||||
const lines: string[] = [
|
||||
'// Auto-generated by scripts/generate-descriptions.ts',
|
||||
'// Do not edit manually',
|
||||
'',
|
||||
'export const TOOL_DESCRIPTIONS: Record<string, string> = {',
|
||||
];
|
||||
|
||||
for (const entry of entries) {
|
||||
const escapedContent = escapeString(entry.content);
|
||||
lines.push(` '${entry.name}': \`${escapedContent}\`,`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('};');
|
||||
lines.push('');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log('Collecting tool descriptions...');
|
||||
const entries = collectDescriptions();
|
||||
console.log(`Found ${entries.length} descriptions`);
|
||||
|
||||
console.log('Generating TypeScript code...');
|
||||
const code = generateCode(entries);
|
||||
|
||||
console.log(`Writing to ${OUTPUT_FILE}...`);
|
||||
fs.writeFileSync(OUTPUT_FILE, code, 'utf-8');
|
||||
|
||||
console.log('Done!');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -354,6 +354,9 @@ export type {
|
||||
FileSearchOptions,
|
||||
} from './file-index/index.js';
|
||||
|
||||
// RepoMap WASM 加载器(用于 standalone 构建)
|
||||
export { setEmbeddedWasm, hasEmbeddedWasm } from './repomap/index.js';
|
||||
|
||||
// Constants - 统一存储路径
|
||||
export {
|
||||
APP_DIR_NAME,
|
||||
|
||||
@@ -1,29 +1,41 @@
|
||||
import { Parser, Language, type Node } from 'web-tree-sitter';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { hasEmbeddedWasm, getEmbeddedWasm } from '../repomap/tags/wasm-loader.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// 解析后的命令结构
|
||||
export interface ParsedCommand {
|
||||
name: string; // 命令名,如 "git"
|
||||
name: string; // 命令名,如 "git"
|
||||
subcommand?: string; // 子命令,如 "push"
|
||||
args: string[]; // 参数列表
|
||||
text: string; // 原始命令文本
|
||||
args: string[]; // 参数列表
|
||||
text: string; // 原始命令文本
|
||||
}
|
||||
|
||||
// 解析结果
|
||||
export interface ParseResult {
|
||||
commands: ParsedCommand[]; // 所有解析出的命令
|
||||
commands: ParsedCommand[]; // 所有解析出的命令
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// 单例解析器
|
||||
let parserInstance: Parser | null = null;
|
||||
let bashLanguage: Language | null = null;
|
||||
// Tree-sitter 类型
|
||||
interface TreeSitterNode {
|
||||
type: string;
|
||||
text: string;
|
||||
childCount: number;
|
||||
child(index: number): TreeSitterNode | null;
|
||||
}
|
||||
|
||||
// 单例解析器 (使用 any 类型避免与 web-tree-sitter 的类型冲突)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let parserInstance: any = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let bashLanguage: any = null;
|
||||
let initPromise: Promise<void> | null = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let TreeSitter: any = null;
|
||||
|
||||
/**
|
||||
* 获取 wasm 文件路径
|
||||
@@ -55,19 +67,51 @@ async function initParser(): Promise<void> {
|
||||
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
// 初始化 tree-sitter
|
||||
await Parser.init({
|
||||
locateFile: (scriptName: string) => {
|
||||
return getWasmPath(scriptName);
|
||||
},
|
||||
});
|
||||
// 动态导入 web-tree-sitter
|
||||
TreeSitter = await import('web-tree-sitter');
|
||||
|
||||
// 检查是否有嵌入的 WASM
|
||||
if (hasEmbeddedWasm()) {
|
||||
const wasmData = getEmbeddedWasm('tree-sitter.wasm');
|
||||
if (wasmData) {
|
||||
// 使用嵌入的 WASM 数据初始化
|
||||
await TreeSitter.Parser.init({
|
||||
wasmBinary: wasmData,
|
||||
});
|
||||
} else {
|
||||
// 回退到文件系统
|
||||
await TreeSitter.Parser.init({
|
||||
locateFile: (scriptName: string) => {
|
||||
return getWasmPath(scriptName);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 使用文件系统加载
|
||||
await TreeSitter.Parser.init({
|
||||
locateFile: (scriptName: string) => {
|
||||
return getWasmPath(scriptName);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 创建解析器实例
|
||||
parserInstance = new Parser();
|
||||
parserInstance = new TreeSitter.Parser();
|
||||
|
||||
// 加载 bash 语言
|
||||
const bashWasmPath = getWasmPath('tree-sitter-bash.wasm');
|
||||
bashLanguage = await Language.load(bashWasmPath);
|
||||
if (hasEmbeddedWasm()) {
|
||||
const bashWasmData = getEmbeddedWasm('tree-sitter-bash.wasm');
|
||||
if (bashWasmData) {
|
||||
bashLanguage = await TreeSitter.Parser.Language.load(bashWasmData);
|
||||
} else {
|
||||
const bashWasmPath = getWasmPath('tree-sitter-bash.wasm');
|
||||
bashLanguage = await TreeSitter.Parser.Language.load(bashWasmPath);
|
||||
}
|
||||
} else {
|
||||
const bashWasmPath = getWasmPath('tree-sitter-bash.wasm');
|
||||
bashLanguage = await TreeSitter.Parser.Language.load(bashWasmPath);
|
||||
}
|
||||
|
||||
parserInstance.setLanguage(bashLanguage);
|
||||
} catch (error) {
|
||||
initPromise = null;
|
||||
@@ -81,7 +125,7 @@ async function initParser(): Promise<void> {
|
||||
/**
|
||||
* 从语法树节点中提取命令信息
|
||||
*/
|
||||
function extractCommandFromNode(node: Node): ParsedCommand {
|
||||
function extractCommandFromNode(node: TreeSitterNode): ParsedCommand {
|
||||
const parts: string[] = [];
|
||||
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
@@ -101,8 +145,10 @@ function extractCommandFromNode(node: Node): ParsedCommand {
|
||||
// 对于字符串类型,提取内部文本(去掉引号)
|
||||
if (child.type === 'string' || child.type === 'raw_string') {
|
||||
const text = child.text;
|
||||
if ((text.startsWith('"') && text.endsWith('"')) ||
|
||||
(text.startsWith("'") && text.endsWith("'"))) {
|
||||
if (
|
||||
(text.startsWith('"') && text.endsWith('"')) ||
|
||||
(text.startsWith("'") && text.endsWith("'"))
|
||||
) {
|
||||
parts.push(text.slice(1, -1));
|
||||
} else {
|
||||
parts.push(text);
|
||||
@@ -138,8 +184,8 @@ function extractCommandFromNode(node: Node): ParsedCommand {
|
||||
/**
|
||||
* 递归查找所有命令节点
|
||||
*/
|
||||
function findCommandNodes(node: Node): Node[] {
|
||||
const commands: Node[] = [];
|
||||
function findCommandNodes(node: TreeSitterNode): TreeSitterNode[] {
|
||||
const commands: TreeSitterNode[] = [];
|
||||
|
||||
if (node.type === 'command') {
|
||||
commands.push(node);
|
||||
|
||||
@@ -11,6 +11,9 @@ export { RepoMap, createRepoMap } from './repomap.js';
|
||||
// Tag 提取
|
||||
export { TagExtractor } from './tags/index.js';
|
||||
|
||||
// WASM 加载器(用于 standalone 构建)
|
||||
export { setEmbeddedWasm, hasEmbeddedWasm } from './tags/wasm-loader.js';
|
||||
|
||||
// PageRank 排序
|
||||
export {
|
||||
Graph,
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import type { Tag } from '../types.js';
|
||||
import { getLanguageFromFilename } from '../types.js';
|
||||
import { hasEmbeddedWasm, getEmbeddedWasm } from './wasm-loader.js';
|
||||
|
||||
// Tree-sitter 类型(web-tree-sitter)
|
||||
interface TreeSitterParser {
|
||||
@@ -56,8 +57,25 @@ async function initTreeSitter(): Promise<void> {
|
||||
|
||||
try {
|
||||
const TreeSitter = await import('web-tree-sitter');
|
||||
// Parser 是命名导出的类,init 是其静态方法
|
||||
await TreeSitter.Parser.init();
|
||||
|
||||
// 检查是否有嵌入的 WASM
|
||||
if (hasEmbeddedWasm()) {
|
||||
const wasmData = getEmbeddedWasm('tree-sitter.wasm');
|
||||
if (wasmData) {
|
||||
// 使用嵌入的 WASM 数据直接初始化
|
||||
// web-tree-sitter 支持传入 WebAssembly.Module 或 ArrayBuffer
|
||||
await TreeSitter.Parser.init({
|
||||
// 传入 WASM 二进制数据
|
||||
wasmBinary: wasmData,
|
||||
});
|
||||
} else {
|
||||
await TreeSitter.Parser.init();
|
||||
}
|
||||
} else {
|
||||
// 使用默认行为(从 node_modules 加载)
|
||||
await TreeSitter.Parser.init();
|
||||
}
|
||||
|
||||
ParserClass = TreeSitter.Parser;
|
||||
treeSitterInitialized = true;
|
||||
} catch (error) {
|
||||
@@ -215,7 +233,19 @@ export class TagExtractor {
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试加载 WASM 语言文件
|
||||
// 首先检查是否有嵌入的语言 WASM
|
||||
if (hasEmbeddedWasm()) {
|
||||
const wasmName = `tree-sitter-${lang}.wasm`;
|
||||
const wasmData = getEmbeddedWasm(wasmName);
|
||||
if (wasmData) {
|
||||
// 使用嵌入的 WASM 数据加载语言
|
||||
const language = await ParserClass.Language.load(wasmData);
|
||||
this.languages.set(lang, language);
|
||||
return language;
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到从文件系统加载
|
||||
const wasmPath = this.getWasmPath(lang);
|
||||
const language = await ParserClass.Language.load(wasmPath);
|
||||
this.languages.set(lang, language);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* WASM Loader for Tree-sitter
|
||||
*
|
||||
* 支持从文件系统或嵌入的 base64 数据加载 WASM
|
||||
*/
|
||||
|
||||
// 嵌入的 WASM 数据(构建时生成)
|
||||
let embeddedWasm: Map<string, string> | null = null;
|
||||
|
||||
/**
|
||||
* 设置嵌入的 WASM 数据
|
||||
*/
|
||||
export function setEmbeddedWasm(wasm: Map<string, string>): void {
|
||||
embeddedWasm = wasm;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有嵌入的 WASM
|
||||
*/
|
||||
export function hasEmbeddedWasm(): boolean {
|
||||
return embeddedWasm !== null && embeddedWasm.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取嵌入的 WASM 数据
|
||||
*/
|
||||
export function getEmbeddedWasm(name: string): Uint8Array | null {
|
||||
if (!embeddedWasm) return null;
|
||||
|
||||
const base64 = embeddedWasm.get(name);
|
||||
if (!base64) return null;
|
||||
|
||||
// 解码 base64
|
||||
const binaryString = atob(base64);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 tree-sitter.wasm 的路径或数据
|
||||
*/
|
||||
export function getTreeSitterWasmPath(): string | undefined {
|
||||
// 如果没有嵌入数据,返回 undefined 让默认行为生效
|
||||
if (!hasEmbeddedWasm()) {
|
||||
return undefined;
|
||||
}
|
||||
// 返回一个特殊标记,在 init 时会被拦截
|
||||
return '__embedded__';
|
||||
}
|
||||
@@ -1,64 +1,16 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
/**
|
||||
* Tool Description Loader
|
||||
*
|
||||
* 从内联的 descriptions.generated.ts 加载工具描述
|
||||
* 支持 Bun 单文件打包
|
||||
*/
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// 工具名到子目录的映射
|
||||
const TOOL_CATEGORY_MAP: Record<string, string> = {
|
||||
// shell
|
||||
bash: 'shell',
|
||||
kill_shell: 'shell',
|
||||
// filesystem
|
||||
read_file: 'filesystem',
|
||||
write_file: 'filesystem',
|
||||
edit_file: 'filesystem',
|
||||
multi_edit: 'filesystem',
|
||||
glob: 'filesystem',
|
||||
grep: 'filesystem',
|
||||
// web
|
||||
web_search: 'web',
|
||||
web_extract: 'web',
|
||||
// git
|
||||
git_status: 'git',
|
||||
git_diff: 'git',
|
||||
git_log: 'git',
|
||||
git_branch: 'git',
|
||||
git_add: 'git',
|
||||
git_commit: 'git',
|
||||
git_push: 'git',
|
||||
git_pull: 'git',
|
||||
git_checkout: 'git',
|
||||
git_stash: 'git',
|
||||
// todo
|
||||
todo_write: 'todo',
|
||||
// plan
|
||||
ask_user_question: 'plan',
|
||||
enter_plan_mode: 'plan',
|
||||
exit_plan_mode: 'plan',
|
||||
// task
|
||||
task: 'task',
|
||||
task_output: 'task',
|
||||
// checkpoint
|
||||
checkpoint_create: 'checkpoint',
|
||||
checkpoint_list: 'checkpoint',
|
||||
checkpoint_diff: 'checkpoint',
|
||||
checkpoint_restore: 'checkpoint',
|
||||
undo: 'checkpoint',
|
||||
// repomap
|
||||
repo_map: 'repomap',
|
||||
};
|
||||
import { TOOL_DESCRIPTIONS } from './descriptions.generated.js';
|
||||
|
||||
export function loadDescription(toolName: string): string {
|
||||
const category = TOOL_CATEGORY_MAP[toolName];
|
||||
const filePath = category
|
||||
? path.join(__dirname, 'descriptions', category, `${toolName}.txt`)
|
||||
: path.join(__dirname, 'descriptions', `${toolName}.txt`);
|
||||
|
||||
try {
|
||||
return fs.readFileSync(filePath, 'utf-8').trim();
|
||||
} catch {
|
||||
throw new Error(`无法加载工具描述文件: ${filePath}`);
|
||||
const description = TOOL_DESCRIPTIONS[toolName];
|
||||
if (!description) {
|
||||
throw new Error(`工具描述未找到: ${toolName}`);
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user