Skip to content

Commit 96802bf

Browse files
authored
Merge pull request #2524 from mendsley/mendsley/design_file
feat: add --design-file flag for reading design from files
2 parents 13c2ecc + 71d7ae5 commit 96802bf

File tree

5 files changed

+160
-3
lines changed

5 files changed

+160
-3
lines changed

cmd/bd/create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ var createCmd = &cobra.Command{
9999
}
100100
}
101101

102-
design, _ := cmd.Flags().GetString("design")
102+
design, _ := getDesignFlag(cmd)
103103
acceptance, _ := cmd.Flags().GetString("acceptance")
104104
notes, _ := cmd.Flags().GetString("notes")
105105
specID, _ := cmd.Flags().GetString("spec-id")

cmd/bd/flags.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ func registerCommonIssueFlags(cmd *cobra.Command) {
2626
cmd.MarkFlagsMutuallyExclusive("stdin", "body")
2727
cmd.MarkFlagsMutuallyExclusive("stdin", "message")
2828
cmd.Flags().String("design", "", "Design notes")
29+
cmd.Flags().String("design-file", "", "Read design from file (use - for stdin)")
30+
cmd.MarkFlagsMutuallyExclusive("design", "design-file")
2931
cmd.Flags().String("acceptance", "", "Acceptance criteria")
3032
cmd.Flags().String("notes", "", "Additional notes")
3133
cmd.Flags().String("append-notes", "", "Append to existing notes (with newline separator)")
@@ -170,6 +172,27 @@ func getDescriptionFlag(cmd *cobra.Command) (string, bool) {
170172
return desc, descChanged
171173
}
172174

175+
// getDesignFlag retrieves the design value from --design-file or --design.
176+
// Returns the value, whether any flag was explicitly changed, and any error.
177+
func getDesignFlag(cmd *cobra.Command) (string, bool) {
178+
if cmd.Flags().Changed("design-file") {
179+
path, _ := cmd.Flags().GetString("design-file")
180+
content, err := readBodyFile(path)
181+
if err != nil {
182+
FatalError("reading from stin: %v", err)
183+
}
184+
185+
return content, true
186+
}
187+
188+
if cmd.Flags().Changed("design") {
189+
v, _ := cmd.Flags().GetString("design")
190+
return v, true
191+
}
192+
193+
return "", false
194+
}
195+
173196
// readBodyFile reads the description content from a file.
174197
// If filePath is "-", reads from stdin.
175198
func readBodyFile(filePath string) (string, error) {

cmd/bd/flags_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,3 +340,107 @@ func TestGetDescriptionFlag(t *testing.T) {
340340
}
341341
})
342342
}
343+
344+
func TestGetDesignFlag(t *testing.T) {
345+
// Helper to create a cobra command with common issue flags registered
346+
newCmd := func() *cobra.Command {
347+
cmd := &cobra.Command{
348+
Use: "test",
349+
Run: func(cmd *cobra.Command, args []string) {},
350+
}
351+
registerCommonIssueFlags(cmd)
352+
return cmd
353+
}
354+
355+
t.Run("InlineDesignFlag", func(t *testing.T) {
356+
cmd := newCmd()
357+
if err := cmd.ParseFlags([]string{"--design", "inline design content"}); err != nil {
358+
t.Fatalf("failed to parse flags: %v", err)
359+
}
360+
361+
got, changed := getDesignFlag(cmd)
362+
if !changed {
363+
t.Error("expected changed=true")
364+
}
365+
if got != "inline design content" {
366+
t.Errorf("expected 'inline design content', got %q", got)
367+
}
368+
})
369+
370+
t.Run("DesignFileFlag", func(t *testing.T) {
371+
tmpDir := t.TempDir()
372+
filePath := filepath.Join(tmpDir, "design.md")
373+
content := "## Architecture\n\nUse microservices.\n"
374+
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
375+
t.Fatalf("failed to write test file: %v", err)
376+
}
377+
378+
cmd := newCmd()
379+
if err := cmd.ParseFlags([]string{"--design-file", filePath}); err != nil {
380+
t.Fatalf("failed to parse flags: %v", err)
381+
}
382+
383+
got, changed := getDesignFlag(cmd)
384+
if !changed {
385+
t.Error("expected changed=true")
386+
}
387+
if got != content {
388+
t.Errorf("expected %q, got %q", content, got)
389+
}
390+
})
391+
392+
t.Run("DesignFileStdin", func(t *testing.T) {
393+
r, w, err := os.Pipe()
394+
if err != nil {
395+
t.Fatalf("failed to create pipe: %v", err)
396+
}
397+
oldStdin := os.Stdin
398+
os.Stdin = r
399+
t.Cleanup(func() { os.Stdin = oldStdin })
400+
401+
content := "Design from stdin\nWith multiple lines\n"
402+
go func() {
403+
w.WriteString(content)
404+
w.Close()
405+
}()
406+
407+
cmd := newCmd()
408+
if err := cmd.ParseFlags([]string{"--design-file", "-"}); err != nil {
409+
t.Fatalf("failed to parse flags: %v", err)
410+
}
411+
412+
got, changed := getDesignFlag(cmd)
413+
if !changed {
414+
t.Error("expected changed=true")
415+
}
416+
if got != content {
417+
t.Errorf("expected %q, got %q", content, got)
418+
}
419+
})
420+
421+
t.Run("NoFlagsSet", func(t *testing.T) {
422+
cmd := newCmd()
423+
if err := cmd.ParseFlags([]string{}); err != nil {
424+
t.Fatalf("failed to parse flags: %v", err)
425+
}
426+
427+
got, changed := getDesignFlag(cmd)
428+
if changed {
429+
t.Error("expected changed=false when no flags set")
430+
}
431+
if got != "" {
432+
t.Errorf("expected empty string, got %q", got)
433+
}
434+
})
435+
436+
t.Run("MutualExclusionRegistered", func(t *testing.T) {
437+
cmd := newCmd()
438+
// Verify both flags are registered
439+
if cmd.Flags().Lookup("design") == nil {
440+
t.Fatal("expected --design flag to be registered")
441+
}
442+
if cmd.Flags().Lookup("design-file") == nil {
443+
t.Fatal("expected --design-file flag to be registered")
444+
}
445+
})
446+
}

