fixsess and logic for web ide
ci-agent / build (push) Failing after 2m42s

This commit is contained in:
d3m0k1d
2026-04-04 23:56:28 +03:00
parent 1e4e65bb84
commit b516a54c17
17 changed files with 1792 additions and 113 deletions
+6
View File
@@ -50,6 +50,11 @@ func (e *Executor) WorkDir() string {
return e.workDir
}
// GRPCURL returns the gRPC server URL (host:port)
func (e *Executor) GRPCURL() string {
return e.grpcServerHost + ":" + e.grpcServerPort
}
// Deploy runs Ansible playbook for the given inventory
func (e *Executor) Deploy(
ctx context.Context,
@@ -66,6 +71,7 @@ func (e *Executor) Deploy(
cmd := exec.CommandContext(ctx, "ansible-playbook",
"-i", inventoryPath,
"-e", fmt.Sprintf("backend_url=%s", e.backendURL),
"-e", fmt.Sprintf("grpc_url=%s", e.grpcServerHost+":"+e.grpcServerPort),
playbookPath,
)
+2
View File
@@ -18,6 +18,7 @@ type InventoryHost struct {
Password string
DeployType string
Token string
GRPCURL string
}
// Inventory represents an Ansible inventory file
@@ -32,6 +33,7 @@ const inventoryTemplateText = `{{ range .Hosts }}
deploy_type={{ .DeployType }}
agent_token={{ .Token }}
agent_label={{ .Name }}
grpc_url={{ .GRPCURL }}
{{ end }}`
+23 -33
View File
@@ -1,6 +1,8 @@
package ansible
// BinaryDeployPlaybook returns the Ansible playbook for binary deployment
// BinaryDeployPlaybook returns the Ansible playbook for binary deployment.
// Downloads the agent binary, writes config, and starts it directly (no systemd).
// systemd unit is managed separately (e.g. via goreleaser .deb/.rpm packages).
const BinaryDeployPlaybook = `---
- name: Deploy HellreigN Agent (Binary)
hosts: all
@@ -11,7 +13,6 @@ const BinaryDeployPlaybook = `---
backend_url: "{{ backend_url }}"
install_dir: /opt/hellreign
bin_name: hellreign-agent
service_name: hellreign-agent
cert_dir: "{{ install_dir }}/certs"
tasks:
@@ -37,45 +38,29 @@ const BinaryDeployPlaybook = `---
copy:
content: |
backend_url: "{{ backend_url }}"
grpc_url: "{{ grpc_url | default('localhost:9001') }}"
label: "{{ agent_label }}"
registration_token: "{{ agent_token }}"
cert_dir: "{{ cert_dir }}"
services:
- name: system
type: journald
dest: "{{ install_dir }}/config.yml"
mode: '0644'
- name: Create systemd service file
copy:
content: |
[Unit]
Description=HellreigN Agent
After=network.target
[Service]
Type=simple
ExecStart={{ install_dir }}/{{ bin_name }}
Restart=always
RestartSec=5
Environment=CONFIG_FILE={{ install_dir }}/config.yml
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
dest: /etc/systemd/system/{{ service_name }}.service
mode: '0644'
- name: Reload systemd daemon
systemd:
daemon_reload: yes
- name: Enable and start HellreigN Agent service
systemd:
name: "{{ service_name }}"
enabled: yes
state: started
- name: Start HellreigN Agent
shell: |
nohup {{ install_dir }}/{{ bin_name }} > /dev/null 2>&1 &
echo $!
args:
executable: /bin/bash
environment:
CONFIG_FILE: "{{ install_dir }}/config.yml"
register: agent_pid
changed_when: true
`
// DockerDeployPlaybook returns the Ansible playbook for Docker deployment
// DockerDeployPlaybook returns the Ansible playbook for Docker deployment.
const DockerDeployPlaybook = `---
- name: Deploy HellreigN Agent (Docker)
hosts: all
@@ -84,6 +69,7 @@ const DockerDeployPlaybook = `---
agent_label: "{{ agent_label }}"
agent_token: "{{ agent_token }}"
backend_url: "{{ backend_url }}"
grpc_url: "{{ grpc_url | default('localhost:9001') }}"
container_name: hellreign-agent-{{ agent_label }}
image: "gitea.d3m0k1d.ru/d3m0k1d/hellreign-agent:latest"
cert_dir: /etc/hellreign-agent/certs
@@ -117,9 +103,13 @@ const DockerDeployPlaybook = `---
copy:
content: |
backend_url: "{{ backend_url }}"
grpc_url: "{{ grpc_url }}"
label: "{{ agent_label }}"
registration_token: "{{ agent_token }}"
cert_dir: "{{ cert_dir }}"
services:
- name: system
type: journald
dest: "{{ cert_dir }}/config.yml"
mode: '0644'
@@ -186,45 +186,3 @@ func (c *Collector) Agents() []*Agent {
}
return result
}
// ServicesStream handles the ServicesUpdate client-streaming RPC.
// Agents send service status updates which are stored in the collector.
// Returns a single response when the agent closes the stream.
func (c *Collector) ServicesStream(stream proto.Collector_ServicesStreamServer) error {
md, ok := metadata.FromIncomingContext(stream.Context())
if !ok {
return fmt.Errorf("no metadata in context")
}
whoamiVals := md["whoami"]
if len(whoamiVals) == 0 {
return fmt.Errorf("whoami metadata missing")
}
agentName := whoamiVals[0]
log.Printf("Agent %s started services update stream", agentName)
for {
update, err := stream.Recv()
if err == io.EOF {
log.Printf("Agent %s finished services update stream", agentName)
return stream.SendAndClose(&proto.ServicesUpdateResp{})
}
if err != nil {
return fmt.Errorf("failed to receive services update: %w", err)
}
c.mu.Lock()
if agent, ok := c.agents[agentName]; ok {
services := make([]string, 0, len(update.Services))
for _, s := range update.Services {
services = append(services, fmt.Sprintf("%s:%s", s.Name, s.Status))
}
agent.Services = services
log.Printf("Updated services for agent %s: %v", agentName, agent.Services)
} else {
log.Printf("Warning: received services update for unknown agent %s", agentName)
}
c.mu.Unlock()
}
}
@@ -60,6 +60,18 @@ func (self *Commander) GetAgent(aid string) (agent Agent, ok bool) {
return
}
// GetAgentByLabel searches for an agent by its human-readable label.
func (self *Commander) GetAgentByLabel(label string) (agent Agent, ok bool) {
self.mu.RLock()
defer self.mu.RUnlock()
for _, a := range self.agents {
if a.Label == label {
return a, true
}
}
return
}
func (self *Commander) Agents() []Agent {
self.mu.RLock()
defer self.mu.RUnlock()
@@ -117,6 +117,7 @@ func (adg *AgentDeployGroup) DeployAgents(c *gin.Context) {
Password: server.Password,
DeployType: string(server.DeployType),
Token: token,
GRPCURL: adg.executor.GRPCURL(),
},
}
+274
View File
@@ -0,0 +1,274 @@
package handlers
import (
"errors"
"fmt"
"net/http"
"strconv"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service"
"github.com/gin-gonic/gin"
)
// ScriptHandlersGroup handles script management routes.
type ScriptHandlersGroup struct {
svc *service.ScriptService
cmder *commander.Commander
}
// NewScriptHandlersGroup creates a new ScriptHandlersGroup.
func NewScriptHandlersGroup(svc *service.ScriptService, cmder *commander.Commander) *ScriptHandlersGroup {
return &ScriptHandlersGroup{svc: svc, cmder: cmder}
}
// GetTree returns the script directory tree.
// @Summary Get script directory tree
// @Description Returns a hierarchical tree of all scripts organized by their paths
// @Tags scripts
// @Produce json
// @Success 200 {array} repository.ScriptTreeNode
// @Security Bearer
// @Router /scripts/tree [get]
func (sh *ScriptHandlersGroup) GetTree(c *gin.Context) {
tree, err := sh.svc.BuildTree()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to build script tree"})
return
}
if tree == nil {
tree = []repository.ScriptTreeNode{}
}
c.JSON(http.StatusOK, tree)
}
// CreateScript creates a new script.
// @Summary Create script
// @Description Creates a new script with path, content, and interpreter binding
// @Tags scripts
// @Accept json
// @Produce json
// @Param body body repository.ScriptCreate true "Script data"
// @Success 201 {object} repository.Script
// @Security Bearer
// @Router /scripts [post]
func (sh *ScriptHandlersGroup) CreateScript(c *gin.Context) {
var req repository.ScriptCreate
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
script, err := sh.svc.Repo.CreateScript(req)
if err != nil {
if isUniqueConstraint(err) {
c.JSON(http.StatusConflict, gin.H{"error": "script with this path already exists"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create script"})
return
}
c.JSON(http.StatusCreated, script)
}
// GetScript returns a script by ID.
// @Summary Get script
// @Description Returns a script by its ID
// @Tags scripts
// @Produce json
// @Param id path int true "Script ID"
// @Success 200 {object} repository.Script
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Security Bearer
// @Router /scripts/:id [get]
func (sh *ScriptHandlersGroup) GetScript(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
script, err := sh.svc.Repo.GetScript(id)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get script"})
return
}
c.JSON(http.StatusOK, script)
}
// UpdateScript updates a script.
// @Summary Update script
// @Description Updates a script's path, content, or interpreter
// @Tags scripts
// @Accept json
// @Produce json
// @Param id path int true "Script ID"
// @Param body body repository.ScriptUpdate true "Script data"
// @Success 200 {object} repository.Script
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Security Bearer
// @Router /scripts/:id [put]
func (sh *ScriptHandlersGroup) UpdateScript(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req repository.ScriptUpdate
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
script, err := sh.svc.Repo.UpdateScript(id, req)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update script"})
return
}
c.JSON(http.StatusOK, script)
}
// DeleteScript deletes a script.
// @Summary Delete script
// @Description Deletes a script by its ID
// @Tags scripts
// @Param id path int true "Script ID"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Security Bearer
// @Router /scripts/:id [delete]
func (sh *ScriptHandlersGroup) DeleteScript(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
if err := sh.svc.Repo.DeleteScript(id); err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete script"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "script deleted"})
}
// RunScriptByID executes a stored script on a target agent.
// @Summary Run script by ID
// @Description Loads a script from storage, resolves interpreter command, and executes on the specified agent
// @Tags scripts
// @Accept json
// @Produce json
// @Param id path int true "Script ID"
// @Param body body RunStoredScriptIn true "Agent token and optional stdin"
// @Success 201 {object} RunScriptOut
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security Bearer
// @Router /scripts/:id/run [post]
func (sh *ScriptHandlersGroup) RunScriptByID(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var in RunStoredScriptIn
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
script, err := sh.svc.Repo.GetScript(id)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "script not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get script"})
return
}
command, err := sh.svc.ResolveCommand(c.Request.Context(), script.InterpreterID, script.Content)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to resolve command: %v", err)})
return
}
agent, ok := sh.cmder.GetAgent(in.Token)
if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
return
}
jid, err := agent.AddJob(models.JobForInsert{
Command: command,
Stdin: in.Stdin,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to add job: %v", err)})
return
}
job, err := agent.WaitJob(jid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("job execution failed: %v", err)})
return
}
c.JSON(http.StatusCreated, RunScriptOut{
ID: job.ID,
Command: job.Command,
Stdin: job.Stdin,
Stdout: job.Stdout,
Stderr: job.Stderr,
Status: job.Status,
})
}
// RunStoredScriptIn is the request body for running a stored script on an agent.
type RunStoredScriptIn struct {
Token string `json:"token" binding:"required"`
Stdin *string `json:"stdin"`
}
// isUniqueConstraint checks if the error is a SQLite UNIQUE constraint violation.
func isUniqueConstraint(err error) bool {
return err != nil && (err.Error() != "" && contains(err.Error(), "UNIQUE constraint"))
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && searchSubstring(s, substr)
}
func searchSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
+34
View File
@@ -149,3 +149,37 @@ type DeployResult struct {
Success bool `json:"success" example:"true" description:"Whether deployment succeeded"`
Error string `json:"error,omitempty" example:"" description:"Error message if deployment failed"`
}
// Script represents a stored script with path and interpreter binding.
type Script struct {
ID int64 `json:"id"`
Path string `json:"path"`
Content string `json:"content"`
InterpreterID int64 `json:"interpreter_id"`
CreatedAt *string `json:"created_at"`
UpdatedAt *string `json:"updated_at"`
}
// ScriptCreate is the request body for creating a script.
type ScriptCreate struct {
Path string `json:"path" binding:"required"`
Content string `json:"content"`
InterpreterID int64 `json:"interpreter_id" binding:"required"`
}
// ScriptUpdate is the request body for updating a script.
type ScriptUpdate struct {
Path *string `json:"path"`
Content *string `json:"content"`
InterpreterID *int64 `json:"interpreter_id"`
}
// ScriptTreeNode represents a node in the script directory tree.
type ScriptTreeNode struct {
Name string `json:"name"`
Type string `json:"type"` // "folder" or "file"
Children []ScriptTreeNode `json:"children,omitempty"`
ID *int64 `json:"id,omitempty"`
Content *string `json:"content,omitempty"`
InterpreterID *int64 `json:"interpreter_id,omitempty"`
}
+131
View File
@@ -507,3 +507,134 @@ func (r *Repository) UpdatePassword(login string, newPassword string) error {
}
return nil
}
// CreateScript inserts a new script into the database.
func (r *Repository) CreateScript(sc ScriptCreate) (*Script, error) {
result, err := r.DB.Exec(
`INSERT INTO scripts (path, content, interpreter_id) VALUES (?, ?, ?)`,
sc.Path, sc.Content, sc.InterpreterID,
)
if err != nil {
return nil, fmt.Errorf("insert script: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return nil, fmt.Errorf("get last insert id: %w", err)
}
return &Script{
ID: id,
Path: sc.Path,
Content: sc.Content,
InterpreterID: sc.InterpreterID,
}, nil
}
// GetScript retrieves a script by ID.
func (r *Repository) GetScript(id int64) (*Script, error) {
var s Script
err := r.DB.QueryRow(
`SELECT id, path, content, interpreter_id, created_at, updated_at FROM scripts WHERE id = ?`,
id,
).Scan(&s.ID, &s.Path, &s.Content, &s.InterpreterID, &s.CreatedAt, &s.UpdatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return &s, nil
}
// GetScriptByPath retrieves a script by its path.
func (r *Repository) GetScriptByPath(path string) (*Script, error) {
var s Script
err := r.DB.QueryRow(
`SELECT id, path, content, interpreter_id, created_at, updated_at FROM scripts WHERE path = ?`,
path,
).Scan(&s.ID, &s.Path, &s.Content, &s.InterpreterID, &s.CreatedAt, &s.UpdatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return &s, nil
}
// ListScripts returns all scripts.
func (r *Repository) ListScripts() ([]Script, error) {
rows, err := r.DB.Query(
`SELECT id, path, content, interpreter_id, created_at, updated_at FROM scripts`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var scripts []Script
for rows.Next() {
var s Script
if err := rows.Scan(&s.ID, &s.Path, &s.Content, &s.InterpreterID, &s.CreatedAt, &s.UpdatedAt); err != nil {
return nil, err
}
scripts = append(scripts, s)
}
return scripts, rows.Err()
}
// UpdateScript updates a script by ID.
func (r *Repository) UpdateScript(id int64, update ScriptUpdate) (*Script, error) {
existing, err := r.GetScript(id)
if err != nil {
return nil, err
}
newPath := existing.Path
newContent := existing.Content
newInterpreterID := existing.InterpreterID
if update.Path != nil {
newPath = *update.Path
}
if update.Content != nil {
newContent = *update.Content
}
if update.InterpreterID != nil {
newInterpreterID = *update.InterpreterID
}
_, err = r.DB.Exec(
`UPDATE scripts SET path = ?, content = ?, interpreter_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
newPath, newContent, newInterpreterID, id,
)
if err != nil {
return nil, fmt.Errorf("update script: %w", err)
}
return &Script{
ID: id,
Path: newPath,
Content: newContent,
InterpreterID: newInterpreterID,
}, nil
}
// DeleteScript deletes a script by ID.
func (r *Repository) DeleteScript(id int64) error {
result, err := r.DB.Exec(`DELETE FROM scripts WHERE id = ?`, id)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return ErrNotFound
}
return nil
}
+141 -37
View File
@@ -3,66 +3,170 @@ package service
import (
"context"
"fmt"
"sort"
"strings"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
)
// ScriptService handles script CRUD, tree building, and interpreter resolution.
type ScriptService struct {
repo *repository.ScriptInterpreterRepo
Repo *repository.Repository
InterpreterRepo *repository.ScriptInterpreterRepo
}
func NewScriptService(repo *repository.ScriptInterpreterRepo) *ScriptService {
return &ScriptService{repo: repo}
// NewScriptService creates a new ScriptService with both script and interpreter repos.
func NewScriptService(repo *repository.Repository) *ScriptService {
return &ScriptService{Repo: repo}
}
// ResolveCommand builds the full argv[] by prepending the interpreter's argv
// to the script text (as the last argument).
func (self *ScriptService) ResolveCommand(
ctx context.Context,
interpreterID int64,
scriptText string,
) ([]string, error) {
interpreter, err := self.repo.GetByID(ctx, interpreterID)
// NewScriptServiceWithInterpreters creates a ScriptService with interpreter support.
func NewScriptServiceWithInterpreters(repo *repository.Repository, interpRepo *repository.ScriptInterpreterRepo) *ScriptService {
return &ScriptService{Repo: repo, InterpreterRepo: interpRepo}
}
// treeNode is an internal representation for building the tree.
type treeNode struct {
name string
typ string // "folder" or "file"
children map[string]*treeNode
// File-specific fields
id *int64
content *string
interpreterID *int64
}
// BuildTree builds a directory tree from all scripts in the database.
// Each script path is treated as a file path (e.g. "deploy/nginx/restart.sh").
func (s *ScriptService) BuildTree() ([]repository.ScriptTreeNode, error) {
scripts, err := s.Repo.ListScripts()
if err != nil {
return nil, err
}
if len(interpreter.Argv) == 0 {
return nil, fmt.Errorf("interpreter %q has empty argv", interpreter.Name)
root := make(map[string]*treeNode)
for _, sc := range scripts {
parts := strings.Split(sc.Path, "/")
// Walk through path parts, creating folders as needed
currentMap := root
for i, part := range parts {
isFile := i == len(parts)-1
if _, exists := currentMap[part]; !exists {
node := &treeNode{
name: part,
children: make(map[string]*treeNode),
}
if isFile {
node.typ = "file"
id := sc.ID
content := sc.Content
interpreterID := sc.InterpreterID
node.id = &id
node.content = &content
node.interpreterID = &interpreterID
} else {
node.typ = "folder"
}
currentMap[part] = node
}
currentMap = currentMap[part].children
}
}
argv := make([]string, len(interpreter.Argv)+1)
copy(argv, interpreter.Argv)
argv[len(argv)-1] = scriptText
return argv, nil
return buildTreeSlice(root), nil
}
func (self *ScriptService) Create(
ctx context.Context,
in repository.ScriptInterpreterCreate,
) (*repository.ScriptInterpreter, error) {
return self.repo.Create(ctx, in)
// buildTreeSlice converts a map of treeNodes to a sorted slice of ScriptTreeNode.
func buildTreeSlice(m map[string]*treeNode) []repository.ScriptTreeNode {
result := make([]repository.ScriptTreeNode, 0, len(m))
for _, node := range m {
result = append(result, toScriptTreeNode(node))
}
// Sort: folders first, then files, alphabetically within each group
sort.Slice(result, func(i, j int) bool {
if result[i].Type != result[j].Type {
return result[i].Type == "folder"
}
return result[i].Name < result[j].Name
})
return result
}
func (self *ScriptService) GetByID(
ctx context.Context,
id int64,
) (*repository.ScriptInterpreter, error) {
return self.repo.GetByID(ctx, id)
// toScriptTreeNode converts a treeNode to a ScriptTreeNode with recursively converted children.
func toScriptTreeNode(node *treeNode) repository.ScriptTreeNode {
result := repository.ScriptTreeNode{
Name: node.name,
Type: node.typ,
Children: []repository.ScriptTreeNode{},
}
if node.typ == "file" {
result.ID = node.id
result.Content = node.content
result.InterpreterID = node.interpreterID
} else {
result.Children = buildTreeSlice(node.children)
}
return result
}
func (self *ScriptService) List(ctx context.Context) ([]repository.ScriptInterpreter, error) {
return self.repo.List(ctx)
// ResolveCommand resolves the full command for a script using its interpreter.
func (s *ScriptService) ResolveCommand(ctx context.Context, interpreterID int64, scriptText string) ([]string, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
interpreter, err := s.InterpreterRepo.GetByID(ctx, interpreterID)
if err != nil {
return nil, fmt.Errorf("get interpreter: %w", err)
}
// Build command: argv[0] argv[1] ... -c scriptText
cmd := append(interpreter.Argv, "-c", scriptText)
return cmd, nil
}
func (self *ScriptService) Update(
ctx context.Context,
id int64,
in repository.ScriptInterpreterUpdate,
) (*repository.ScriptInterpreter, error) {
return self.repo.Update(ctx, id, in)
// List returns all interpreters.
func (s *ScriptService) List(ctx context.Context) ([]repository.ScriptInterpreter, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.List(ctx)
}
func (self *ScriptService) Delete(ctx context.Context, id int64) error {
return self.repo.Delete(ctx, id)
// Create creates a new interpreter.
func (s *ScriptService) Create(ctx context.Context, in repository.ScriptInterpreterCreate) (*repository.ScriptInterpreter, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.Create(ctx, in)
}
// GetByID returns an interpreter by ID.
func (s *ScriptService) GetByID(ctx context.Context, id int64) (*repository.ScriptInterpreter, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.GetByID(ctx, id)
}
// Update updates an interpreter.
func (s *ScriptService) Update(ctx context.Context, id int64, in repository.ScriptInterpreterUpdate) (*repository.ScriptInterpreter, error) {
if s.InterpreterRepo == nil {
return nil, fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.Update(ctx, id, in)
}
// Delete deletes an interpreter.
func (s *ScriptService) Delete(ctx context.Context, id int64) error {
if s.InterpreterRepo == nil {
return fmt.Errorf("interpreter repo not configured")
}
return s.InterpreterRepo.Delete(ctx, id)
}
+12
View File
@@ -57,6 +57,18 @@ CREATE TABLE IF NOT EXISTS script_interpreters (
);
`
const CreateScriptsTable = `
CREATE TABLE IF NOT EXISTS scripts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL UNIQUE,
content TEXT NOT NULL DEFAULT '',
interpreter_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (interpreter_id) REFERENCES script_interpreters(id)
);
`
const CreateLogsTable = `
CREATE TABLE IF NOT EXISTS logs (
timestamp DateTime64(3) DEFAULT now(),
+5
View File
@@ -44,5 +44,10 @@ func Open(path string) (*sql.DB, error) {
log.Println("[sqlite] is_active column migration applied")
}
// Create scripts table if not exists
if _, err := db.Exec(CreateScriptsTable); err != nil {
return nil, fmt.Errorf("migrate scripts: %w", err)
}
return db, nil
}