@@ -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
190166func 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