Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions agent/app/api/v2/toolbox_installer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package v2

import (
"errors"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"

"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)

// validToolKey matches only lowercase alphanumeric characters and hyphens.
var validToolKey = regexp.MustCompile(`^[a-z0-9-]+$`)

// scriptMap maps tool keys to their ci/ script filenames.
var installerScriptMap = map[string]string{
"claude-code": "install_ai_tools.sh",
"codex-cli": "install_ai_tools.sh",
"odoo": "install_odoo.sh",
"shopify": "install_shopify.sh",
"contentful": "install_contentful.sh",
"strapi": "install_strapi.sh",
"react-bricks": "install_react_bricks.sh",
}

// installerCheckCmd maps tool keys to commands that check if the tool is installed.
var installerCheckCmd = map[string][]string{
"claude-code": {"claude", "--version"},
"codex-cli": {"codex", "--version"},
"odoo": {"docker", "inspect", "--format={{.State.Status}}", "odoo"},
"shopify": {"shopify", "version"},
"contentful": {"contentful", "--version"},
"strapi": {"node", "-e", "require('@strapi/strapi')"},
"react-bricks": {"node", "-e", "require('react-bricks')"},
}

// getCIDir returns the absolute path to the ci/ directory relative to the binary.
func getCIDir() string {
exe, err := os.Executable()
if err != nil {
return "/opt/1panel/ci"
}
return filepath.Join(filepath.Dir(exe), "ci")
}

// InstallTool runs the installer script for the requested tool and returns combined stdout/stderr.
// POST /api/v2/toolbox/installer/install
func (b *BaseApi) InstallTool(c *gin.Context) {
var req dto.InstallerToolReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}

if !validToolKey.MatchString(req.Tool) {
helper.BadRequest(c, errors.New("invalid tool key"))
return
}

scriptFile, ok := installerScriptMap[req.Tool]
if !ok {
helper.BadRequest(c, errors.New("unknown tool: "+req.Tool))
return
}

if runtime.GOOS == "windows" {
helper.BadRequest(c, errors.New("toolbox installers require Linux/macOS"))
return
}

ciDir := getCIDir()
scriptPath := filepath.Join(ciDir, scriptFile)

if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
helper.BadRequest(c, errors.New("installer script not found at "+scriptPath))
return
}

cmd := exec.CommandContext(c.Request.Context(), "bash", scriptPath)
cmd.Env = append(os.Environ(), "INSTALL_TOOL="+req.Tool)

out, err := cmd.CombinedOutput()
res := dto.InstallerToolRes{Output: strings.TrimSpace(string(out))}
if err != nil {
res.Error = err.Error()
helper.SuccessWithData(c, res)
return
}
helper.SuccessWithData(c, res)
}

// GetToolInstallStatus checks whether a tool's binary / container is present.
// GET /api/v2/toolbox/installer/status/:tool
func (b *BaseApi) GetToolInstallStatus(c *gin.Context) {
tool := c.Param("tool")

if !validToolKey.MatchString(tool) {
helper.BadRequest(c, errors.New("invalid tool key"))
return
}

args, ok := installerCheckCmd[tool]
if !ok {
helper.BadRequest(c, errors.New("unknown tool: "+tool))
return
}

out, err := exec.Command(args[0], args[1:]...).CombinedOutput() //nolint:gosec
if err != nil {
helper.SuccessWithData(c, dto.InstallerToolStatus{Installed: false})
return
}
helper.SuccessWithData(c, dto.InstallerToolStatus{
Installed: true,
Version: strings.TrimSpace(string(out)),
})
}
15 changes: 15 additions & 0 deletions agent/app/dto/toolbox_installer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dto

type InstallerToolReq struct {
Tool string `json:"tool" binding:"required"`
}

type InstallerToolRes struct {
Output string `json:"output"`
Error string `json:"error,omitempty"`
}

type InstallerToolStatus struct {
Installed bool `json:"installed"`
Version string `json:"version,omitempty"`
}
200 changes: 93 additions & 107 deletions agent/go.sum

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions agent/router/ro_toolbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,8 @@ func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) {
toolboxRouter.POST("/clam/status/update", baseApi.UpdateClamStatus)
toolboxRouter.POST("/clam/del", baseApi.DeleteClam)
toolboxRouter.POST("/clam/handle", baseApi.HandleClamScan)

toolboxRouter.POST("/installer/install", baseApi.InstallTool)
toolboxRouter.GET("/installer/status/:tool", baseApi.GetToolInstallStatus)
}
}
23 changes: 23 additions & 0 deletions frontend/src/api/modules/toolbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,26 @@ export const deleteClam = (params: { ids: number[]; removeInfected: boolean }) =
export const handleClamScan = (id: number) => {
return http.post(`/toolbox/clam/handle`, { id: id });
};

