Unify session-aware CLI parsing and add session-id compatibility
This commit is contained in:
@@ -16,7 +16,6 @@ import (
|
||||
// Clean removes local workspace/spool state while preserving durable cache
|
||||
// state unless cache cleanup is explicitly requested.
|
||||
func Clean(ctx context.Context, args []string, out io.Writer) error {
|
||||
positionalSessionID, args := pullLeadingSessionID(args)
|
||||
fs := flag.NewFlagSet("clean", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
var flags commonConfigFlags
|
||||
@@ -27,20 +26,8 @@ func Clean(ctx context.Context, args []string, out io.Writer) error {
|
||||
fs.BoolVar(&all, "all", false, "clean all local session work/spool state")
|
||||
fs.BoolVar(&dryRun, "dry-run", false, "print cleanup targets without deleting")
|
||||
fs.BoolVar(&clearCache, "clear-cache", false, "also clear durable S3 audio cache entries")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("clean: invalid flags: %w", err)
|
||||
}
|
||||
if positionalSessionID == "" {
|
||||
if err := applyParsedSessionIDArg("clean", fs, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("clean: unexpected positional arguments")
|
||||
}
|
||||
if err := applyPositionalSessionID("clean", positionalSessionID, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := parseSessionAwareFlags("clean", fs, args, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if all {
|
||||
return cleanAllLocal(flags, dryRun, clearCache, out)
|
||||
|
||||
@@ -47,6 +47,7 @@ func addCommonConfigFlags(fs *flag.FlagSet, flags *commonConfigFlags) {
|
||||
fs.StringVar(&flags.campaignPath, "campaign", "", "campaign ID")
|
||||
fs.StringVar(&flags.campaignFilePath, "campaign-file", "", "path to campaign.yml")
|
||||
fs.StringVar(&flags.sessionPath, "session", "", "path to session.yml")
|
||||
fs.StringVar(&flags.sessionID, "session-id", "", "session identifier")
|
||||
fs.StringVar(&flags.previousSessionID, "previous-session-id", "", "expected previous session identifier")
|
||||
}
|
||||
|
||||
@@ -111,25 +112,12 @@ func Artifacts(ctx context.Context, args []string, out io.Writer) error {
|
||||
|
||||
// SessionValidate performs a read-only session preflight.
|
||||
func SessionValidate(ctx context.Context, args []string, out io.Writer) error {
|
||||
positionalSessionID, args := pullLeadingSessionID(args)
|
||||
fs := flag.NewFlagSet("session validate", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
var flags commonConfigFlags
|
||||
addCommonConfigFlags(fs, &flags)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("session validate: invalid flags: %w", err)
|
||||
}
|
||||
if positionalSessionID == "" {
|
||||
if err := applyParsedSessionIDArg("session validate", fs, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("session validate: unexpected positional arguments")
|
||||
}
|
||||
if err := applyPositionalSessionID("session validate", positionalSessionID, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := parseSessionAwareFlags("session validate", fs, args, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("session validate: session_id is required")
|
||||
@@ -193,25 +181,12 @@ func SessionValidate(ctx context.Context, args []string, out io.Writer) error {
|
||||
|
||||
// Status reports effective local/remote session state.
|
||||
func Status(ctx context.Context, args []string, out io.Writer) error {
|
||||
positionalSessionID, args := pullLeadingSessionID(args)
|
||||
fs := flag.NewFlagSet("status", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
var flags commonConfigFlags
|
||||
addCommonConfigFlags(fs, &flags)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("status: invalid flags: %w", err)
|
||||
}
|
||||
if positionalSessionID == "" {
|
||||
if err := applyParsedSessionIDArg("status", fs, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("status: unexpected positional arguments")
|
||||
}
|
||||
if err := applyPositionalSessionID("status", positionalSessionID, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := parseSessionAwareFlags("status", fs, args, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("status: session_id is required")
|
||||
@@ -282,7 +257,6 @@ func Status(ctx context.Context, args []string, out io.Writer) error {
|
||||
|
||||
// SessionInit creates a local or remote session.yml skeleton.
|
||||
func SessionInit(ctx context.Context, args []string, out io.Writer) error {
|
||||
positionalSessionID, args := pullLeadingSessionID(args)
|
||||
fs := flag.NewFlagSet("session init", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
var pipelinePath, campaignPath, campaignFilePath, sessionID, previousSessionID, date, title, output, audioS3Prefix, audioDir string
|
||||
@@ -290,6 +264,7 @@ func SessionInit(ctx context.Context, args []string, out io.Writer) error {
|
||||
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
||||
fs.StringVar(&campaignPath, "campaign", "", "campaign ID")
|
||||
fs.StringVar(&campaignFilePath, "campaign-file", "", "path to campaign.yml")
|
||||
fs.StringVar(&sessionID, "session-id", "", "session identifier")
|
||||
fs.StringVar(&previousSessionID, "previous-session-id", "", "previous session identifier")
|
||||
fs.StringVar(&date, "date", "", "session date")
|
||||
fs.StringVar(&title, "title", "", "session title")
|
||||
@@ -298,20 +273,8 @@ func SessionInit(ctx context.Context, args []string, out io.Writer) error {
|
||||
fs.StringVar(&audioDir, "audio-dir", "", "local audio directory")
|
||||
fs.BoolVar(&remote, "remote", false, "write session.yml to S3 session prefix")
|
||||
fs.BoolVar(&force, "force", false, "overwrite existing target")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("session init: invalid flags: %w", err)
|
||||
}
|
||||
if positionalSessionID == "" {
|
||||
if err := applyParsedSessionIDArg("session init", fs, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("session init: unexpected positional arguments")
|
||||
}
|
||||
if err := applyPositionalSessionID("session init", positionalSessionID, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := parseSessionAwareFlags("session init", fs, args, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(sessionID) == "" {
|
||||
return fmt.Errorf("session init: session_id is required")
|
||||
@@ -405,27 +368,14 @@ func SessionInit(ctx context.Context, args []string, out io.Writer) error {
|
||||
|
||||
// ArtifactsList lists effective artifact sources.
|
||||
func ArtifactsList(ctx context.Context, args []string, out io.Writer) error {
|
||||
positionalSessionID, args := pullLeadingSessionID(args)
|
||||
fs := flag.NewFlagSet("artifacts list", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
var flags commonConfigFlags
|
||||
var remote bool
|
||||
addCommonConfigFlags(fs, &flags)
|
||||
fs.BoolVar(&remote, "remote", false, "inspect remote publish availability")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("artifacts list: invalid flags: %w", err)
|
||||
}
|
||||
if positionalSessionID == "" {
|
||||
if err := applyParsedSessionIDArg("artifacts list", fs, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("artifacts list: unexpected positional arguments")
|
||||
}
|
||||
if err := applyPositionalSessionID("artifacts list", positionalSessionID, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := parseSessionAwareFlags("artifacts list", fs, args, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("artifacts list: session_id is required")
|
||||
@@ -463,25 +413,12 @@ func Locks(ctx context.Context, args []string, out io.Writer) error {
|
||||
|
||||
// LocksList lists effective publish locks.
|
||||
func LocksList(ctx context.Context, args []string, out io.Writer) error {
|
||||
positionalSessionID, args := pullLeadingSessionID(args)
|
||||
fs := flag.NewFlagSet("locks", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
var flags commonConfigFlags
|
||||
addCommonConfigFlags(fs, &flags)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("locks: invalid flags: %w", err)
|
||||
}
|
||||
if positionalSessionID == "" {
|
||||
if err := applyParsedSessionIDArg("locks", fs, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("locks: unexpected positional arguments")
|
||||
}
|
||||
if err := applyPositionalSessionID("locks", positionalSessionID, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := parseSessionAwareFlags("locks", fs, args, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("locks: session_id is required")
|
||||
@@ -515,11 +452,18 @@ func LocksAdd(ctx context.Context, args []string, out io.Writer) error {
|
||||
return fmt.Errorf("locks add: invalid flags: %w", err)
|
||||
}
|
||||
if source == "" {
|
||||
if fs.NArg() != 2 {
|
||||
switch fs.NArg() {
|
||||
case 2:
|
||||
positionalSessionID = strings.TrimSpace(fs.Arg(0))
|
||||
source = strings.TrimSpace(fs.Arg(1))
|
||||
case 1:
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("locks add: expected session_id and source id")
|
||||
}
|
||||
source = strings.TrimSpace(fs.Arg(0))
|
||||
default:
|
||||
return fmt.Errorf("locks add: expected session_id and source id")
|
||||
}
|
||||
positionalSessionID = strings.TrimSpace(fs.Arg(0))
|
||||
source = strings.TrimSpace(fs.Arg(1))
|
||||
} else if fs.NArg() != 0 {
|
||||
return fmt.Errorf("locks add: unexpected positional arguments")
|
||||
}
|
||||
@@ -572,11 +516,18 @@ func LocksRemove(ctx context.Context, args []string, out io.Writer) error {
|
||||
return fmt.Errorf("locks remove: invalid flags: %w", err)
|
||||
}
|
||||
if source == "" {
|
||||
if fs.NArg() != 2 {
|
||||
switch fs.NArg() {
|
||||
case 2:
|
||||
positionalSessionID = strings.TrimSpace(fs.Arg(0))
|
||||
source = strings.TrimSpace(fs.Arg(1))
|
||||
case 1:
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("locks remove: expected session_id and source id")
|
||||
}
|
||||
source = strings.TrimSpace(fs.Arg(0))
|
||||
default:
|
||||
return fmt.Errorf("locks remove: expected session_id and source id")
|
||||
}
|
||||
positionalSessionID = strings.TrimSpace(fs.Arg(0))
|
||||
source = strings.TrimSpace(fs.Arg(1))
|
||||
} else if fs.NArg() != 0 {
|
||||
return fmt.Errorf("locks remove: unexpected positional arguments")
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
@@ -17,46 +16,21 @@ import (
|
||||
|
||||
// Plan validates configuration, prepares the local workdir, and prints stage order.
|
||||
func Plan(ctx context.Context, args []string, out io.Writer) error {
|
||||
positionalSessionID, args := pullLeadingSessionID(args)
|
||||
fs := flag.NewFlagSet("plan", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
var pipelinePath string
|
||||
var campaignPath string
|
||||
var campaignFilePath string
|
||||
var sessionPath string
|
||||
var sessionID string
|
||||
var previousSessionID string
|
||||
var flags commonConfigFlags
|
||||
var force bool
|
||||
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
||||
fs.StringVar(&campaignPath, "campaign", "", "campaign ID")
|
||||
fs.StringVar(&campaignFilePath, "campaign-file", "", "path to campaign.yml")
|
||||
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
||||
fs.StringVar(&previousSessionID, "previous-session-id", "", "expected previous session identifier")
|
||||
addCommonConfigFlags(fs, &flags)
|
||||
fs.BoolVar(&force, "force", false, "force stage execution (reserved for future behavior)")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("plan: invalid flags: %w", err)
|
||||
if err := parseSessionAwareFlags("plan", fs, args, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if positionalSessionID == "" {
|
||||
if err := applyParsedSessionIDArg("plan", fs, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("plan: unexpected positional arguments")
|
||||
}
|
||||
if err := applyPositionalSessionID("plan", positionalSessionID, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(sessionID) == "" {
|
||||
if flags.sessionID == "" {
|
||||
return fmt.Errorf("plan: session_id is required")
|
||||
}
|
||||
cfg, err := loadCommandConfig(ctx, pipelinePath, campaignPath, campaignFilePath, sessionPath, config.SessionLoadOptions{
|
||||
SessionID: sessionID,
|
||||
PreviousSessionID: previousSessionID,
|
||||
})
|
||||
cfg, err := loadCommandConfig(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions())
|
||||
if err != nil {
|
||||
return fmt.Errorf("plan: %w", err)
|
||||
}
|
||||
|
||||
@@ -27,20 +27,11 @@ func Restore(ctx context.Context, args []string, out io.Writer) error {
|
||||
fs := flag.NewFlagSet("restore", flag.ContinueOnError)
|
||||
fs.SetOutput(out)
|
||||
|
||||
var pipelinePath string
|
||||
var campaignPath string
|
||||
var campaignFilePath string
|
||||
var sessionPath string
|
||||
var sessionID string
|
||||
var previousSessionID string
|
||||
var flags commonConfigFlags
|
||||
var dryRun bool
|
||||
var force bool
|
||||
var includeAudio bool
|
||||
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
||||
fs.StringVar(&campaignPath, "campaign", "", "campaign ID")
|
||||
fs.StringVar(&campaignFilePath, "campaign-file", "", "path to campaign.yml")
|
||||
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
||||
fs.StringVar(&previousSessionID, "previous-session-id", "", "expected previous session identifier")
|
||||
addCommonConfigFlags(fs, &flags)
|
||||
fs.BoolVar(&dryRun, "dry-run", false, "plan restore actions without writing local files")
|
||||
fs.BoolVar(&force, "force", false, "overwrite local conflicts with remote state")
|
||||
fs.BoolVar(&includeAudio, "include-audio", false, "include archived session-level audio objects")
|
||||
@@ -57,25 +48,13 @@ func Restore(ctx context.Context, args []string, out io.Writer) error {
|
||||
}
|
||||
return fmt.Errorf("restore: invalid flags: %w", err)
|
||||
}
|
||||
if positionalSessionID == "" {
|
||||
if err := applyParsedSessionIDArg("restore", fs, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("restore: unexpected positional arguments")
|
||||
}
|
||||
if err := applyPositionalSessionID("restore", positionalSessionID, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveParsedSessionID("restore", positionalSessionID, fs, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(sessionID) == "" {
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("restore: session_id is required")
|
||||
}
|
||||
cfg, err := loadCommandConfig(ctx, pipelinePath, campaignPath, campaignFilePath, sessionPath, config.SessionLoadOptions{
|
||||
SessionID: sessionID,
|
||||
PreviousSessionID: previousSessionID,
|
||||
})
|
||||
cfg, err := loadCommandConfig(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions())
|
||||
if err != nil {
|
||||
return fmt.Errorf("restore: %w", err)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
@@ -14,48 +13,23 @@ import (
|
||||
|
||||
// Resume continues execution from the first non-succeeded stage in the manifest.
|
||||
func Resume(ctx context.Context, args []string, out io.Writer) error {
|
||||
positionalSessionID, args := pullLeadingSessionID(args)
|
||||
fs := flag.NewFlagSet("resume", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
var pipelinePath string
|
||||
var campaignPath string
|
||||
var campaignFilePath string
|
||||
var sessionPath string
|
||||
var sessionID string
|
||||
var previousSessionID string
|
||||
var flags commonConfigFlags
|
||||
var force bool
|
||||
var selectedArtifacts artifactSelectionFlag
|
||||
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
||||
fs.StringVar(&campaignPath, "campaign", "", "campaign ID")
|
||||
fs.StringVar(&campaignFilePath, "campaign-file", "", "path to campaign.yml")
|
||||
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
||||
fs.StringVar(&previousSessionID, "previous-session-id", "", "expected previous session identifier")
|
||||
addCommonConfigFlags(fs, &flags)
|
||||
fs.BoolVar(&force, "force", false, "force stage execution")
|
||||
fs.Var(&selectedArtifacts, "artifacts", "configured artifact names to execute and publish (comma-separated or repeatable)")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("resume: invalid flags: %w", err)
|
||||
if err := parseSessionAwareFlags("resume", fs, args, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if positionalSessionID == "" {
|
||||
if err := applyParsedSessionIDArg("resume", fs, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("resume: unexpected positional arguments")
|
||||
}
|
||||
if err := applyPositionalSessionID("resume", positionalSessionID, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(sessionID) == "" {
|
||||
if flags.sessionID == "" {
|
||||
return fmt.Errorf("resume: session_id is required")
|
||||
}
|
||||
cfg, err := loadCommandConfig(ctx, pipelinePath, campaignPath, campaignFilePath, sessionPath, config.SessionLoadOptions{
|
||||
SessionID: sessionID,
|
||||
PreviousSessionID: previousSessionID,
|
||||
})
|
||||
cfg, err := loadCommandConfig(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions())
|
||||
if err != nil {
|
||||
return fmt.Errorf("resume: %w", err)
|
||||
}
|
||||
|
||||
@@ -5,55 +5,29 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
)
|
||||
|
||||
// Run executes the pipeline plan and persists manifest state.
|
||||
func Run(ctx context.Context, args []string, out io.Writer) error {
|
||||
positionalSessionID, args := pullLeadingSessionID(args)
|
||||
fs := flag.NewFlagSet("run", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
var pipelinePath string
|
||||
var campaignPath string
|
||||
var campaignFilePath string
|
||||
var sessionPath string
|
||||
var sessionID string
|
||||
var previousSessionID string
|
||||
var flags commonConfigFlags
|
||||
var force bool
|
||||
var selectedArtifacts artifactSelectionFlag
|
||||
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
||||
fs.StringVar(&campaignPath, "campaign", "", "campaign ID")
|
||||
fs.StringVar(&campaignFilePath, "campaign-file", "", "path to campaign.yml")
|
||||
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
||||
fs.StringVar(&previousSessionID, "previous-session-id", "", "expected previous session identifier")
|
||||
addCommonConfigFlags(fs, &flags)
|
||||
fs.BoolVar(&force, "force", false, "force stage execution (reserved for future behavior)")
|
||||
fs.Var(&selectedArtifacts, "artifacts", "configured artifact names to execute and publish (comma-separated or repeatable)")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("run: invalid flags: %w", err)
|
||||
if err := parseSessionAwareFlags("run", fs, args, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if positionalSessionID == "" {
|
||||
if err := applyParsedSessionIDArg("run", fs, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("run: unexpected positional arguments")
|
||||
}
|
||||
if err := applyPositionalSessionID("run", positionalSessionID, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(sessionID) == "" {
|
||||
if flags.sessionID == "" {
|
||||
return fmt.Errorf("run: session_id is required")
|
||||
}
|
||||
cfg, err := loadCommandConfig(ctx, pipelinePath, campaignPath, campaignFilePath, sessionPath, config.SessionLoadOptions{
|
||||
SessionID: sessionID,
|
||||
PreviousSessionID: previousSessionID,
|
||||
})
|
||||
cfg, err := loadCommandConfig(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions())
|
||||
if err != nil {
|
||||
return fmt.Errorf("run: %w", err)
|
||||
}
|
||||
|
||||
@@ -23,19 +23,10 @@ func RunStage(ctx context.Context, args []string, out io.Writer) error {
|
||||
fs := flag.NewFlagSet("run-stage", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
var pipelinePath string
|
||||
var campaignPath string
|
||||
var campaignFilePath string
|
||||
var sessionPath string
|
||||
var sessionID string
|
||||
var previousSessionID string
|
||||
var flags commonConfigFlags
|
||||
var force bool
|
||||
var selectedArtifacts artifactSelectionFlag
|
||||
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
||||
fs.StringVar(&campaignPath, "campaign", "", "campaign ID")
|
||||
fs.StringVar(&campaignFilePath, "campaign-file", "", "path to campaign.yml")
|
||||
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
||||
fs.StringVar(&previousSessionID, "previous-session-id", "", "expected previous session identifier")
|
||||
addCommonConfigFlags(fs, &flags)
|
||||
fs.BoolVar(&force, "force", false, "force stage execution (reserved for future behavior)")
|
||||
fs.Var(&selectedArtifacts, "artifacts", "configured artifact names to execute or publish (comma-separated or repeatable)")
|
||||
|
||||
@@ -47,16 +38,21 @@ func RunStage(ctx context.Context, args []string, out io.Writer) error {
|
||||
case 2:
|
||||
stageName = strings.TrimSpace(fs.Arg(0))
|
||||
positionalSessionID = strings.TrimSpace(fs.Arg(1))
|
||||
case 1:
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("run-stage: expected stage name and session_id")
|
||||
}
|
||||
stageName = strings.TrimSpace(fs.Arg(0))
|
||||
default:
|
||||
return fmt.Errorf("run-stage: expected stage name and session_id")
|
||||
}
|
||||
} else if fs.NArg() != 0 {
|
||||
return fmt.Errorf("run-stage: unexpected positional arguments")
|
||||
}
|
||||
if err := applyPositionalSessionID("run-stage", positionalSessionID, &sessionID); err != nil {
|
||||
if err := applyPositionalSessionID("run-stage", positionalSessionID, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(sessionID) == "" {
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("run-stage: session_id is required")
|
||||
}
|
||||
normalizedArtifacts, err := selectedArtifacts.Normalize()
|
||||
@@ -70,12 +66,12 @@ func RunStage(ctx context.Context, args []string, out io.Writer) error {
|
||||
summary, err := runSingleStageCommand(ctx, singleStageCommand{
|
||||
CommandName: "run-stage",
|
||||
StageName: stageName,
|
||||
PipelinePath: pipelinePath,
|
||||
CampaignPath: campaignPath,
|
||||
CampaignFilePath: campaignFilePath,
|
||||
SessionPath: sessionPath,
|
||||
SessionID: sessionID,
|
||||
PreviousSessionID: previousSessionID,
|
||||
PipelinePath: flags.pipelinePath,
|
||||
CampaignPath: flags.campaignPath,
|
||||
CampaignFilePath: flags.campaignFilePath,
|
||||
SessionPath: flags.sessionPath,
|
||||
SessionID: flags.sessionID,
|
||||
PreviousSessionID: flags.previousSessionID,
|
||||
Force: force,
|
||||
SelectedArtifacts: normalizedArtifacts,
|
||||
})
|
||||
@@ -97,40 +93,18 @@ func RunStage(ctx context.Context, args []string, out io.Writer) error {
|
||||
|
||||
// Analyze force-runs the analyze stage.
|
||||
func Analyze(ctx context.Context, args []string, out io.Writer) error {
|
||||
positionalSessionID, args := pullLeadingSessionID(args)
|
||||
fs := flag.NewFlagSet("analyze", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
var pipelinePath string
|
||||
var campaignPath string
|
||||
var campaignFilePath string
|
||||
var sessionPath string
|
||||
var sessionID string
|
||||
var previousSessionID string
|
||||
var flags commonConfigFlags
|
||||
var selectedArtifacts artifactSelectionFlag
|
||||
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
||||
fs.StringVar(&campaignPath, "campaign", "", "campaign ID")
|
||||
fs.StringVar(&campaignFilePath, "campaign-file", "", "path to campaign.yml")
|
||||
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
||||
fs.StringVar(&previousSessionID, "previous-session-id", "", "expected previous session identifier")
|
||||
addCommonConfigFlags(fs, &flags)
|
||||
fs.Var(&selectedArtifacts, "artifacts", "configured artifact names to execute during analyze (comma-separated or repeatable)")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("analyze: invalid flags: %w", err)
|
||||
if err := parseSessionAwareFlags("analyze", fs, args, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if positionalSessionID == "" {
|
||||
if err := applyParsedSessionIDArg("analyze", fs, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("analyze: unexpected positional arguments")
|
||||
}
|
||||
if err := applyPositionalSessionID("analyze", positionalSessionID, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(sessionID) == "" {
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("analyze: session_id is required")
|
||||
}
|
||||
normalizedArtifacts, err := selectedArtifacts.Normalize()
|
||||
@@ -141,12 +115,12 @@ func Analyze(ctx context.Context, args []string, out io.Writer) error {
|
||||
summary, err := runSingleStageCommand(ctx, singleStageCommand{
|
||||
CommandName: "analyze",
|
||||
StageName: "analyze",
|
||||
PipelinePath: pipelinePath,
|
||||
CampaignPath: campaignPath,
|
||||
CampaignFilePath: campaignFilePath,
|
||||
SessionPath: sessionPath,
|
||||
SessionID: sessionID,
|
||||
PreviousSessionID: previousSessionID,
|
||||
PipelinePath: flags.pipelinePath,
|
||||
CampaignPath: flags.campaignPath,
|
||||
CampaignFilePath: flags.campaignFilePath,
|
||||
SessionPath: flags.sessionPath,
|
||||
SessionID: flags.sessionID,
|
||||
PreviousSessionID: flags.previousSessionID,
|
||||
Force: true,
|
||||
SelectedArtifacts: normalizedArtifacts,
|
||||
})
|
||||
@@ -166,40 +140,18 @@ func Analyze(ctx context.Context, args []string, out io.Writer) error {
|
||||
|
||||
// Publish force-runs the publish stage.
|
||||
func Publish(ctx context.Context, args []string, out io.Writer) error {
|
||||
positionalSessionID, args := pullLeadingSessionID(args)
|
||||
fs := flag.NewFlagSet("publish", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
var pipelinePath string
|
||||
var campaignPath string
|
||||
var campaignFilePath string
|
||||
var sessionPath string
|
||||
var sessionID string
|
||||
var previousSessionID string
|
||||
var flags commonConfigFlags
|
||||
var selectedArtifacts artifactSelectionFlag
|
||||
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
||||
fs.StringVar(&campaignPath, "campaign", "", "campaign ID")
|
||||
fs.StringVar(&campaignFilePath, "campaign-file", "", "path to campaign.yml")
|
||||
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
||||
fs.StringVar(&previousSessionID, "previous-session-id", "", "expected previous session identifier")
|
||||
addCommonConfigFlags(fs, &flags)
|
||||
fs.Var(&selectedArtifacts, "artifacts", "configured artifact names to publish (comma-separated or repeatable)")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("publish: invalid flags: %w", err)
|
||||
if err := parseSessionAwareFlags("publish", fs, args, &flags.sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
if positionalSessionID == "" {
|
||||
if err := applyParsedSessionIDArg("publish", fs, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("publish: unexpected positional arguments")
|
||||
}
|
||||
if err := applyPositionalSessionID("publish", positionalSessionID, &sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(sessionID) == "" {
|
||||
if strings.TrimSpace(flags.sessionID) == "" {
|
||||
return fmt.Errorf("publish: session_id is required")
|
||||
}
|
||||
normalizedArtifacts, err := selectedArtifacts.Normalize()
|
||||
@@ -210,12 +162,12 @@ func Publish(ctx context.Context, args []string, out io.Writer) error {
|
||||
summary, err := runSingleStageCommand(ctx, singleStageCommand{
|
||||
CommandName: "publish",
|
||||
StageName: "publish",
|
||||
PipelinePath: pipelinePath,
|
||||
CampaignPath: campaignPath,
|
||||
CampaignFilePath: campaignFilePath,
|
||||
SessionPath: sessionPath,
|
||||
SessionID: sessionID,
|
||||
PreviousSessionID: previousSessionID,
|
||||
PipelinePath: flags.pipelinePath,
|
||||
CampaignPath: flags.campaignPath,
|
||||
CampaignFilePath: flags.campaignFilePath,
|
||||
SessionPath: flags.sessionPath,
|
||||
SessionID: flags.sessionID,
|
||||
PreviousSessionID: flags.previousSessionID,
|
||||
Force: true,
|
||||
SelectedArtifacts: normalizedArtifacts,
|
||||
})
|
||||
|
||||
@@ -41,3 +41,21 @@ func applyParsedSessionIDArg(command string, fs *flag.FlagSet, sessionID *string
|
||||
return fmt.Errorf("%s: unexpected positional arguments", command)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveParsedSessionID(command, positionalSessionID string, fs *flag.FlagSet, sessionID *string) error {
|
||||
if strings.TrimSpace(positionalSessionID) == "" {
|
||||
return applyParsedSessionIDArg(command, fs, sessionID)
|
||||
}
|
||||
if fs.NArg() != 0 {
|
||||
return fmt.Errorf("%s: unexpected positional arguments", command)
|
||||
}
|
||||
return applyPositionalSessionID(command, positionalSessionID, sessionID)
|
||||
}
|
||||
|
||||
func parseSessionAwareFlags(command string, fs *flag.FlagSet, args []string, sessionID *string) error {
|
||||
positionalSessionID, args := pullLeadingSessionID(args)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("%s: invalid flags: %w", command, err)
|
||||
}
|
||||
return resolveParsedSessionID(command, positionalSessionID, fs, sessionID)
|
||||
}
|
||||
|
||||
@@ -71,15 +71,36 @@ func TestExecutePositionalSessionIDMismatchFails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSessionIDFlagFails(t *testing.T) {
|
||||
func TestExecuteSessionIDFlagMismatchFails(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{"session", "status", "2026-05-03", "--session-id", "2026-05-04"}, &stdout, &stderr)
|
||||
if code == 0 {
|
||||
t.Fatal("exit code = 0, want non-zero")
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "flag provided but not defined: -session-id") {
|
||||
t.Fatalf("stderr = %q, want invalid --session-id flag", stderr.String())
|
||||
if !strings.Contains(stderr.String(), "does not match expected session id") {
|
||||
t.Fatalf("stderr = %q, want positional/flag mismatch", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSessionIDFlagAcceptedWithoutPositional(t *testing.T) {
|
||||
workspaceRoot := t.TempDir()
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{
|
||||
"session", "status",
|
||||
"--session-id", "2026-05-03",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "Session: 2026-05-03") {
|
||||
t.Fatalf("stdout = %q, want status output", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user