feat: add argument parsing and module resolution for assistant app
- Implemented argument parsing in args.ts to handle root, home, and help options. - Added parseHomeArg and parseHelpArg functions for command line argument handling. - Created ModuleResolver class in assistant-app-resolve.ts to resolve module paths, including scoped packages and relative paths. - Introduced caching mechanism for package.json reads to improve performance. - Added utility functions for checking file existence and clearing the cache.
This commit is contained in:
86
assistant/src/module/assistant/config/args.ts
Normal file
86
assistant/src/module/assistant/config/args.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
|
||||
|
||||
import { logger } from '@/module/logger.ts';
|
||||
import path from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
let kevisualDir = path.join(homedir(), 'kevisual');
|
||||
const envKevisualDir = process.env.ASSISTANT_CONFIG_DIR
|
||||
if (envKevisualDir) {
|
||||
kevisualDir = envKevisualDir;
|
||||
logger.debug('使用环境变量 ASSISTANT_CONFIG_DIR 作为 kevisual 目录:', kevisualDir);
|
||||
}
|
||||
|
||||
export const HomeConfigDir = path.join(kevisualDir, 'assistant-app');
|
||||
|
||||
export function parseArgs(args: string[]) {
|
||||
const result: { root?: string; home?: boolean; help?: boolean } = { home: true };
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
// 处理 root 参数
|
||||
if (arg === '--root') {
|
||||
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
||||
result.root = args[i + 1];
|
||||
i++; // 跳过下一个参数,因为它是值
|
||||
}
|
||||
}
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
result.help = true;
|
||||
}
|
||||
}
|
||||
if (result.root) {
|
||||
result.home = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 手动解析命令行参数
|
||||
* @param homedir
|
||||
* @returns
|
||||
*/
|
||||
export const parseHomeArg = (homedir?: string) => {
|
||||
const args = process.argv.slice(2);
|
||||
const execPath = process.execPath;
|
||||
const options = parseArgs(args);
|
||||
let _configDir: string | undefined = undefined;
|
||||
if (options.home && homedir) {
|
||||
_configDir = homedir;
|
||||
} else if (options.root) {
|
||||
_configDir = options.root;
|
||||
}
|
||||
const checkUrl = ['.opencode', 'bin/opencode', 'opencode.exe']
|
||||
const isOpencode = checkUrl.some((item) => execPath.includes(item))
|
||||
let isServer = false;
|
||||
// 如果args包含 server 则认为是服务端运行。其中config中server必须存在
|
||||
const checkArgs = process.argv.slice(1);
|
||||
// console.log('parseHomeArg args:', checkArgs, execPath);
|
||||
const isArgsServer = checkArgs.some((item) => item === 'server' || item.includes('asst-server') || item.includes('run-server.ts'));
|
||||
const isDev = checkArgs.some(item => item.includes('run-server.ts'));
|
||||
let isDaemon = false;
|
||||
if (isArgsServer) {
|
||||
// 判断 --daemon 参数, 如果有则认为是守护进程运行
|
||||
if (checkArgs.includes('--daemon') || checkArgs.includes('-d')) {
|
||||
isDaemon = true;
|
||||
}
|
||||
// 判断 -s 或者 --start 参数
|
||||
if (checkArgs.includes('-s') || checkArgs.includes('--start')) {
|
||||
isServer = true;
|
||||
}
|
||||
}
|
||||
return {
|
||||
isOpencode,
|
||||
options,
|
||||
isDev,
|
||||
isDaemon,
|
||||
configDir: _configDir,
|
||||
isServer
|
||||
};
|
||||
};
|
||||
|
||||
export const parseHelpArg = () => {
|
||||
const args = process.argv.slice(2);
|
||||
const options = parseArgs(args);
|
||||
return !!options?.help;
|
||||
};
|
||||
@@ -6,17 +6,12 @@ import { ProxyInfo } from '../proxy/proxy.ts';
|
||||
import dotenv from 'dotenv';
|
||||
import { logger } from '@/module/logger.ts';
|
||||
import { z } from 'zod'
|
||||
import { HomeConfigDir } from './args.ts'
|
||||
|
||||
let kevisualDir = path.join(homedir(), 'kevisual');
|
||||
const envKevisualDir = process.env.ASSISTANT_CONFIG_DIR
|
||||
if (envKevisualDir) {
|
||||
kevisualDir = envKevisualDir;
|
||||
logger.debug('使用环境变量 ASSISTANT_CONFIG_DIR 作为 kevisual 目录:', kevisualDir);
|
||||
}
|
||||
/**
|
||||
* 助手配置文件路径, 全局配置文件目录
|
||||
*/
|
||||
export const configDir = createDir(path.join(kevisualDir, 'assistant-app'));
|
||||
export const configDir = createDir(HomeConfigDir);
|
||||
|
||||
/**
|
||||
* 助手配置文件初始化
|
||||
@@ -383,72 +378,6 @@ export class AssistantConfig {
|
||||
type AppConfig = {
|
||||
list: any[];
|
||||
};
|
||||
export function parseArgs(args: string[]) {
|
||||
const result: { root?: string; home?: boolean; help?: boolean } = { home: true };
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
// 处理 root 参数
|
||||
if (arg === '--root') {
|
||||
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
||||
result.root = args[i + 1];
|
||||
i++; // 跳过下一个参数,因为它是值
|
||||
}
|
||||
}
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
result.help = true;
|
||||
}
|
||||
}
|
||||
if (result.root) {
|
||||
result.home = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* 手动解析命令行参数
|
||||
* @param homedir
|
||||
* @returns
|
||||
*/
|
||||
export const parseHomeArg = (homedir?: string) => {
|
||||
const args = process.argv.slice(2);
|
||||
const execPath = process.execPath;
|
||||
const options = parseArgs(args);
|
||||
let _configDir = undefined;
|
||||
if (options.home && homedir) {
|
||||
_configDir = homedir;
|
||||
} else if (options.root) {
|
||||
_configDir = options.root;
|
||||
}
|
||||
const checkUrl = ['.opencode', 'bin/opencode', 'opencode.exe']
|
||||
const isOpencode = checkUrl.some((item) => execPath.includes(item))
|
||||
let isServer = false;
|
||||
// 如果args包含 server 则认为是服务端运行。其中config中server必须存在
|
||||
console.log('parseHomeArg args:', args);
|
||||
if (args.includes('server') || args.includes('assistant-server')) {
|
||||
let isDaemon = false;
|
||||
// 判断 --daemon 参数, 如果有则认为是守护进程运行
|
||||
if (args.includes('--daemon') || args.includes('-d')) {
|
||||
isDaemon = true;
|
||||
}
|
||||
if (!isDaemon) {
|
||||
// 判断 -s 或者 --start 参数
|
||||
if (args.includes('-s') || args.includes('--start')) {
|
||||
isServer = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
isOpencode,
|
||||
options,
|
||||
configDir: _configDir,
|
||||
isServer
|
||||
};
|
||||
};
|
||||
|
||||
export const parseHelpArg = () => {
|
||||
const args = process.argv.slice(2);
|
||||
const options = parseArgs(args);
|
||||
return !!options?.help;
|
||||
};
|
||||
|
||||
export const parseIfJson = (content: string) => {
|
||||
try {
|
||||
@@ -458,3 +387,5 @@ export const parseIfJson = (content: string) => {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export * from './args.ts';
|
||||
@@ -0,0 +1,108 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
export class ModuleResolver {
|
||||
private root: string;
|
||||
private pkgCache = new Map<string, { pkg: any; mtime: number }>();
|
||||
|
||||
constructor(root: string) {
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析模块路径
|
||||
* - 绝对路径 → 直接返回
|
||||
* - @开头 → 解析 scoped 包 exports/main
|
||||
* - 普通包名 → 从 node_modules 查找
|
||||
* - 相对路径 → 从 root 查找
|
||||
*/
|
||||
resolve(routePath: string): string {
|
||||
if (path.isAbsolute(routePath)) {
|
||||
return routePath;
|
||||
}
|
||||
|
||||
// Scoped 包 @org/pkg/subpath
|
||||
if (routePath.startsWith('@')) {
|
||||
return this.resolveScopedPackage(routePath);
|
||||
}
|
||||
|
||||
// 普通包名 pkg 或 pkg/subpath
|
||||
if (!routePath.startsWith('.') && !routePath.startsWith('/')) {
|
||||
const pkgPath = path.join(this.root, 'node_modules', routePath);
|
||||
if (this.fileIsExists(pkgPath)) {
|
||||
return pkgPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 相对路径 ./xxx 或 ../xxx
|
||||
const localFullPath = path.resolve(this.root, routePath);
|
||||
return this.fileIsExists(localFullPath) ? localFullPath : routePath;
|
||||
}
|
||||
|
||||
/** 解析 scoped 包 */
|
||||
private resolveScopedPackage(routePath: string): string {
|
||||
const parts = routePath.split('/');
|
||||
const pkgName = parts.slice(0, 2).join('/'); // @org/pkg
|
||||
const subPath = parts.slice(2).join('/'); // routes
|
||||
|
||||
const pkgPath = path.join(this.root, 'node_modules', pkgName);
|
||||
const pkgJsonPath = path.join(pkgPath, 'package.json');
|
||||
|
||||
const pkg = this.readPackageJson(pkgJsonPath);
|
||||
if (!pkg) {
|
||||
return routePath;
|
||||
}
|
||||
|
||||
const entryPath = this.resolvePackageExport(pkg, subPath);
|
||||
return path.join(pkgPath, entryPath);
|
||||
}
|
||||
|
||||
/** 解析 package.json exports/main 字段 */
|
||||
private resolvePackageExport(pkg: any, subPath: string): string {
|
||||
const exportsField = pkg.exports;
|
||||
|
||||
if (exportsField && typeof exportsField === 'object') {
|
||||
const exportKey = subPath ? `./${subPath}` : '.';
|
||||
if (exportsField[exportKey]) {
|
||||
const entry = exportsField[exportKey];
|
||||
if (typeof entry === 'object') {
|
||||
return entry.import || entry.default || entry;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return pkg.main || 'index.js';
|
||||
}
|
||||
|
||||
/** 带缓存读取 package.json */
|
||||
private readPackageJson(pkgJsonPath: string): any | null {
|
||||
try {
|
||||
const cached = this.pkgCache.get(pkgJsonPath);
|
||||
const stats = fs.statSync(pkgJsonPath);
|
||||
if (cached && cached.mtime === stats.mtimeMs) {
|
||||
return cached.pkg;
|
||||
}
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
||||
this.pkgCache.set(pkgJsonPath, { pkg, mtime: stats.mtimeMs });
|
||||
return pkg;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 文件是否存在 */
|
||||
private fileIsExists(filepath: string): boolean {
|
||||
try {
|
||||
fs.accessSync(filepath, fs.constants.F_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 清空缓存 */
|
||||
clearCache(): void {
|
||||
this.pkgCache.clear();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { getEnvToken } from '@/module/http-token.ts';
|
||||
import { initApi } from '@kevisual/api/proxy'
|
||||
import { Query } from '@kevisual/query';
|
||||
import { initLightCode } from '@/module/light-code/index.ts';
|
||||
import { ModuleResolver } from './assistant-app-resolve.ts';
|
||||
export class AssistantApp extends Manager {
|
||||
config: AssistantConfig;
|
||||
pagesPath: string;
|
||||
@@ -18,6 +19,8 @@ export class AssistantApp extends Manager {
|
||||
attemptedConnectTimes = 0;
|
||||
remoteApp: RemoteApp | null = null;
|
||||
remoteUrl: string | null = null;
|
||||
private resolver: ModuleResolver;
|
||||
|
||||
constructor(config: AssistantConfig, mainApp?: App) {
|
||||
config.checkMounted();
|
||||
const appsPath = config?.configPath?.appsDir || path.join(process.cwd(), 'apps');
|
||||
@@ -31,6 +34,7 @@ export class AssistantApp extends Manager {
|
||||
});
|
||||
this.pagesPath = pagesPath;
|
||||
this.config = config;
|
||||
this.resolver = new ModuleResolver(config.configPath.configDir);
|
||||
}
|
||||
async pageList() {
|
||||
const pages = await glob(['*/*/package.json'], {
|
||||
@@ -212,21 +216,16 @@ export class AssistantApp extends Manager {
|
||||
}
|
||||
}
|
||||
async initRoutes() {
|
||||
// TODO 初始化应用内置路由
|
||||
const routes = this.config.getConfig().routes || [];
|
||||
for (const route of routes) {
|
||||
try {
|
||||
if (typeof route === 'string') {
|
||||
await import(route);
|
||||
console.log('安装路由', route);
|
||||
} else if (typeof route === 'object' && route.path) {
|
||||
const routePath = route.path;
|
||||
await import(routePath);
|
||||
console.log('安装路由', routePath);
|
||||
}
|
||||
const routeStr = typeof route === 'string' ? route : route.path;
|
||||
const resolvedPath = this.resolver.resolve(routeStr);
|
||||
await import(resolvedPath);
|
||||
console.log('路由已初始化', route);
|
||||
} catch (err) {
|
||||
console.error('初始化路由失败', route, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,4 +33,14 @@ export const installDeps = async (opts: InstallDepsOptions) => {
|
||||
} else {
|
||||
syncSpawn('npm', params, { cwd: appPath, stdio: 'inherit', env: process.env });
|
||||
}
|
||||
};
|
||||
|
||||
export const execCommand = (command: string, options: { cwd?: string } = {}) => {
|
||||
const { cwd } = options;
|
||||
return spawnSync(command, {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
cwd: cwd,
|
||||
env: process.env,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user