From c06fd2edfc153df8c2757605576b66fe9bf9b7fb Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 07:16:56 +0000 Subject: [PATCH] feat(dev): add confirmation to file sync Add a confirmation gate and --yes bypass to core dev sync so batch file distribution follows the same safety model as the other AI-agent commands. --- cmd/dev/cmd_file_sync.go | 33 ++++++++++++++------------------- cmd/dev/cmd_file_sync_test.go | 26 ++++++++++++++++++++++++++ locales/en.json | 5 ++++- 3 files changed, 44 insertions(+), 20 deletions(-) create mode 100644 cmd/dev/cmd_file_sync_test.go diff --git a/cmd/dev/cmd_file_sync.go b/cmd/dev/cmd_file_sync.go index 06c0299..93e4b86 100644 --- a/cmd/dev/cmd_file_sync.go +++ b/cmd/dev/cmd_file_sync.go @@ -14,12 +14,12 @@ import ( "path/filepath" "strings" - "forge.lthn.ai/core/cli/pkg/cli" - "dappco.re/go/core/scm/git" "dappco.re/go/core/i18n" coreio "dappco.re/go/core/io" "dappco.re/go/core/log" + "dappco.re/go/core/scm/git" "dappco.re/go/core/scm/repos" + "forge.lthn.ai/core/cli/pkg/cli" ) // File sync command flags @@ -29,6 +29,7 @@ var ( fileSyncCoAuthor string fileSyncDryRun bool fileSyncPush bool + fileSyncYes bool ) // AddFileSyncCommand adds the 'sync' command to dev for file syncing. @@ -48,6 +49,7 @@ func AddFileSyncCommand(parent *cli.Command) { syncCmd.Flags().StringVar(&fileSyncCoAuthor, "co-author", "", i18n.T("cmd.dev.file_sync.flag.co_author")) syncCmd.Flags().BoolVar(&fileSyncDryRun, "dry-run", false, i18n.T("cmd.dev.file_sync.flag.dry_run")) syncCmd.Flags().BoolVar(&fileSyncPush, "push", false, i18n.T("cmd.dev.file_sync.flag.push")) + syncCmd.Flags().BoolVarP(&fileSyncYes, "yes", "y", false, i18n.T("cmd.dev.file_sync.flag.yes")) _ = syncCmd.MarkFlagRequired("to") @@ -64,23 +66,6 @@ func runFileSync(source string) error { // Validate source exists sourceInfo, err := os.Stat(source) // Keep os.Stat for local source check or use coreio? coreio.Local.IsFile is bool. - // If source is local file on disk (not in medium), we can use os.Stat. - // But concept is everything is via Medium? - // User is running CLI on host. `source` is relative to CWD. - // coreio.Local uses absolute path or relative to root (which is "/" by default). - // So coreio.Local works. - if !coreio.Local.IsFile(source) { - // Might be directory - // IsFile returns false for directory. - } - // Let's rely on os.Stat for initial source check to distinguish dir vs file easily if coreio doesn't expose Stat. - // coreio doesn't expose Stat. - - // Check using standard os for source determination as we are outside strict sandbox for input args potentially? - // But we should use coreio where possible. - // coreio.Local.List worked for dirs. - // Let's stick to os.Stat for source properties finding as typically allowed for CLI args. - if err != nil { return log.E("dev.sync", i18n.T("cmd.dev.file_sync.error.source_not_found", map[string]any{"Path": source}), err) } @@ -103,6 +88,16 @@ func runFileSync(source string) error { } cli.Blank() + if !fileSyncDryRun && !fileSyncYes { + cli.Print("%s\n", warningStyle.Render(i18n.T("cmd.dev.file_sync.warning"))) + cli.Blank() + if !cli.Confirm(i18n.T("cmd.dev.file_sync.confirm")) { + cli.Text(i18n.T("cli.aborted")) + return nil + } + cli.Blank() + } + var succeeded, skipped, failed int for _, repo := range targetRepos { diff --git a/cmd/dev/cmd_file_sync_test.go b/cmd/dev/cmd_file_sync_test.go new file mode 100644 index 0000000..4498895 --- /dev/null +++ b/cmd/dev/cmd_file_sync_test.go @@ -0,0 +1,26 @@ +package dev + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "forge.lthn.ai/core/cli/pkg/cli" +) + +func TestAddFileSyncCommand_Good(t *testing.T) { + root := &cli.Command{Use: "core"} + + AddDevCommands(root) + + syncCmd, _, err := root.Find([]string{"dev", "sync"}) + require.NoError(t, err) + require.NotNil(t, syncCmd) + + yesFlag := syncCmd.Flags().Lookup("yes") + require.NotNil(t, yesFlag) + require.Equal(t, "y", yesFlag.Shorthand) + + require.NotNil(t, syncCmd.Flags().Lookup("dry-run")) + require.NotNil(t, syncCmd.Flags().Lookup("push")) +} diff --git a/locales/en.json b/locales/en.json index 0e14191..090a768 100644 --- a/locales/en.json +++ b/locales/en.json @@ -173,6 +173,8 @@ "long": "Copy a file or directory to matching repos, optionally committing and pushing the changes.\n\nDesigned for safe file distribution by AI agents.", "source": "Source", "targets": "Targets", + "warning": "This will copy files into each target repo.", + "confirm": "Sync these repos?", "dry_run_mode": "[dry-run] No changes will be made", "no_changes": "no changes", "summary": "Summary", @@ -181,7 +183,8 @@ "message": "Commit message (omit to leave uncommitted)", "push": "Push after committing", "co_author": "Co-author for commits", - "dry_run": "Preview without making changes" + "dry_run": "Preview without making changes", + "yes": "Skip confirmation prompt" }, "error": { "source_not_found": "Source not found: {{.Path}}",