Skip to content

Commit 2f5db70

Browse files
grokifyclaude
andcommitted
feat(cli): add 'generate agents' and 'generate all' commands
New simplified commands for multi-agent-spec workflow: generate agents: - Reads from specs/agents/*.md (YAML frontmatter) - Uses specs/deployments/<target>.json for output locations - Flags: --specs, --target, --output generate all: - Combines plugins and agents generation - Generates complete plugin packages per platform - Flags: --specs, --target, --output, --platforms Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 42abcd7 commit 2f5db70

File tree

2 files changed

+239
-52
lines changed

2 files changed

+239
-52
lines changed

cmd/assistantkit/generate.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,64 @@ Example:
7171
RunE: runGenerateDeployment,
7272
}
7373

74+
var (
75+
agentsSpecDir string
76+
agentsTarget string
77+
agentsOutputDir string
78+
)
79+
80+
var generateAgentsCmd = &cobra.Command{
81+
Use: "agents",
82+
Short: "Generate agents from specs directory (simplified)",
83+
Long: `Generate platform-specific agents from a specs directory.
84+
85+
This is a simplified command that reads from specs/agents/*.md and uses
86+
specs/deployments/<target>.json to determine output locations.
87+
88+
The specs directory should contain:
89+
- agents/: Agent definitions (*.md with YAML frontmatter)
90+
- deployments/: Deployment definitions (*.json, defaults to local.json)
91+
92+
Example:
93+
assistantkit generate agents
94+
assistantkit generate agents --specs=specs --target=local --output=.`,
95+
RunE: runGenerateAgents,
96+
}
97+
98+
var (
99+
allSpecsDir string
100+
allTarget string
101+
allOutputDir string
102+
allPlatforms []string
103+
)
104+
105+
var generateAllCmd = &cobra.Command{
106+
Use: "all",
107+
Short: "Generate all plugin artifacts from a unified specs directory",
108+
Long: `Generate all platform-specific artifacts from a unified specs directory.
109+
110+
This command combines 'generate plugins' and 'generate agents' into a single
111+
operation. It reads all specs from a single directory and generates complete
112+
plugin packages for each platform.
113+
114+
The specs directory should contain:
115+
- plugin.json: Plugin metadata
116+
- commands/: Command definitions (*.md or *.json)
117+
- skills/: Skill definitions (*.md or *.json)
118+
- agents/: Agent definitions (*.md with YAML frontmatter)
119+
- deployments/: Deployment definitions (*.json)
120+
121+
Example:
122+
assistantkit generate all --specs=specs --target=local
123+
assistantkit generate all --specs=specs --target=local --output=. --platforms=claude,kiro,gemini`,
124+
RunE: runGenerateAll,
125+
}
126+
74127
func init() {
75128
generateCmd.AddCommand(generatePluginsCmd)
76129
generateCmd.AddCommand(generateDeploymentCmd)
130+
generateCmd.AddCommand(generateAgentsCmd)
131+
generateCmd.AddCommand(generateAllCmd)
77132

78133
generatePluginsCmd.Flags().StringVar(&specDir, "spec", "plugins/spec", "Path to canonical spec directory")
79134
generatePluginsCmd.Flags().StringVar(&outputDir, "output", "plugins", "Output directory for generated plugins")
@@ -83,6 +138,15 @@ func init() {
83138
generateDeploymentCmd.Flags().StringVar(&deploymentSpecDir, "specs", "specs", "Path to multi-agent-spec directory")
84139
generateDeploymentCmd.Flags().StringVar(&deploymentFile, "deployment", "", "Path to deployment definition file (required)")
85140
_ = generateDeploymentCmd.MarkFlagRequired("deployment")
141+
142+
generateAgentsCmd.Flags().StringVar(&agentsSpecDir, "specs", "specs", "Path to specs directory")
143+
generateAgentsCmd.Flags().StringVar(&agentsTarget, "target", "local", "Deployment target (looks for specs/deployments/<target>.json)")
144+
generateAgentsCmd.Flags().StringVar(&agentsOutputDir, "output", ".", "Output base directory (repo root)")
145+
146+
generateAllCmd.Flags().StringVar(&allSpecsDir, "specs", "specs", "Path to unified specs directory")
147+
generateAllCmd.Flags().StringVar(&allTarget, "target", "local", "Deployment target (looks for specs/deployments/<target>.json)")
148+
generateAllCmd.Flags().StringVar(&allOutputDir, "output", ".", "Output base directory (repo root)")
149+
generateAllCmd.Flags().StringSliceVar(&allPlatforms, "platforms", []string{"claude", "kiro", "gemini"}, "Platforms to generate")
86150
}
87151

88152
func runGenerateDeployment(cmd *cobra.Command, args []string) error {
@@ -172,3 +236,106 @@ func runGeneratePlugins(cmd *cobra.Command, args []string) error {
172236
fmt.Println("\nDone!")
173237
return nil
174238
}
239+
240+
func runGenerateAgents(cmd *cobra.Command, args []string) error {
241+
// Resolve paths
242+
absSpecsDir, err := filepath.Abs(agentsSpecDir)
243+
if err != nil {
244+
return fmt.Errorf("resolving specs dir: %w", err)
245+
}
246+
247+
absOutputDir, err := filepath.Abs(agentsOutputDir)
248+
if err != nil {
249+
return fmt.Errorf("resolving output dir: %w", err)
250+
}
251+
252+
// Validate specs directory exists
253+
if _, err := os.Stat(absSpecsDir); os.IsNotExist(err) {
254+
return fmt.Errorf("specs directory not found: %s", absSpecsDir)
255+
}
256+
257+
// Print header
258+
fmt.Println("=== AssistantKit Agent Generator ===")
259+
fmt.Printf("Specs directory: %s\n", absSpecsDir)
260+
fmt.Printf("Target: %s\n", agentsTarget)
261+
fmt.Printf("Output directory: %s\n", absOutputDir)
262+
fmt.Println()
263+
264+
// Generate agents
265+
result, err := generate.Agents(absSpecsDir, agentsTarget, absOutputDir)
266+
if err != nil {
267+
return fmt.Errorf("generating agents: %w", err)
268+
}
269+
270+
// Print results
271+
fmt.Printf("Team: %s\n", result.TeamName)
272+
fmt.Printf("Loaded: %d agents\n\n", result.AgentCount)
273+
274+
fmt.Println("Generated targets:")
275+
for _, target := range result.TargetsGenerated {
276+
dir := result.GeneratedDirs[target]
277+
fmt.Printf(" - %s: %s\n", target, dir)
278+
}
279+
280+
fmt.Println("\nDone!")
281+
return nil
282+
}
283+
284+
func runGenerateAll(cmd *cobra.Command, args []string) error {
285+
// Resolve paths
286+
absSpecsDir, err := filepath.Abs(allSpecsDir)
287+
if err != nil {
288+
return fmt.Errorf("resolving specs dir: %w", err)
289+
}
290+
291+
absOutputDir, err := filepath.Abs(allOutputDir)
292+
if err != nil {
293+
return fmt.Errorf("resolving output dir: %w", err)
294+
}
295+
296+
// Validate specs directory exists
297+
if _, err := os.Stat(absSpecsDir); os.IsNotExist(err) {
298+
return fmt.Errorf("specs directory not found: %s", absSpecsDir)
299+
}
300+
301+
// Print header
302+
fmt.Println("=== AssistantKit Unified Generator ===")
303+
fmt.Printf("Specs directory: %s\n", absSpecsDir)
304+
fmt.Printf("Output directory: %s\n", absOutputDir)
305+
fmt.Printf("Target: %s\n", allTarget)
306+
fmt.Printf("Platforms: %s\n", strings.Join(allPlatforms, ", "))
307+
fmt.Println()
308+
309+
// Step 1: Generate plugins (commands, skills, plugin manifest)
310+
pluginsOutputDir := filepath.Join(absOutputDir, "plugins")
311+
fmt.Println("1. Generating plugins (commands, skills, manifest)...")
312+
313+
pluginResult, err := generate.Plugins(absSpecsDir, pluginsOutputDir, allPlatforms)
314+
if err != nil {
315+
return fmt.Errorf("generating plugins: %w", err)
316+
}
317+
318+
fmt.Printf(" Loaded: %d commands, %d skills\n", pluginResult.CommandCount, pluginResult.SkillCount)
319+
for platform, dir := range pluginResult.GeneratedDirs {
320+
fmt.Printf(" Generated %s: %s\n", platform, dir)
321+
}
322+
fmt.Println()
323+
324+
// Step 2: Generate agents from deployment target
325+
fmt.Println("2. Generating agents from deployment target...")
326+
327+
agentResult, err := generate.Agents(absSpecsDir, allTarget, absOutputDir)
328+
if err != nil {
329+
return fmt.Errorf("generating agents: %w", err)
330+
}
331+
332+
fmt.Printf(" Team: %s\n", agentResult.TeamName)
333+
fmt.Printf(" Loaded: %d agents\n", agentResult.AgentCount)
334+
for _, target := range agentResult.TargetsGenerated {
335+
dir := agentResult.GeneratedDirs[target]
336+
fmt.Printf(" Generated %s: %s\n", target, dir)
337+
}
338+
339+
fmt.Println("\nDone!")
340+
return nil
341+
}

generate/generate.go

Lines changed: 72 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -141,65 +141,17 @@ func loadCommands(dir string) ([]*commands.Command, error) {
141141
return nil, nil // Commands are optional
142142
}
143143

144-
entries, err := os.ReadDir(dir)
145-
if err != nil {
146-
return nil, err
147-
}
148-
149-
var cmds []*commands.Command
150-
for _, entry := range entries {
151-
if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
152-
continue
153-
}
154-
155-
path := filepath.Join(dir, entry.Name())
156-
data, err := os.ReadFile(path)
157-
if err != nil {
158-
return nil, err
159-
}
160-
161-
var cmd commands.Command
162-
if err := json.Unmarshal(data, &cmd); err != nil {
163-
return nil, fmt.Errorf("parse %s: %w", entry.Name(), err)
164-
}
165-
166-
cmds = append(cmds, &cmd)
167-
}
168-
169-
return cmds, nil
144+
// Use ReadCanonicalDir which supports both .json and .md files
145+
return commands.ReadCanonicalDir(dir)
170146
}
171147

172148
func loadSkills(dir string) ([]*skills.Skill, error) {
173149
if _, err := os.Stat(dir); os.IsNotExist(err) {
174150
return nil, nil // Skills are optional
175151
}
176152

177-
entries, err := os.ReadDir(dir)
178-
if err != nil {
179-
return nil, err
180-
}
181-
182-
var skls []*skills.Skill
183-
for _, entry := range entries {
184-
if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
185-
continue
186-
}
187-
188-
path := filepath.Join(dir, entry.Name())
189-
data, err := os.ReadFile(path)
190-
if err != nil {
191-
return nil, err
192-
}
193-
194-
var skl skills.Skill
195-
if err := json.Unmarshal(data, &skl); err != nil {
196-
return nil, fmt.Errorf("parse %s: %w", entry.Name(), err)
197-
}
198-
199-
skls = append(skls, &skl)
200-
}
201-
202-
return skls, nil
153+
// Use ReadCanonicalDir which supports both .json and .md files
154+
return skills.ReadCanonicalDir(dir)
203155
}
204156

205157
func loadAgents(dir string) ([]*agents.Agent, error) {
@@ -831,3 +783,71 @@ func generateGeminiCLIDeployment(agts []*agents.Agent, outputDir string) error {
831783

832784
return nil
833785
}
786+
787+
// AgentsResult contains the results of simplified agent generation.
788+
type AgentsResult struct {
789+
// AgentCount is the number of agents loaded.
790+
AgentCount int
791+
792+
// TeamName is the name of the team being deployed.
793+
TeamName string
794+
795+
// TargetsGenerated lists the names of generated targets.
796+
TargetsGenerated []string
797+
798+
// GeneratedDirs maps target names to their output directories.
799+
GeneratedDirs map[string]string
800+
}
801+
802+
// Agents generates platform-specific agents from a specs directory with simplified options.
803+
//
804+
// The specsDir should contain:
805+
// - agents/: Agent definitions (*.md with YAML frontmatter)
806+
// - deployments/: Deployment definitions (*.json)
807+
//
808+
// The target parameter specifies which deployment file to use (looks for {target}.json).
809+
// The outputDir is the base directory for resolving relative output paths in the deployment.
810+
func Agents(specsDir, target, outputDir string) (*AgentsResult, error) {
811+
result := &AgentsResult{
812+
GeneratedDirs: make(map[string]string),
813+
}
814+
815+
// Load agents from multi-agent-spec format
816+
agentsDir := filepath.Join(specsDir, "agents")
817+
agts, err := loadMultiAgentSpecAgents(agentsDir)
818+
if err != nil {
819+
return nil, fmt.Errorf("loading agents: %w", err)
820+
}
821+
result.AgentCount = len(agts)
822+
823+
// Construct deployment file path
824+
deploymentFile := filepath.Join(specsDir, "deployments", target+".json")
825+
if _, err := os.Stat(deploymentFile); os.IsNotExist(err) {
826+
return nil, fmt.Errorf("deployment file not found: %s", deploymentFile)
827+
}
828+
829+
// Load deployment
830+
deployment, err := loadDeployment(deploymentFile)
831+
if err != nil {
832+
return nil, fmt.Errorf("loading deployment: %w", err)
833+
}
834+
result.TeamName = deployment.Team
835+
836+
// Generate each target
837+
for _, tgt := range deployment.Targets {
838+
// Resolve output path relative to outputDir (not specsDir)
839+
targetOutputDir := tgt.Output
840+
if !filepath.IsAbs(targetOutputDir) {
841+
targetOutputDir = filepath.Join(outputDir, targetOutputDir)
842+
}
843+
844+
if err := generateDeploymentTarget(tgt, agts, targetOutputDir); err != nil {
845+
return nil, fmt.Errorf("generating target %s: %w", tgt.Name, err)
846+
}
847+
848+
result.TargetsGenerated = append(result.TargetsGenerated, tgt.Name)
849+
result.GeneratedDirs[tgt.Name] = targetOutputDir
850+
}
851+
852+
return result, nil
853+
}

0 commit comments

Comments
 (0)