@@ -204,12 +204,82 @@ Examples:
204204 },
205205}
206206
207+ var keyCmd = & cobra.Command {
208+ Use : "key" ,
209+ Short : "Manage encryption keys" ,
210+ Long : `Manage encryption keys for secure syncing of auth tokens.` ,
211+ RunE : func (cmd * cobra.Command , args []string ) error {
212+ return runKeyExport ()
213+ },
214+ }
215+
216+ var keyExportCmd = & cobra.Command {
217+ Use : "export" ,
218+ Short : "Export private key for backup" ,
219+ Long : `Export your private encryption key.
220+
221+ IMPORTANT: Store this key securely (e.g., password manager).
222+ Without it, encrypted data (auth tokens) cannot be recovered.` ,
223+ RunE : func (cmd * cobra.Command , args []string ) error {
224+ return runKeyExport ()
225+ },
226+ }
227+
228+ var keyImportCmd = & cobra.Command {
229+ Use : "import <key>" ,
230+ Short : "Import a private key" ,
231+ Long : `Import a private key from backup.
232+
233+ Use this when setting up a new machine to decrypt existing auth tokens.
234+
235+ Example:
236+ opencode-sync key import "AGE-SECRET-KEY-1QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ"` ,
237+ Args : cobra .ExactArgs (1 ),
238+ RunE : func (cmd * cobra.Command , args []string ) error {
239+ return runKeyImport (args [0 ])
240+ },
241+ }
242+
243+ var keyRegenCmd = & cobra.Command {
244+ Use : "regen" ,
245+ Short : "Regenerate encryption key" ,
246+ Long : `Generate a new encryption key, replacing the existing one.
247+
248+ WARNING: Previously encrypted data will become unrecoverable!
249+ Only use this if you've lost your key and need to start fresh.` ,
250+ RunE : func (cmd * cobra.Command , args []string ) error {
251+ return runKeyRegen ()
252+ },
253+ }
254+
255+ var rebindCmd = & cobra.Command {
256+ Use : "rebind <url>" ,
257+ Short : "Change the remote repository URL" ,
258+ Long : `Change the remote repository URL without reinitializing.
259+
260+ This updates the remote URL for an existing sync repository.
261+ Useful when migrating to a new git host or changing repo location.
262+
263+ Examples:
264+ opencode-sync rebind git@github.com:user/new-repo.git
265+ opencode-sync rebind https://github.com/user/new-repo.git` ,
266+ Args : cobra .ExactArgs (1 ),
267+ RunE : func (cmd * cobra.Command , args []string ) error {
268+ return runRebind (args [0 ])
269+ },
270+ }
271+
207272func init () {
208273 // Add config subcommands
209274 configCmd .AddCommand (configShowCmd )
210275 configCmd .AddCommand (configPathCmd )
211276 configCmd .AddCommand (configEditCmd )
212277 configCmd .AddCommand (configSetCmd )
278+
279+ // Add key subcommands
280+ keyCmd .AddCommand (keyExportCmd )
281+ keyCmd .AddCommand (keyImportCmd )
282+ keyCmd .AddCommand (keyRegenCmd )
213283}
214284
215285// Command implementations
@@ -1027,11 +1097,184 @@ func runClone(repoURL string) error {
10271097 return nil
10281098}
10291099
1030- // getHostname returns the hostname or "unknown"
10311100func getHostname () string {
10321101 hostname , err := os .Hostname ()
10331102 if err != nil {
10341103 return "unknown"
10351104 }
10361105 return hostname
10371106}
1107+
1108+ func runKeyExport () error {
1109+ p , err := paths .Get ()
1110+ if err != nil {
1111+ return fmt .Errorf ("failed to get paths: %w" , err )
1112+ }
1113+
1114+ keyFile := p .KeyFile ()
1115+ if _ , err := os .Stat (keyFile ); os .IsNotExist (err ) {
1116+ return fmt .Errorf ("no encryption key found. Run 'opencode-sync setup' with encryption enabled first" )
1117+ }
1118+
1119+ privateKey , err := crypto .LoadKeyFromFile (keyFile )
1120+ if err != nil {
1121+ return fmt .Errorf ("failed to load key: %w" , err )
1122+ }
1123+
1124+ ui .Warn ("PRIVATE KEY - Store securely! Anyone with this key can decrypt your auth tokens." )
1125+ fmt .Println ()
1126+ fmt .Println (privateKey )
1127+ fmt .Println ()
1128+ ui .Info ("Copy this key to your password manager or secure storage." )
1129+ ui .Info ("Use 'opencode-sync key import <key>' on other machines." )
1130+
1131+ return nil
1132+ }
1133+
1134+ func runKeyImport (key string ) error {
1135+ if _ , err := crypto .NewAgeEncryption (key ); err != nil {
1136+ return fmt .Errorf ("invalid key format: %w" , err )
1137+ }
1138+
1139+ p , err := paths .Get ()
1140+ if err != nil {
1141+ return fmt .Errorf ("failed to get paths: %w" , err )
1142+ }
1143+
1144+ if err := p .EnsureDirs (); err != nil {
1145+ return fmt .Errorf ("failed to create directories: %w" , err )
1146+ }
1147+
1148+ keyFile := p .KeyFile ()
1149+ if _ , err := os .Stat (keyFile ); err == nil {
1150+ confirmed , err := ui .Confirm ("Key already exists. Overwrite?" , "This will replace your existing encryption key" )
1151+ if err != nil {
1152+ return err
1153+ }
1154+ if ! confirmed {
1155+ ui .Info ("Import cancelled" )
1156+ return nil
1157+ }
1158+ }
1159+
1160+ if err := crypto .SaveKeyToFile (key , keyFile ); err != nil {
1161+ return fmt .Errorf ("failed to save key: %w" , err )
1162+ }
1163+
1164+ cfg , err := config .Load ()
1165+ if err == nil && cfg != nil {
1166+ cfg .Encryption .Enabled = true
1167+ if err := config .Save (cfg ); err != nil {
1168+ ui .Warn ("Key saved but failed to update config. Run: opencode-sync config set encryption.enabled true" )
1169+ }
1170+ }
1171+
1172+ ui .Success (fmt .Sprintf ("Key imported to: %s" , keyFile ))
1173+ ui .Info ("You can now pull encrypted data from your repo." )
1174+
1175+ return nil
1176+ }
1177+
1178+ func runKeyRegen () error {
1179+ ui .Warn ("WARNING: Regenerating your key will make previously encrypted data unrecoverable!" )
1180+ ui .Warn ("Only proceed if you've lost your key and need to start fresh." )
1181+ fmt .Println ()
1182+
1183+ confirmed , err := ui .Confirm ("Regenerate encryption key?" , "Previously encrypted auth tokens will be lost" )
1184+ if err != nil {
1185+ return err
1186+ }
1187+ if ! confirmed {
1188+ ui .Info ("Cancelled" )
1189+ return nil
1190+ }
1191+
1192+ p , err := paths .Get ()
1193+ if err != nil {
1194+ return fmt .Errorf ("failed to get paths: %w" , err )
1195+ }
1196+
1197+ if err := p .EnsureDirs (); err != nil {
1198+ return fmt .Errorf ("failed to create directories: %w" , err )
1199+ }
1200+
1201+ keyPair , err := crypto .GenerateKey ()
1202+ if err != nil {
1203+ return fmt .Errorf ("failed to generate key: %w" , err )
1204+ }
1205+
1206+ keyFile := p .KeyFile ()
1207+ if err := crypto .SaveKeyToFile (keyPair .PrivateKey , keyFile ); err != nil {
1208+ return fmt .Errorf ("failed to save key: %w" , err )
1209+ }
1210+
1211+ cfg , err := config .Load ()
1212+ if err == nil && cfg != nil {
1213+ cfg .Encryption .Enabled = true
1214+ if err := config .Save (cfg ); err != nil {
1215+ ui .Warn ("Key saved but failed to update config" )
1216+ }
1217+ }
1218+
1219+ ui .Success (fmt .Sprintf ("New encryption key saved to: %s" , keyFile ))
1220+ fmt .Println ()
1221+ ui .Warn ("IMPORTANT: Back up your new key!" )
1222+ ui .Info ("Run 'opencode-sync key export' to view it for backup." )
1223+
1224+ return nil
1225+ }
1226+
1227+ func runRebind (newURL string ) error {
1228+ cfg , err := config .Load ()
1229+ if err != nil {
1230+ return fmt .Errorf ("failed to load config: %w" , err )
1231+ }
1232+ if cfg == nil {
1233+ return fmt .Errorf ("no configuration found. Run 'opencode-sync setup' first" )
1234+ }
1235+
1236+ p , err := paths .Get ()
1237+ if err != nil {
1238+ return fmt .Errorf ("failed to get paths: %w" , err )
1239+ }
1240+
1241+ repoDir := p .SyncRepoDir ()
1242+ gitDir := filepath .Join (repoDir , ".git" )
1243+
1244+ if _ , err := os .Stat (gitDir ); os .IsNotExist (err ) {
1245+ return fmt .Errorf ("no repository found. Run 'opencode-sync init' or 'opencode-sync clone' first" )
1246+ }
1247+
1248+ oldURL := cfg .Repo .URL
1249+ if oldURL == newURL {
1250+ ui .Info ("URL is already set to: " + newURL )
1251+ return nil
1252+ }
1253+
1254+ ui .Info (fmt .Sprintf ("Changing remote URL from: %s" , oldURL ))
1255+ ui .Info (fmt .Sprintf (" to: %s" , newURL ))
1256+
1257+ if err := runGitCommand (repoDir , "remote" , "set-url" , "origin" , newURL ); err != nil {
1258+ return fmt .Errorf ("failed to update git remote: %w" , err )
1259+ }
1260+
1261+ cfg .Repo .URL = newURL
1262+ if err := config .Save (cfg ); err != nil {
1263+ return fmt .Errorf ("failed to save config: %w" , err )
1264+ }
1265+
1266+ ui .Success ("Repository URL updated!" )
1267+ ui .Info ("Run 'opencode-sync sync' to sync with the new remote." )
1268+
1269+ return nil
1270+ }
1271+
1272+ func runGitCommand (dir string , args ... string ) error {
1273+ cmd := exec .Command ("git" , args ... )
1274+ if dir != "" {
1275+ cmd .Dir = dir
1276+ }
1277+ cmd .Stdout = os .Stdout
1278+ cmd .Stderr = os .Stderr
1279+ return cmd .Run ()
1280+ }
0 commit comments