Skip to content

Commit b2d8fb1

Browse files
grokifyclaude
andcommitted
feat(generate): add unified Generate() function for deployment-driven plugin generation
- Fix loadAgents() to use agents.ReadCanonicalDir() supporting both .md and .json - Add GenerateResult type for unified generation results - Add Generate() function that orchestrates complete plugin generation from specs/ - Add generatePlatformPlugin() helper for per-platform plugin generation - Each deployment target now receives complete plugin (agents, commands, skills, manifest) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 24ce8de commit b2d8fb1

File tree

1 file changed

+176
-26
lines changed

1 file changed

+176
-26
lines changed

generate/generate.go

Lines changed: 176 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -159,32 +159,8 @@ func loadAgents(dir string) ([]*agents.Agent, error) {
159159
return nil, nil // Agents are optional
160160
}
161161

162-
entries, err := os.ReadDir(dir)
163-
if err != nil {
164-
return nil, err
165-
}
166-
167-
var agts []*agents.Agent
168-
for _, entry := range entries {
169-
if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
170-
continue
171-
}
172-
173-
path := filepath.Join(dir, entry.Name())
174-
data, err := os.ReadFile(path)
175-
if err != nil {
176-
return nil, err
177-
}
178-
179-
var agt agents.Agent
180-
if err := json.Unmarshal(data, &agt); err != nil {
181-
return nil, fmt.Errorf("parse %s: %w", entry.Name(), err)
182-
}
183-
184-
agts = append(agts, &agt)
185-
}
186-
187-
return agts, nil
162+
// Use agents.ReadCanonicalDir which supports both .md (multi-agent-spec) and .json files
163+
return agents.ReadCanonicalDir(dir)
188164
}
189165

