feat: add silky cli tools
This commit is contained in:
36
assistant/src/command/app/index.ts
Normal file
36
assistant/src/command/app/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { program, Command, assistantConfig } from '@/program.ts';
|
||||
import { AppDownload } from '@/services/app/index.ts';
|
||||
|
||||
const appManagerCommand = new Command('app').description('本地的应用模块的安装和下载, 分为 app 和 web 两种类型');
|
||||
program.addCommand(appManagerCommand);
|
||||
|
||||
const downloadCommand = new Command('download')
|
||||
.description('下载应用')
|
||||
.option('-i, --id <id>', '应用名称')
|
||||
.option('-t, --type <type>', '应用类型', 'web')
|
||||
.option('-r, --registry <registry>', '应用源 https://kevisual.cn')
|
||||
.action(async (options) => {
|
||||
const { id, type } = options;
|
||||
assistantConfig.checkMounted();
|
||||
const registry = options.registry || assistantConfig.getRegistry();
|
||||
// console.log('registry', registry);
|
||||
const app = new AppDownload(assistantConfig);
|
||||
if (id) {
|
||||
await app.downloadApp({ id, type, registry });
|
||||
}
|
||||
});
|
||||
|
||||
appManagerCommand.addCommand(downloadCommand);
|
||||
|
||||
const deleteCommand = new Command('delete')
|
||||
.description('删除应用')
|
||||
.option('-i, --id <id>', '应用名称')
|
||||
.option('-t, --type <type>', '应用类型', 'web')
|
||||
.action(async (options) => {
|
||||
const { id, type } = options;
|
||||
const app = new AppDownload(assistantConfig);
|
||||
if (id) {
|
||||
await app.deleteApp({ id, type });
|
||||
}
|
||||
});
|
||||
appManagerCommand.addCommand(deleteCommand);
|
||||
@@ -2,6 +2,7 @@ import { program, runProgram } from '@/program.ts';
|
||||
import './command/config-manager/index.ts';
|
||||
import './command/app-manager/index.ts';
|
||||
import './command/asst-server/index.ts';
|
||||
import './command/app/index.ts';
|
||||
|
||||
/**
|
||||
* 通过命令行解析器解析参数
|
||||
|
||||
2
assistant/src/lib.ts
Normal file
2
assistant/src/lib.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './module/assistant/index.ts';
|
||||
export { AssistantInit } from './services/init/index.ts';
|
||||
@@ -60,8 +60,9 @@ export const initConfig = (configRootPath: string) => {
|
||||
};
|
||||
export type ReturnInitConfigType = ReturnType<typeof initConfig>;
|
||||
|
||||
type AssistantConfigData = {
|
||||
pageApi?: string; // https://kevisual.silkyai.cn
|
||||
export type AssistantConfigData = {
|
||||
pageApi?: string; // https://kevisual.cn
|
||||
registry?: string; // https://kevisual.cn
|
||||
proxy?: ProxyInfo[];
|
||||
apiProxyList?: ProxyInfo[];
|
||||
description?: string;
|
||||
@@ -135,6 +136,10 @@ export class AssistantConfig {
|
||||
}
|
||||
return this.getConfig();
|
||||
}
|
||||
getRegistry() {
|
||||
const config = this.getCacheAssistantConfig();
|
||||
return config?.registry || config?.pageApi;
|
||||
}
|
||||
/**
|
||||
* 设置 assistant-config.json 配置
|
||||
* @param config
|
||||
@@ -211,19 +216,23 @@ export class AssistantConfig {
|
||||
* pem: 证书目录
|
||||
* @param configDir
|
||||
*/
|
||||
static detectConfigDir(configDir?: string) {
|
||||
static detectConfigDir(configDir?: string, deep = 3) {
|
||||
const checkConfigDir = path.resolve(configDir || process.env.ASSISTANT_CONFIG_DIR || process.cwd());
|
||||
const configPath = path.join(checkConfigDir, 'assistant-app');
|
||||
if (checkFileExists(configPath)) {
|
||||
return path.join(checkConfigDir);
|
||||
}
|
||||
const lastConfigPath = path.join(checkConfigDir, '..', 'assistant-app');
|
||||
if (checkFileExists(lastConfigPath)) {
|
||||
return path.join(checkConfigDir, '..');
|
||||
if (deep >= 2) {
|
||||
const lastConfigPath = path.join(checkConfigDir, '..', 'assistant-app');
|
||||
if (checkFileExists(lastConfigPath)) {
|
||||
return path.join(checkConfigDir, '..');
|
||||
}
|
||||
}
|
||||
const lastConfigPath2 = path.join(checkConfigDir, '../..', 'assistant-app');
|
||||
if (checkFileExists(lastConfigPath2)) {
|
||||
return path.join(checkConfigDir, '../..');
|
||||
if (deep >= 3) {
|
||||
const lastConfigPath2 = path.join(checkConfigDir, '../..', 'assistant-app');
|
||||
if (checkFileExists(lastConfigPath2)) {
|
||||
return path.join(checkConfigDir, '../..');
|
||||
}
|
||||
}
|
||||
// 如果没有找到助手配置文件目录,则返回当前目录, 执行默认创建助手配置文件目录
|
||||
return checkConfigDir;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './install/index.ts';
|
||||
export * from './config/index.ts';
|
||||
export * from './file/index.ts';
|
||||
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { checkFileExists } from '../file/index.ts';
|
||||
|
||||
type DownloadTask = {
|
||||
downloadPath: string;
|
||||
downloadUrl: string;
|
||||
user: string;
|
||||
key: string;
|
||||
version: string;
|
||||
};
|
||||
export type Package = {
|
||||
id: string;
|
||||
name?: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
title?: string;
|
||||
user?: string;
|
||||
key?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
type InstallAppOpts = {
|
||||
appDir?: string;
|
||||
kevisualUrl?: string;
|
||||
/**
|
||||
* 是否是客户端, 下载到 assistant-config的下面
|
||||
*/
|
||||
};
|
||||
export const installApp = async (app: Package, opts: InstallAppOpts = {}) => {
|
||||
// const _app = demoData;
|
||||
const { appDir = '', kevisualUrl = 'https://kevisual.cn' } = opts;
|
||||
const _app = app;
|
||||
try {
|
||||
let files = _app.data.files || [];
|
||||
const version = _app.version;
|
||||
const user = _app.user;
|
||||
const key = _app.key;
|
||||
|
||||
const downFiles = files.map((file: any) => {
|
||||
const noVersionPath = file.path.replace(`/${version}`, '');
|
||||
return {
|
||||
...file,
|
||||
downloadPath: path.join(appDir, noVersionPath),
|
||||
downloadUrl: `${kevisualUrl}/${noVersionPath}`,
|
||||
};
|
||||
});
|
||||
const downloadTasks: DownloadTask[] = downFiles as any;
|
||||
for (const file of downloadTasks) {
|
||||
const downloadPath = file.downloadPath;
|
||||
const downloadUrl = file.downloadUrl;
|
||||
const dir = path.dirname(downloadPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
const res = await fetch(downloadUrl);
|
||||
const blob = await res.blob();
|
||||
fs.writeFileSync(downloadPath, Buffer.from(await blob.arrayBuffer()));
|
||||
}
|
||||
let indexHtml = files.find((file: any) => file.name === 'index.html');
|
||||
if (!indexHtml) {
|
||||
files.push({
|
||||
name: 'index.html',
|
||||
path: `${user}/${key}/index.html`,
|
||||
});
|
||||
fs.writeFileSync(path.join(appDir, `${user}/${key}/index.html`), JSON.stringify(app, null, 2));
|
||||
}
|
||||
_app.data.files = files;
|
||||
return {
|
||||
code: 200,
|
||||
data: _app,
|
||||
message: 'Install app success',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
code: 500,
|
||||
message: 'Install app failed',
|
||||
};
|
||||
}
|
||||
};
|
||||
export const checkAppDir = (appDir: string) => {
|
||||
const files = fs.readdirSync(appDir);
|
||||
if (files.length === 0) {
|
||||
fs.rmSync(appDir, { recursive: true });
|
||||
}
|
||||
};
|
||||
|
||||
type UninstallAppOpts = {
|
||||
appDir?: string;
|
||||
};
|
||||
export const uninstallApp = async (app: Partial<Package>, opts: UninstallAppOpts = {}) => {
|
||||
const { appDir = '' } = opts;
|
||||
try {
|
||||
const { user, key } = app;
|
||||
const keyDir = path.join(appDir, user, key);
|
||||
const parentDir = path.join(appDir, user);
|
||||
if (!checkFileExists(appDir) || !checkFileExists(keyDir)) {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'uninstall app success',
|
||||
};
|
||||
}
|
||||
try {
|
||||
// 删除appDir和文件
|
||||
fs.rmSync(keyDir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
checkAppDir(parentDir);
|
||||
return {
|
||||
code: 200,
|
||||
message: 'Uninstall app success',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
code: 500,
|
||||
message: 'Uninstall app failed',
|
||||
};
|
||||
}
|
||||
};
|
||||
0
assistant/src/module/index.ts
Normal file
0
assistant/src/module/index.ts
Normal file
@@ -1 +1,124 @@
|
||||
// app downlaod
|
||||
import { checkFileExists, AssistantConfig } from '@/module/assistant/index.ts';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import inquirer from 'inquirer';
|
||||
|
||||
import { spawnSync } from 'child_process';
|
||||
export const runCommand = (command: string, args: string[]) => {
|
||||
const result = spawnSync(command, args, {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
if (result.error) {
|
||||
console.error('Error executing command:', result.error);
|
||||
throw result.error;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
console.error('Command failed with status:', result.status);
|
||||
throw new Error(`Command failed with status: ${result.status}`);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
export type appType = 'web' | 'app';
|
||||
|
||||
type DownloadAppOptions = {
|
||||
/**
|
||||
* 应用名称
|
||||
* @type {string}
|
||||
* @example user/app
|
||||
* @description 应用名称
|
||||
*/
|
||||
id: string;
|
||||
type?: appType;
|
||||
registry?: string;
|
||||
/**
|
||||
* 应用名称,默认为 user/app的 app
|
||||
*/
|
||||
appName?: string;
|
||||
};
|
||||
type DeleteAppOptions = {
|
||||
/**
|
||||
* 应用名称
|
||||
* @type {string}
|
||||
* @example user/app
|
||||
* @description 应用名称
|
||||
*/
|
||||
id: string;
|
||||
type?: appType;
|
||||
appName?: string;
|
||||
};
|
||||
export class AppDownload {
|
||||
config: AssistantConfig;
|
||||
constructor(config: AssistantConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
async downloadApp(opts: DownloadAppOptions) {
|
||||
const { id, type = 'web' } = opts;
|
||||
const configDir = this.config.configDir;
|
||||
this.config?.checkMounted();
|
||||
const appsDir = this.config.configPath?.appsDir;
|
||||
const pageDir = this.config.configPath?.pageDir;
|
||||
if (!id) {
|
||||
throw new Error('应用名称不能为空');
|
||||
}
|
||||
const command = 'ev';
|
||||
const args = ['app', 'download'];
|
||||
args.push('-i', id);
|
||||
if (type) {
|
||||
args.push('-t', type);
|
||||
}
|
||||
const appName = opts?.appName || id.split('/').pop();
|
||||
if (type === 'web') {
|
||||
args.push('-o', pageDir);
|
||||
} else if (type === 'app') {
|
||||
args.push('-o', path.join(appsDir, appName));
|
||||
} else {
|
||||
throw new Error('应用类型错误,只能是 web 或 app');
|
||||
}
|
||||
if (opts.registry) {
|
||||
args.push('-r', opts.registry);
|
||||
}
|
||||
return runCommand(command, args);
|
||||
}
|
||||
async confirm(message?: string) {
|
||||
const { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: message || '是否继续删除应用?',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
return confirm;
|
||||
}
|
||||
async deleteApp(opts: DeleteAppOptions) {
|
||||
const { id, type = 'web' } = opts;
|
||||
const appName = opts?.appName || id.split('/').pop();
|
||||
this.config?.checkMounted();
|
||||
const appsDir = this.config.configPath?.appsDir;
|
||||
const pageDir = this.config.configPath?.pageDir;
|
||||
if (!id) {
|
||||
throw new Error('应用名称不能为空');
|
||||
}
|
||||
let deletePath = '';
|
||||
let isDelete = false;
|
||||
if (type === 'web') {
|
||||
// 直接删除路径就行
|
||||
const pagePath = path.join(pageDir, id);
|
||||
deletePath = pagePath;
|
||||
} else if (type === 'app') {
|
||||
const appPath = path.join(appsDir, appName);
|
||||
deletePath = appPath;
|
||||
}
|
||||
if (deletePath && checkFileExists(deletePath)) {
|
||||
const confirm = await this.confirm(`是否删除 ${deletePath} 应用?`);
|
||||
if (!confirm) {
|
||||
console.log('取消删除应用');
|
||||
return;
|
||||
}
|
||||
fs.rmSync(deletePath, { recursive: true });
|
||||
isDelete = true;
|
||||
console.log(`删除应用成功: ${deletePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { checkFileExists, AssistantConfig } from '@/module/assistant/index.ts';
|
||||
import { checkFileExists, AssistantConfig, AssistantConfigData } from '@/module/assistant/index.ts';
|
||||
import { chalk } from '@/module/chalk.ts';
|
||||
import { HttpsPem } from '@/module/assistant/https/sign.ts';
|
||||
export type AssistantInitOptions = {
|
||||
@@ -66,13 +66,16 @@ export class AssistantInit extends AssistantConfig {
|
||||
const assistantPath = this.configPath?.configPath;
|
||||
// 创建助手配置文件 assistant-config.json
|
||||
if (!checkFileExists(assistantPath, true)) {
|
||||
this.setConfig({
|
||||
description: '助手配置文件',
|
||||
home: '/root/center',
|
||||
proxy: [],
|
||||
apiProxyList: [],
|
||||
});
|
||||
this.setConfig(this.getDefaultInitAssistantConfig());
|
||||
console.log(chalk.green('助手配置文件assistant-config.json创建成功'));
|
||||
}
|
||||
}
|
||||
protected getDefaultInitAssistantConfig() {
|
||||
return {
|
||||
description: '助手配置文件',
|
||||
home: '/root/center',
|
||||
proxy: [],
|
||||
apiProxyList: [],
|
||||
} as AssistantConfigData;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user