dify/dify-agent-runtime/internal/agentcli/config.go
Yunlu Wen 302d7b1e1b
feat(agent): shellctl rewritten in go (#38841)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-15 01:58:36 +00:00

499 lines
12 KiB
Go

package agentcli
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
)
const defaultConfigBase = ".dify_conf"
// RunConfigManifest executes the `config manifest` command.
func RunConfigManifest(env *Environment) error {
client, err := NewStubClient(env)
if err != nil {
return err
}
defer func() { _ = client.Close() }()
body, err := client.GetConfigManifest(context.Background())
if err != nil {
return err
}
fmt.Println(string(body))
return nil
}
// RunConfigSkillsPull executes the `config skills pull` command.
func RunConfigSkillsPull(env *Environment, names []string, localDir string, jsonOutput bool) error {
client, err := NewStubClient(env)
if err != nil {
return err
}
defer func() { _ = client.Close() }()
ctx := context.Background()
// Get manifest to find available skills if no names specified
if len(names) == 0 {
body, err := client.GetConfigManifest(ctx)
if err != nil {
return err
}
var manifest struct {
Skills struct {
Items []struct {
Name string `json:"name"`
} `json:"items"`
} `json:"skills"`
}
if err := json.Unmarshal(body, &manifest); err != nil {
return fmt.Errorf("parse manifest: %w", err)
}
for _, item := range manifest.Skills.Items {
names = append(names, item.Name)
}
}
targetDir := localDir
if targetDir == "" {
targetDir = filepath.Join(defaultConfigBase, "skills")
}
if err := os.MkdirAll(targetDir, 0o755); err != nil {
return fmt.Errorf("create target directory: %w", err)
}
type pullItem struct {
Name string `json:"name"`
ArchivePath string `json:"archive_path"`
DirectoryPath string `json:"directory_path"`
SkillMD string `json:"skill_md"`
}
var items []pullItem
for _, name := range names {
archiveBytes, err := client.PullConfigSkill(ctx, name)
if err != nil {
return err
}
archivePath := filepath.Join(targetDir, name+".zip")
skillDir := filepath.Join(targetDir, name)
if err := os.MkdirAll(skillDir, 0o755); err != nil {
return fmt.Errorf("create skill directory: %w", err)
}
if err := os.WriteFile(archivePath, archiveBytes, 0o644); err != nil {
return fmt.Errorf("write archive: %w", err)
}
// Extract archive
if err := extractZipArchive(archivePath, skillDir); err != nil {
return fmt.Errorf("extract skill archive: %w", err)
}
skillMDPath := filepath.Join(skillDir, "SKILL.md")
skillMD := ""
if data, err := os.ReadFile(skillMDPath); err == nil {
skillMD = string(data)
}
items = append(items, pullItem{
Name: name,
ArchivePath: archivePath,
DirectoryPath: skillDir,
SkillMD: skillMD,
})
}
if jsonOutput {
out, _ := json.Marshal(map[string]any{"items": items})
fmt.Println(string(out))
return nil
}
for i, item := range items {
if i > 0 {
fmt.Println()
}
fmt.Println(item.DirectoryPath)
fmt.Print(item.SkillMD)
}
return nil
}
// RunConfigFilesPull executes the `config files pull` command.
func RunConfigFilesPull(env *Environment, names []string, localDir string, jsonOutput bool) error {
client, err := NewStubClient(env)
if err != nil {
return err
}
defer func() { _ = client.Close() }()
ctx := context.Background()
if len(names) == 0 {
body, err := client.GetConfigManifest(ctx)
if err != nil {
return err
}
var manifest struct {
Files struct {
Items []struct {
Name string `json:"name"`
} `json:"items"`
} `json:"files"`
}
if err := json.Unmarshal(body, &manifest); err != nil {
return fmt.Errorf("parse manifest: %w", err)
}
for _, item := range manifest.Files.Items {
names = append(names, item.Name)
}
}
targetDir := localDir
if targetDir == "" {
targetDir = filepath.Join(defaultConfigBase, "files")
}
if err := os.MkdirAll(targetDir, 0o755); err != nil {
return fmt.Errorf("create target directory: %w", err)
}
type fileItem struct {
Name string `json:"name"`
Path string `json:"path"`
}
var items []fileItem
for _, name := range names {
payload, err := client.PullConfigFile(ctx, name)
if err != nil {
return err
}
targetPath := filepath.Join(targetDir, name)
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
return fmt.Errorf("create parent directory: %w", err)
}
if err := os.WriteFile(targetPath, payload, 0o644); err != nil {
return fmt.Errorf("write file: %w", err)
}
items = append(items, fileItem{Name: name, Path: targetPath})
}
if jsonOutput {
out, _ := json.Marshal(map[string]any{"items": items})
fmt.Println(string(out))
return nil
}
for _, item := range items {
fmt.Println(item.Path)
}
return nil
}
// RunConfigSkillsPush executes the `config skills push` command.
func RunConfigSkillsPush(env *Environment, paths []string) error {
if len(paths) == 0 {
return fmt.Errorf("at least one skill directory is required")
}
client, err := NewStubClient(env)
if err != nil {
return err
}
defer func() { _ = client.Close() }()
type skillPushItem struct {
Name string `json:"name"`
FileRef *DriveFileRef `json:"file_ref"`
}
var skills []skillPushItem
for _, path := range paths {
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("resolve path: %w", err)
}
info, err := os.Stat(absPath)
if err != nil || !info.IsDir() {
return fmt.Errorf("config skill path must be a directory: %s", absPath)
}
skillMDPath := filepath.Join(absPath, "SKILL.md")
if _, err := os.Stat(skillMDPath); os.IsNotExist(err) {
return fmt.Errorf("config skill directory must contain SKILL.md: %s", absPath)
}
// Build archive and upload
archivePath, err := buildSkillArchive(absPath)
if err != nil {
return err
}
defer func() { _ = os.Remove(archivePath) }()
name := filepath.Base(absPath)
commitItem, err := uploadAndPrepareCommitItem(client, archivePath, name)
if err != nil {
return err
}
skills = append(skills, skillPushItem{
Name: name,
FileRef: &DriveFileRef{Kind: commitItem.FileRef.Kind, ID: commitItem.FileRef.ID},
})
}
payload := map[string]any{
"skills": skills,
"files": []any{},
}
body, err := client.PushConfig(context.Background(), payload)
if err != nil {
return err
}
fmt.Println(string(body))
return nil
}
// RunConfigFilesPush executes the `config files push` command.
func RunConfigFilesPush(env *Environment, paths []string) error {
if len(paths) == 0 {
return fmt.Errorf("at least one file path is required")
}
client, err := NewStubClient(env)
if err != nil {
return err
}
defer func() { _ = client.Close() }()
type filePushItem struct {
Name string `json:"name"`
FileRef *DriveFileRef `json:"file_ref"`
}
var files []filePushItem
for _, path := range paths {
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("resolve path: %w", err)
}
info, err := os.Stat(absPath)
if err != nil || info.IsDir() {
return fmt.Errorf("config file path must be a regular file: %s", absPath)
}
name := filepath.Base(absPath)
commitItem, err := uploadAndPrepareCommitItem(client, absPath, name)
if err != nil {
return err
}
files = append(files, filePushItem{
Name: name,
FileRef: &DriveFileRef{Kind: commitItem.FileRef.Kind, ID: commitItem.FileRef.ID},
})
}
payload := map[string]any{
"files": files,
"skills": []any{},
}
body, err := client.PushConfig(context.Background(), payload)
if err != nil {
return err
}
fmt.Println(string(body))
return nil
}
// RunConfigSkillsDelete executes the `config skills delete` command.
func RunConfigSkillsDelete(env *Environment, names []string) error {
if len(names) == 0 {
return fmt.Errorf("at least one skill name is required")
}
type skillDeleteItem struct {
Name string `json:"name"`
FileRef *any `json:"file_ref"`
}
var skills []skillDeleteItem
for _, name := range names {
skills = append(skills, skillDeleteItem{Name: name, FileRef: nil})
}
client, err := NewStubClient(env)
if err != nil {
return err
}
defer func() { _ = client.Close() }()
payload := map[string]any{
"skills": skills,
"files": []any{},
}
body, err := client.PushConfig(context.Background(), payload)
if err != nil {
return err
}
fmt.Println(string(body))
return nil
}
// RunConfigFilesDelete executes the `config files delete` command.
func RunConfigFilesDelete(env *Environment, names []string) error {
if len(names) == 0 {
return fmt.Errorf("at least one file name is required")
}
type fileDeleteItem struct {
Name string `json:"name"`
FileRef *any `json:"file_ref"`
}
var files []fileDeleteItem
for _, name := range names {
files = append(files, fileDeleteItem{Name: name, FileRef: nil})
}
client, err := NewStubClient(env)
if err != nil {
return err
}
defer func() { _ = client.Close() }()
payload := map[string]any{
"files": files,
"skills": []any{},
}
body, err := client.PushConfig(context.Background(), payload)
if err != nil {
return err
}
fmt.Println(string(body))
return nil
}
// RunConfigEnvPush executes the `config env push` command.
func RunConfigEnvPush(env *Environment, localPath string) error {
var envText string
if localPath == "-" {
data, err := os.ReadFile("/dev/stdin")
if err != nil {
return fmt.Errorf("read stdin: %w", err)
}
envText = string(data)
} else {
absPath, err := filepath.Abs(localPath)
if err != nil {
return fmt.Errorf("resolve path: %w", err)
}
data, err := os.ReadFile(absPath)
if err != nil {
return fmt.Errorf("read file: %w", err)
}
envText = string(data)
}
client, err := NewStubClient(env)
if err != nil {
return err
}
defer func() { _ = client.Close() }()
body, err := client.PatchConfigEnv(context.Background(), envText)
if err != nil {
return err
}
fmt.Println(string(body))
return nil
}
// RunConfigNotePull executes the `config note pull` command.
func RunConfigNotePull(env *Environment, localPath string) error {
client, err := NewStubClient(env)
if err != nil {
return err
}
defer func() { _ = client.Close() }()
body, err := client.GetConfigManifest(context.Background())
if err != nil {
return err
}
var manifest struct {
Note string `json:"note"`
}
if err := json.Unmarshal(body, &manifest); err != nil {
return fmt.Errorf("parse manifest: %w", err)
}
targetPath := localPath
if targetPath == "" {
targetPath = filepath.Join(defaultConfigBase, "note.md")
}
absPath, err := filepath.Abs(targetPath)
if err != nil {
return fmt.Errorf("resolve path: %w", err)
}
if err := os.MkdirAll(filepath.Dir(absPath), 0o755); err != nil {
return fmt.Errorf("create directory: %w", err)
}
if err := os.WriteFile(absPath, []byte(manifest.Note), 0o644); err != nil {
return fmt.Errorf("write note: %w", err)
}
fmt.Println(absPath)
return nil
}
// RunConfigNotePush executes the `config note push` command.
func RunConfigNotePush(env *Environment, localPath string) error {
var note string
if localPath == "-" {
data, err := os.ReadFile("/dev/stdin")
if err != nil {
return fmt.Errorf("read stdin: %w", err)
}
note = string(data)
} else {
targetPath := localPath
if targetPath == "" {
targetPath = filepath.Join(defaultConfigBase, "note.md")
}
absPath, err := filepath.Abs(targetPath)
if err != nil {
return fmt.Errorf("resolve path: %w", err)
}
data, err := os.ReadFile(absPath)
if err != nil {
return fmt.Errorf("read file: %w", err)
}
note = string(data)
}
client, err := NewStubClient(env)
if err != nil {
return err
}
defer func() { _ = client.Close() }()
body, err := client.PutConfigNote(context.Background(), note)
if err != nil {
return err
}
fmt.Println(string(body))
return nil
}
// extractZipArchive extracts a zip file into targetDir.
func extractZipArchive(archivePath string, targetDir string) error {
return extractZip(archivePath, targetDir)
}