190166
func generateClaude(dir string, plugin *PluginSpec, cmds []*commands.Command, skls []*skills.Skill, agts []*agents.Agent) error {
@@ -851,3 +827,177 @@ func Agents(specsDir, target, outputDir string) (*AgentsResult, error) {
851827

852828
return result, nil
853829
}
830+
831+
// GenerateResult contains the results of unified plugin generation.
832+
type GenerateResult struct {
833+
// CommandCount is the number of commands loaded.
834+
CommandCount int
835+
836+
// SkillCount is the number of skills loaded.
837+
SkillCount int
838+
839+
// AgentCount is the number of agents loaded.
840+
AgentCount int
841+
842+
// TeamName is the name of the team being deployed.
843+
TeamName string
844+
845+
// TargetsGenerated lists the names of generated targets.
846+
TargetsGenerated []string
847+
848+
// GeneratedDirs maps target names to their output directories.
849+
GeneratedDirs map[string]string
850+
}
851+
852+
// Generate generates platform-specific plugins from a unified specs directory.
853+
// Output is driven by the deployment file at specs/deployments/{target}.json.
854+
//
855+
// Each deployment target receives a complete plugin:
856+
// - agents (from specs/agents/*.md)
857+
// - commands (from specs/commands/*.md)
858+
// - skills (from specs/skills/*.md)
859+
// - plugin manifest (from specs/plugin.json)
860+
//
861+
// The specsDir should contain:
862+
// - plugin.json: Plugin metadata
863+
// - commands/: Command definitions (*.md or *.json)
864+
// - skills/: Skill definitions (*.md or *.json)
865+
// - agents/: Agent definitions (*.md with YAML frontmatter)
866+
// - deployments/: Deployment definitions (*.json)
867+
//
868+
// The target parameter specifies which deployment file to use (looks for {target}.json).
869+
// The outputDir is the base directory for resolving relative output paths in the deployment.
870+
func Generate(specsDir, target, outputDir string) (*GenerateResult, error) {
871+
result := &GenerateResult{
872+
GeneratedDirs: make(map[string]string),
873+
}
874+
875+
// Load plugin metadata
876+
pluginPath := filepath.Join(specsDir, "plugin.json")
877+
var plugin *PluginSpec
878+
if _, err := os.Stat(pluginPath); err == nil {
879+
plugin, err = loadPlugin(pluginPath)
880+
if err != nil {
881+
return nil, fmt.Errorf("loading plugin spec: %w", err)
882+
}
883+
} else {
884+
// Create minimal plugin spec if not present
885+
plugin = &PluginSpec{}
886+
}
887+
888+
// Load commands
889+
commandsDir := filepath.Join(specsDir, "commands")
890+
cmds, err := loadCommands(commandsDir)
891+
if err != nil {
892+
return nil, fmt.Errorf("loading commands: %w", err)
893+
}
894+
result.CommandCount = len(cmds)
895+
896+
// Load skills
897+
skillsDir := filepath.Join(specsDir, "skills")
898+
skls, err := loadSkills(skillsDir)
899+
if err != nil {
900+
return nil, fmt.Errorf("loading skills: %w", err)
901+
}
902+
result.SkillCount = len(skls)
903+
904+
// Load agents from multi-agent-spec format (.md files)
905+
agentsDir := filepath.Join(specsDir, "agents")
906+
agts, err := loadMultiAgentSpecAgents(agentsDir)
907+
if err != nil {
908+
return nil, fmt.Errorf("loading agents: %w", err)
909+
}
910+
result.AgentCount = len(agts)
911+
912+
// Load deployment
913+
deploymentFile := filepath.Join(specsDir, "deployments", target+".json")
914+
if _, err := os.Stat(deploymentFile); os.IsNotExist(err) {
915+
return nil, fmt.Errorf("deployment file not found: %s", deploymentFile)
916+
}
917+
918+
deployment, err := loadDeployment(deploymentFile)
919+
if err != nil {
920+
return nil, fmt.Errorf("loading deployment: %w", err)
921+
}
922+
result.TeamName = deployment.Team
923+
924+
// Generate each target
925+
for _, tgt := range deployment.Targets {
926+
// Resolve output path relative to outputDir
927+
targetOutputDir := tgt.Output
928+
if !filepath.IsAbs(targetOutputDir) {
929+
targetOutputDir = filepath.Join(outputDir, targetOutputDir)
930+
}
931+
932+
if err := generatePlatformPlugin(tgt.Platform, targetOutputDir, plugin, cmds, skls, agts); err != nil {
933+
return nil, fmt.Errorf("generating target %s: %w", tgt.Name, err)
934+
}
935+
936+
result.TargetsGenerated = append(result.TargetsGenerated, tgt.Name)
937+
result.GeneratedDirs[tgt.Name] = targetOutputDir
938+
}
939+
940+
return result, nil
941+
}
942+
943+
// generatePlatformPlugin generates a complete plugin for a specific platform.
944+
// It combines agents, commands, skills, and plugin manifest into a platform-specific format.
945+
func generatePlatformPlugin(
946+
platform string,
947+
outputDir string,
948+
plugin *PluginSpec,
949+
cmds []*commands.Command,
950+
skls []*skills.Skill,
951+
agts []*agents.Agent,
952+
) error {
953+
// Create output directory
954+
if err := os.MkdirAll(outputDir, 0755); err != nil {
955+
return fmt.Errorf("creating output dir: %w", err)
956+
}
957+
958+
switch platform {
959+
case "claude", "claude-code":
960+
return generateClaude(outputDir, plugin, cmds, skls, agts)
961+
case "kiro", "kiro-cli":
962+
return generateKiro(outputDir, plugin, skls, agts)
963+
case "gemini", "gemini-cli":
964+
return generateGemini(outputDir, plugin, cmds)
965+
default:
966+
// For unsupported platforms, log a warning but don't fail
967+
fmt.Printf(" Warning: platform %s not fully supported, generating agents only\n", platform)
968+
return generateDeploymentTargetAgentsOnly(platform, agts, outputDir)
969+
}
970+
}
971+
972+
// generateDeploymentTargetAgentsOnly generates only agents for unsupported platforms.
973+
func generateDeploymentTargetAgentsOnly(platform string, agts []*agents.Agent, outputDir string) error {
974+
if len(agts) == 0 {
975+
return nil
976+
}
977+
978+
// Map platform names to adapter names
979+
adapterName := platform
980+
switch platform {
981+
case "claude-code":
982+
adapterName = "claude"
983+
case "kiro-cli":
984+
adapterName = "kiro"
985+
case "gemini-cli":
986+
adapterName = "gemini"
987+
}
988+
989+
adapter, ok := agents.GetAdapter(adapterName)
990+
if !ok {
991+
return fmt.Errorf("%s adapter not found", adapterName)
992+
}
993+
994+
for _, agt := range agts {
995+
ext := adapter.FileExtension()
996+
path := filepath.Join(outputDir, agt.Name+ext)
997+
if err := adapter.WriteFile(agt, path); err != nil {
998+
return fmt.Errorf("writing %s: %w", agt.Name, err)
999+
}
1000+
}
1001+
1002+
return nil
1003+
}

0 commit comments

Comments
 (0)