// installer
export interface InstallerToolReq {
tool: string;
}

export interface InstallerToolRes {
output: string;
error?: string;
}

export interface ToolInstallStatus {
installed: boolean;
version?: string;
}

export const installTool = (req: InstallerToolReq) => {
return http.post<InstallerToolRes>(`/toolbox/installer/install`, req, TimeoutEnum.T_10M);
};

export const getToolInstallStatus = (tool: string) => {
return http.get<ToolInstallStatus>(`/toolbox/installer/status/${tool}`);
};
16 changes: 16 additions & 0 deletions frontend/src/lang/modules/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ const message = {
terminal: 'Terminal | Terminals',
settings: 'Settings',
toolbox: 'Toolbox',
toolboxInstallers: 'Tool Installers',
logs: 'Log | Logs',
runtime: 'Runtime | Runtimes',
processManage: 'Process | Processes',
Expand Down Expand Up @@ -1545,6 +1546,21 @@ const message = {
alertHelper: 'Professional version supports scheduled scan and SMS alert',
alertTitle: 'Virus scan task 「{0}」 detected infected file alert',
},
installers: {
installed: 'Installed',
notInstalled: 'Not Installed',
install: 'Install',
viewLog: 'View Log',
installLog: 'Install Log',
installSuccess: 'installed successfully',
claudeCodeDesc: 'Anthropic Claude Code CLI — AI-powered coding assistant in your terminal.',
codexCliDesc: 'OpenAI Codex CLI — natural language to code in your terminal.',
odooDesc: 'Odoo ERP — open-source business apps (CRM, inventory, accounting) via Docker.',
shopifyDesc: 'Shopify CLI — build and deploy Shopify apps and themes from the command line.',
contentfulDesc: 'Contentful CLI — manage Contentful spaces, content models, and migrations.',
strapiDesc: 'Strapi CMS — open-source headless CMS with admin panel and REST/GraphQL API.',
reactBricksDesc: 'React Bricks CMS — visual headless CMS for React, Next.js, and Remix.',
},
},
logs: {
core: 'Panel Service',
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/lang/modules/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ const message = {
terminal: '终端',
settings: '面板设置',
toolbox: '工具箱',
toolboxInstallers: '工具安装器',
logs: '日志审计',
runtime: '运行环境',
processManage: '进程管理',
Expand Down Expand Up @@ -1456,6 +1457,21 @@ const message = {
alertHelper: '专业版支持定时扫描和短信告警功能',
alertTitle: '病毒扫描「 {0} 」任务检测到感染文件告警',
},
installers: {
installed: '已安装',
notInstalled: '未安装',
install: '安装',
viewLog: '查看日志',
installLog: '安装日志',
installSuccess: '安装成功',
claudeCodeDesc: 'Anthropic Claude Code CLI — 终端中的 AI 编程助手。',
codexCliDesc: 'OpenAI Codex CLI — 在终端中将自然语言转换为代码。',
odooDesc: 'Odoo ERP — 通过 Docker 运行的开源企业应用(CRM、库存、财务等)。',
shopifyDesc: 'Shopify CLI — 通过命令行构建和部署 Shopify 应用及主题。',
contentfulDesc: 'Contentful CLI — 管理 Contentful 空间、内容模型和数据迁移。',
strapiDesc: 'Strapi CMS — 带有管理面板和 REST/GraphQL API 的开源无头 CMS。',
reactBricksDesc: 'React Bricks CMS — 面向 React、Next.js 和 Remix 的可视化无头 CMS。',
},
},
logs: {
core: '面板服务',
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/routers/modules/toolbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ const toolboxRouter = {
requiresAuth: false,
},
},
{
path: 'installers',
name: 'ToolboxInstallers',
component: () => import('@/views/toolbox/installers/index.vue'),
hidden: true,
meta: {
parent: 'menu.toolbox',
title: 'menu.toolboxInstallers',
activeMenu: '/toolbox',
requiresAuth: false,
},
},
],
},
],
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/views/toolbox/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,9 @@ const buttons = [
label: 'Fail2ban',
path: '/toolbox/fail2ban',
},
{
label: i18n.global.t('menu.toolboxInstallers'),
path: '/toolbox/installers',
},
];
</script>
Loading