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.
This commit is contained in:
Virgil 2026-04-01 07:16:56 +00:00
parent cbf650918a
commit c06fd2edfc
3 changed files with 44 additions and 20 deletions

View file

@ -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 {

View file

@ -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"))
}

View file

@ -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}}",