cmd/bd/update.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ create, update, show, or close operation).`,
9494
if descChanged {
9595
updates["description"] = description
9696
}
97-
if cmd.Flags().Changed("design") {
98-
design, _ := cmd.Flags().GetString("design")
97+
design, designChanged := getDesignFlag(cmd)
98+
if designChanged {
9999
updates["design"] = design
100100
}
101101
if cmd.Flags().Changed("notes") && cmd.Flags().Changed("append-notes") {

cmd/bd/update_edge_cases_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,36 @@ func TestCLI_UpdateDesignField(t *testing.T) {
390390
}
391391
}
392392

393+
// TestCLI_UpdateDesignFile tests setting the design field via --design-file.
394+
func TestCLI_UpdateDesignFile(t *testing.T) {
395+
if testing.Short() {
396+
t.Skip("skipping slow CLI test in short mode")
397+
}
398+
tmpDir := setupCLITestDB(t)
399+
400+
out := runBDInProcess(t, tmpDir, "create", "Design file test", "-p", "1", "--json")
401+
var issue map[string]interface{}
402+
json.Unmarshal([]byte(out), &issue)
403+
id := issue["id"].(string)
404+
405+
// Write design content to a temp file
406+
designText := "## Architecture\n\nUse microservices pattern with gRPC."
407+
designFile := filepath.Join(tmpDir, "design.md")
408+
if err := os.WriteFile(designFile, []byte(designText), 0644); err != nil {
409+
t.Fatalf("failed to write design file: %v", err)
410+
}
411+
412+
runBDInProcess(t, tmpDir, "update", id, "--design-file", designFile)
413+
414+
out = runBDInProcess(t, tmpDir, "show", id, "--json")
415+
var updated []map[string]interface{}
416+
json.Unmarshal([]byte(out), &updated)
417+
got, _ := updated[0]["design"].(string)
418+
if got != designText {
419+
t.Errorf("Expected design=%q, got: %q", designText, got)
420+
}
421+
}
422+
393423
// TestCLI_UpdateAcceptanceCriteria tests the --acceptance flag.
394424
func TestCLI_UpdateAcceptanceCriteria(t *testing.T) {
395425
if testing.Short() {

0 commit comments

Comments
 (0)