Skip to content

Instantly share code, notes, and snippets.

@chhoumann
Created February 27, 2026 21:26
Show Gist options
  • Select an option

  • Save chhoumann/5152bf85ce9c7aa4e51285e04702f6fc to your computer and use it in GitHub Desktop.

Select an option

Save chhoumann/5152bf85ce9c7aa4e51285e04702f6fc to your computer and use it in GitHub Desktop.
QuickAdd safe duplicate-folder macro script and package for issue #785
{
"schemaVersion": 1,
"quickAddVersion": "2.11.0",
"createdAt": "2026-02-27T21:25:54.977Z",
"rootChoiceIds": [
"qa-macro-duplicate-folder-v1"
],
"choices": [
{
"choice": {
"id": "qa-macro-duplicate-folder-v1",
"name": "Duplicate Folder",
"type": "Macro",
"command": true,
"runOnStartup": false,
"macro": {
"id": "qa-macro-duplicate-folder-v1-macro",
"name": "Duplicate Folder",
"commands": [
{
"id": "qa-command-duplicate-folder-script-v1",
"name": "Duplicate Folder Script",
"type": "UserScript",
"path": "Scripts/duplicateFolder.js",
"settings": {}
}
]
}
},
"pathHint": [
"Duplicate Folder"
],
"parentChoiceId": null
}
],
"assets": [
{
"kind": "user-script",
"originalPath": "Scripts/duplicateFolder.js",
"contentEncoding": "base64",
"content": "LyoqCiAqIER1cGxpY2F0ZSBhIGZvbGRlciAoaW5jbHVkaW5nIHN1YmZvbGRlcnMgYW5kIGZpbGVzKSBpbnNpZGUgdGhlIGN1cnJlbnQgdmF1bHQuCiAqCiAqIFNhZmUgZGVmYXVsdHM6CiAqIC0gTmV2ZXIgb3ZlcndyaXRlcyBleGlzdGluZyBmaWxlcy4KICogLSBCbG9ja3Mgb3ZlcmxhcHBpbmcgc291cmNlL2Rlc3RpbmF0aW9uIHBhdGhzLgogKiAtIE9wdGlvbmFsIGRyeS1ydW4gbW9kZS4KICogLSBPcHRpb25hbCBtYXJrZG93biB0ZXh0IHJlcGxhY2VtZW50IGZvciB0ZW1wbGF0aW5nLgogKi8KbW9kdWxlLmV4cG9ydHMgPSB7CiAgICBlbnRyeTogc3RhcnQsCiAgICBzZXR0aW5nczogewogICAgICAgIG5hbWU6ICJEdXBsaWNhdGUgRm9sZGVyIiwKICAgICAgICBhdXRob3I6ICJRdWlja0FkZCIsCiAgICAgICAgb3B0aW9uczogewogICAgICAgICAgICAiRGVmYXVsdCBTb3VyY2UgRm9sZGVyIjogewogICAgICAgICAgICAgICAgdHlwZTogInRleHQiLAogICAgICAgICAgICAgICAgZGVmYXVsdFZhbHVlOiAiIiwKICAgICAgICAgICAgICAgIHBsYWNlaG9sZGVyOiAiUHJvamVjdHMvVGVtcGxhdGUiLAogICAgICAgICAgICAgICAgZGVzY3JpcHRpb246ICJPcHRpb25hbCBkZWZhdWx0IHNvdXJjZSBmb2xkZXIgdG8gcHJlc2VsZWN0IgogICAgICAgICAgICB9LAogICAgICAgICAgICAiRGVzdGluYXRpb24gU3VmZml4IjogewogICAgICAgICAgICAgICAgdHlwZTogInRleHQiLAogICAgICAgICAgICAgICAgZGVmYXVsdFZhbHVlOiAiIChDb3B5KSIsCiAgICAgICAgICAgICAgICBwbGFjZWhvbGRlcjogIiAoQ29weSkiLAogICAgICAgICAgICAgICAgZGVzY3JpcHRpb246ICJEZWZhdWx0IHN1ZmZpeCBhZGRlZCB0byB0aGUgc3VnZ2VzdGVkIGRlc3RpbmF0aW9uIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiQWxsb3cgRXhpc3RpbmcgRGVzdGluYXRpb24gRm9sZGVyIjogewogICAgICAgICAgICAgICAgdHlwZTogInRvZ2dsZSIsCiAgICAgICAgICAgICAgICBkZWZhdWx0VmFsdWU6IGZhbHNlLAogICAgICAgICAgICAgICAgZGVzY3JpcHRpb246ICJJZiBkaXNhYmxlZCwgYWJvcnQgd2hlbiBkZXN0aW5hdGlvbiBmb2xkZXIgYWxyZWFkeSBleGlzdHMiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJTa2lwIEV4aXN0aW5nIEZpbGVzIjogewogICAgICAgICAgICAgICAgdHlwZTogInRvZ2dsZSIsCiAgICAgICAgICAgICAgICBkZWZhdWx0VmFsdWU6IGZhbHNlLAogICAgICAgICAgICAgICAgZGVzY3JpcHRpb246ICJJZiBlbmFibGVkLCBza2lwIGZpbGUgY29sbGlzaW9ucyBpbnN0ZWFkIG9mIGFib3J0aW5nIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiRmluZCBUZXh0IGluIE1hcmtkb3duIjogewogICAgICAgICAgICAgICAgdHlwZTogInRleHQiLAogICAgICAgICAgICAgICAgZGVmYXVsdFZhbHVlOiAiIiwKICAgICAgICAgICAgICAgIHBsYWNlaG9sZGVyOiAiT2xkIFByb2plY3QgTmFtZSIsCiAgICAgICAgICAgICAgICBkZXNjcmlwdGlvbjogIk9wdGlvbmFsIHBsYWluIHRleHQgdG8gcmVwbGFjZSBpbiAubWQgZmlsZXMiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJSZXBsYWNlIFRleHQgaW4gTWFya2Rvd24iOiB7CiAgICAgICAgICAgICAgICB0eXBlOiAidGV4dCIsCiAgICAgICAgICAgICAgICBkZWZhdWx0VmFsdWU6ICIiLAogICAgICAgICAgICAgICAgcGxhY2Vob2xkZXI6ICJOZXcgUHJvamVjdCBOYW1lIiwKICAgICAgICAgICAgICAgIGRlc2NyaXB0aW9uOiAiUmVwbGFjZW1lbnQgdGV4dCBmb3IgbWFya2Rvd24gbm90ZXMiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJEcnkgUnVuIjogewogICAgICAgICAgICAgICAgdHlwZTogInRvZ2dsZSIsCiAgICAgICAgICAgICAgICBkZWZhdWx0VmFsdWU6IGZhbHNlLAogICAgICAgICAgICAgICAgZGVzY3JpcHRpb246ICJQcmV2aWV3IGFjdGlvbnMgd2l0aG91dCB3cml0aW5nIGZpbGVzIgogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfQp9OwoKYXN5bmMgZnVuY3Rpb24gc3RhcnQocGFyYW1zLCBzZXR0aW5ncykgewogICAgY29uc3QgeyBhcHAsIG9ic2lkaWFuLCBxdWlja0FkZEFwaSwgdmFyaWFibGVzIH0gPSBwYXJhbXM7CiAgICBjb25zdCB7IE5vdGljZSwgVEZpbGUsIFRGb2xkZXIsIG5vcm1hbGl6ZVBhdGggfSA9IG9ic2lkaWFuOwoKICAgIGNvbnN0IHNvdXJjZUZvbGRlclBhdGggPSBhd2FpdCBzZWxlY3RTb3VyY2VGb2xkZXIoCiAgICAgICAgYXBwLAogICAgICAgIHF1aWNrQWRkQXBpLAogICAgICAgIHNldHRpbmdzWyJEZWZhdWx0IFNvdXJjZSBGb2xkZXIiXSwKICAgICAgICBub3JtYWxpemVQYXRoCiAgICApOwogICAgaWYgKCFzb3VyY2VGb2xkZXJQYXRoKSB7CiAgICAgICAgbmV3IE5vdGljZSgiRm9sZGVyIGR1cGxpY2F0aW9uIGNhbmNlbGxlZC4iKTsKICAgICAgICByZXR1cm47CiAgICB9CgogICAgY29uc3Qgc291cmNlRm9sZGVyID0gYXBwLnZhdWx0LmdldEFic3RyYWN0RmlsZUJ5UGF0aChzb3VyY2VGb2xkZXJQYXRoKTsKICAgIGlmICghKHNvdXJjZUZvbGRlciBpbnN0YW5jZW9mIFRGb2xkZXIpKSB7CiAgICAgICAgbmV3IE5vdGljZShgU291cmNlIGZvbGRlciBkb2VzIG5vdCBleGlzdDogJHtzb3VyY2VGb2xkZXJQYXRofWApOwogICAgICAgIHJldHVybjsKICAgIH0KCiAgICBjb25zdCBzdWdnZXN0ZWREZXN0aW5hdGlvbiA9IG5vcm1hbGl6ZVBhdGgoCiAgICAgICAgYCR7c291cmNlRm9sZGVyUGF0aH0ke3NldHRpbmdzWyJEZXN0aW5hdGlvbiBTdWZmaXgiXSB8fCAiIChDb3B5KSJ9YAogICAgKTsKICAgIGNvbnN0IGRlc3RpbmF0aW9uSW5wdXQgPSBhd2FpdCBxdWlja0FkZEFwaS5pbnB1dFByb21wdCgKICAgICAgICAiRGVzdGluYXRpb24gZm9sZGVyIiwKICAgICAgICAiV2hlcmUgc2hvdWxkIHRoZSBjb3B5IGJlIGNyZWF0ZWQ/IiwKICAgICAgICBzdWdnZXN0ZWREZXN0aW5hdGlvbgogICAgKTsKCiAgICBpZiAoIWRlc3RpbmF0aW9uSW5wdXQgfHwgIWRlc3RpbmF0aW9uSW5wdXQudHJpbSgpKSB7CiAgICAgICAgbmV3IE5vdGljZSgiRm9sZGVyIGR1cGxpY2F0aW9uIGNhbmNlbGxlZC4iKTsKICAgICAgICByZXR1cm47CiAgICB9CgogICAgY29uc3QgZGVzdGluYXRpb25Gb2xkZXJQYXRoID0gbm9ybWFsaXplUGF0aChkZXN0aW5hdGlvbklucHV0LnRyaW0oKSk7CgogICAgaWYgKCFpc1NhZmVWYXVsdFBhdGgoZGVzdGluYXRpb25Gb2xkZXJQYXRoKSkgewogICAgICAgIG5ldyBOb3RpY2UoIkRlc3RpbmF0aW9uIHBhdGggaXMgaW52YWxpZC4iKTsKICAgICAgICByZXR1cm47CiAgICB9CgogICAgaWYgKGRlc3RpbmF0aW9uRm9sZGVyUGF0aCA9PT0gc291cmNlRm9sZGVyUGF0aCkgewogICAgICAgIG5ldyBOb3RpY2UoIkRlc3RpbmF0aW9uIGNhbm5vdCBiZSB0aGUgc2FtZSBhcyBzb3VyY2UuIik7CiAgICAgICAgcmV0dXJuOwogICAgfQoKICAgIGlmICgKICAgICAgICBpc1N1YlBhdGgoc291cmNlRm9sZGVyUGF0aCwgZGVzdGluYXRpb25Gb2xkZXJQYXRoKSB8fAogICAgICAgIGlzU3ViUGF0aChkZXN0aW5hdGlvbkZvbGRlclBhdGgsIHNvdXJjZUZvbGRlclBhdGgpCiAgICApIHsKICAgICAgICBuZXcgTm90aWNlKAogICAgICAgICAgICAiU291cmNlIGFuZCBkZXN0aW5hdGlvbiBmb2xkZXJzIGNhbm5vdCBvdmVybGFwIChwYXJlbnQvY2hpbGQpLiIKICAgICAgICApOwogICAgICAgIHJldHVybjsKICAgIH0KCiAgICBjb25zdCBhbGxvd0V4aXN0aW5nRGVzdGluYXRpb24gPSAhIXNldHRpbmdzWyJBbGxvdyBFeGlzdGluZyBEZXN0aW5hdGlvbiBGb2xkZXIiXTsKICAgIGNvbnN0IHNraXBFeGlzdGluZ0ZpbGVzID0gISFzZXR0aW5nc1siU2tpcCBFeGlzdGluZyBGaWxlcyJdOwogICAgY29uc3QgZHJ5UnVuID0gISFzZXR0aW5nc1siRHJ5IFJ1biJdOwoKICAgIGNvbnN0IGRlc3RpbmF0aW9uRXhpc3RzID0gYXdhaXQgYXBwLnZhdWx0LmFkYXB0ZXIuZXhpc3RzKGRlc3RpbmF0aW9uRm9sZGVyUGF0aCk7CiAgICBpZiAoZGVzdGluYXRpb25FeGlzdHMgJiYgIWFsbG93RXhpc3RpbmdEZXN0aW5hdGlvbikgewogICAgICAgIG5ldyBOb3RpY2UoCiAgICAgICAgICAgIGBEZXN0aW5hdGlvbiBhbHJlYWR5IGV4aXN0czogJHtkZXN0aW5hdGlvbkZvbGRlclBhdGh9LiBgICsKICAgICAgICAgICAgIkVuYWJsZSAnQWxsb3cgRXhpc3RpbmcgRGVzdGluYXRpb24gRm9sZGVyJyB0byBjb250aW51ZS4iCiAgICAgICAgKTsKICAgICAgICByZXR1cm47CiAgICB9CgogICAgY29uc3Qgc2NhbiA9IHNjYW5Gb2xkZXIoc291cmNlRm9sZGVyLCBURm9sZGVyLCBURmlsZSk7CiAgICBjb25zdCBwbGFuID0gYnVpbGRQbGFuKAogICAgICAgIHNvdXJjZUZvbGRlclBhdGgsCiAgICAgICAgZGVzdGluYXRpb25Gb2xkZXJQYXRoLAogICAgICAgIHNjYW4uZm9sZGVycywKICAgICAgICBzY2FuLmZpbGVzLAogICAgICAgIG5vcm1hbGl6ZVBhdGgKICAgICk7CgogICAgaWYgKHBsYW4uaW52YWxpZFBhdGhzLmxlbmd0aCA+IDApIHsKICAgICAgICBuZXcgTm90aWNlKAogICAgICAgICAgICBgQWJvcnRlZCBkdWUgdG8gaW52YWxpZCBwYXRoIG1hcHBpbmc6ICR7cGxhbi5pbnZhbGlkUGF0aHNbMF19YAogICAgICAgICk7CiAgICAgICAgcmV0dXJuOwogICAgfQoKICAgIGNvbnN0IGV4aXN0aW5nVGFyZ2V0cyA9IGF3YWl0IGZpbmRFeGlzdGluZ1RhcmdldHMoYXBwLCBwbGFuKTsKICAgIGlmIChleGlzdGluZ1RhcmdldHMuZmlsZVBhdGhzLmxlbmd0aCA+IDAgJiYgIXNraXBFeGlzdGluZ0ZpbGVzKSB7CiAgICAgICAgbmV3IE5vdGljZSgKICAgICAgICAgICAgYEFib3J0ZWQ6ICR7ZXhpc3RpbmdUYXJnZXRzLmZpbGVQYXRocy5sZW5ndGh9IGZpbGUocykgYWxyZWFkeSBleGlzdCBpbiBkZXN0aW5hdGlvbi5gCiAgICAgICAgKTsKICAgICAgICByZXR1cm47CiAgICB9CgogICAgY29uc3QgZmluZFRleHQgPSBgJHtzZXR0aW5nc1siRmluZCBUZXh0IGluIE1hcmtkb3duIl0gfHwgIiJ9YDsKICAgIGNvbnN0IHJlcGxhY2VUZXh0ID0gYCR7c2V0dGluZ3NbIlJlcGxhY2UgVGV4dCBpbiBNYXJrZG93biJdIHx8ICIifWA7CiAgICBjb25zdCBzaG91bGRSZXBsYWNlSW5NYXJrZG93biA9IGZpbmRUZXh0Lmxlbmd0aCA+IDA7CgogICAgY29uc3QgcHJldmlld0xpbmVzID0gWwogICAgICAgIGBTb3VyY2U6ICR7c291cmNlRm9sZGVyUGF0aH1gLAogICAgICAgIGBEZXN0aW5hdGlvbjogJHtkZXN0aW5hdGlvbkZvbGRlclBhdGh9YCwKICAgICAgICBgRm9sZGVycyB0byBjcmVhdGU6ICR7cGxhbi5mb2xkZXJQYXRocy5sZW5ndGh9YCwKICAgICAgICBgRmlsZXMgdG8gY29weTogJHtwbGFuLmZpbGVQbGFucy5sZW5ndGh9YCwKICAgICAgICBgRXhpc3RpbmcgZmlsZXM6ICR7ZXhpc3RpbmdUYXJnZXRzLmZpbGVQYXRocy5sZW5ndGh9YCwKICAgICAgICBgRHJ5IHJ1bjogJHtkcnlSdW4gPyAieWVzIiA6ICJubyJ9YCwKICAgIF07CgogICAgaWYgKHNob3VsZFJlcGxhY2VJbk1hcmtkb3duKSB7CiAgICAgICAgcHJldmlld0xpbmVzLnB1c2goCiAgICAgICAgICAgIGBNYXJrZG93biByZXBsYWNlbWVudDogXCIke2ZpbmRUZXh0fVwiIOKGkiBcIiR7cmVwbGFjZVRleHR9XCJgCiAgICAgICAgKTsKICAgIH0KCiAgICBjb25zdCBzaG91bGRDb250aW51ZSA9IGF3YWl0IHF1aWNrQWRkQXBpLnllc05vUHJvbXB0KAogICAgICAgICJEdXBsaWNhdGUgZm9sZGVyPyIsCiAgICAgICAgcHJldmlld0xpbmVzLmpvaW4oIlxuIikKICAgICk7CiAgICBpZiAoIXNob3VsZENvbnRpbnVlKSB7CiAgICAgICAgbmV3IE5vdGljZSgiRm9sZGVyIGR1cGxpY2F0aW9uIGNhbmNlbGxlZC4iKTsKICAgICAgICByZXR1cm47CiAgICB9CgogICAgbGV0IHJlc3VsdDsKICAgIHRyeSB7CiAgICAgICAgcmVzdWx0ID0gYXdhaXQgZXhlY3V0ZVBsYW4oewogICAgICAgICAgICBhcHAsCiAgICAgICAgICAgIHBsYW4sCiAgICAgICAgICAgIGV4aXN0aW5nVGFyZ2V0cywKICAgICAgICAgICAgc2tpcEV4aXN0aW5nRmlsZXMsCiAgICAgICAgICAgIHNob3VsZFJlcGxhY2VJbk1hcmtkb3duLAogICAgICAgICAgICBmaW5kVGV4dCwKICAgICAgICAgICAgcmVwbGFjZVRleHQsCiAgICAgICAgICAgIGRyeVJ1biwKICAgICAgICB9KTsKICAgIH0gY2F0Y2ggKGVycm9yKSB7CiAgICAgICAgY29uc3QgbWVzc2FnZSA9IGVycm9yPy5tZXNzYWdlIHx8IGAke2Vycm9yfWA7CiAgICAgICAgbmV3IE5vdGljZShgRm9sZGVyIGR1cGxpY2F0aW9uIGZhaWxlZDogJHttZXNzYWdlfWAsIDgwMDApOwogICAgICAgIHRocm93IGVycm9yOwogICAgfQoKICAgIHZhcmlhYmxlcy5kdXBsaWNhdGVkU291cmNlRm9sZGVyID0gc291cmNlRm9sZGVyUGF0aDsKICAgIHZhcmlhYmxlcy5kdXBsaWNhdGVkRGVzdGluYXRpb25Gb2xkZXIgPSBkZXN0aW5hdGlvbkZvbGRlclBhdGg7CiAgICB2YXJpYWJsZXMuZHVwbGljYXRlZEZvbGRlcnNDcmVhdGVkID0gcmVzdWx0LmZvbGRlcnNDcmVhdGVkOwogICAgdmFyaWFibGVzLmR1cGxpY2F0ZWRGaWxlc0NvcGllZCA9IHJlc3VsdC5maWxlc0NvcGllZDsKICAgIHZhcmlhYmxlcy5kdXBsaWNhdGVkRmlsZXNTa2lwcGVkID0gcmVzdWx0LmZpbGVzU2tpcHBlZDsKICAgIHZhcmlhYmxlcy5kdXBsaWNhdGVkTWFya2Rvd25GaWxlc1VwZGF0ZWQgPSByZXN1bHQubWFya2Rvd25GaWxlc1VwZGF0ZWQ7CgogICAgY29uc3Qgc3VtbWFyeSA9IGRyeVJ1bgogICAgICAgID8gYERyeSBydW4gY29tcGxldGUuIFBsYW5uZWQgJHtwbGFuLmZvbGRlclBhdGhzLmxlbmd0aH0gZm9sZGVyKHMpIGFuZCAke3BsYW4uZmlsZVBsYW5zLmxlbmd0aH0gZmlsZShzKS5gCiAgICAgICAgOiBgRHVwbGljYXRlZCBmb2xkZXIgc3VjY2Vzc2Z1bGx5LiBDcmVhdGVkICR7cmVzdWx0LmZvbGRlcnNDcmVhdGVkfSBmb2xkZXIocyksIGNvcGllZCAke3Jlc3VsdC5maWxlc0NvcGllZH0gZmlsZShzKSwgc2tpcHBlZCAke3Jlc3VsdC5maWxlc1NraXBwZWR9LmA7CgogICAgbmV3IE5vdGljZShzdW1tYXJ5LCA4MDAwKTsKfQoKYXN5bmMgZnVuY3Rpb24gc2VsZWN0U291cmNlRm9sZGVyKGFwcCwgcXVpY2tBZGRBcGksIGRlZmF1bHRQYXRoLCBub3JtYWxpemVQYXRoKSB7CiAgICBjb25zdCBmb2xkZXJzID0gYXBwLnZhdWx0CiAgICAgICAgLmdldEFsbExvYWRlZEZpbGVzKCkKICAgICAgICAuZmlsdGVyKChmaWxlKSA9PiBmaWxlICYmIEFycmF5LmlzQXJyYXkoZmlsZS5jaGlsZHJlbikpCiAgICAgICAgLm1hcCgoZm9sZGVyKSA9PiBmb2xkZXIucGF0aCkKICAgICAgICAuZmlsdGVyKChwYXRoKSA9PiAhIXBhdGgpCiAgICAgICAgLnNvcnQoKGEsIGIpID0+IGEubG9jYWxlQ29tcGFyZShiKSk7CgogICAgaWYgKGZvbGRlcnMubGVuZ3RoID09PSAwKSB7CiAgICAgICAgcmV0dXJuIG51bGw7CiAgICB9CgogICAgY29uc3Qgbm9ybWFsaXplZERlZmF1bHQgPSBkZWZhdWx0UGF0aCA/IG5vcm1hbGl6ZVBhdGgoZGVmYXVsdFBhdGgudHJpbSgpKSA6ICIiOwogICAgY29uc3QgZGVmYXVsdEV4aXN0cyA9IG5vcm1hbGl6ZWREZWZhdWx0ICYmIGZvbGRlcnMuaW5jbHVkZXMobm9ybWFsaXplZERlZmF1bHQpOwoKICAgIGNvbnN0IHNvdXJjZSA9IGF3YWl0IHF1aWNrQWRkQXBpLnN1Z2dlc3RlcigKICAgICAgICBmb2xkZXJzLAogICAgICAgIGZvbGRlcnMsCiAgICAgICAgZGVmYXVsdEV4aXN0cwogICAgICAgICAgICA/IGBDaG9vc2Ugc291cmNlIGZvbGRlciAoZGVmYXVsdDogJHtub3JtYWxpemVkRGVmYXVsdH0pYAogICAgICAgICAgICA6ICJDaG9vc2Ugc291cmNlIGZvbGRlciIKICAgICk7CgogICAgcmV0dXJuIHNvdXJjZSA/IG5vcm1hbGl6ZVBhdGgoc291cmNlKSA6IG51bGw7Cn0KCmZ1bmN0aW9uIHNjYW5Gb2xkZXIoc291cmNlRm9sZGVyLCBURm9sZGVyLCBURmlsZSkgewogICAgY29uc3QgZm9sZGVycyA9IFtdOwogICAgY29uc3QgZmlsZXMgPSBbXTsKICAgIGNvbnN0IHF1ZXVlID0gWy4uLnNvdXJjZUZvbGRlci5jaGlsZHJlbl07CgogICAgd2hpbGUgKHF1ZXVlLmxlbmd0aCA+IDApIHsKICAgICAgICBjb25zdCBjdXJyZW50ID0gcXVldWUuc2hpZnQoKTsKICAgICAgICBpZiAoIWN1cnJlbnQpIGNvbnRpbnVlOwoKICAgICAgICBpZiAoY3VycmVudCBpbnN0YW5jZW9mIFRGb2xkZXIpIHsKICAgICAgICAgICAgZm9sZGVycy5wdXNoKGN1cnJlbnQpOwogICAgICAgICAgICBxdWV1ZS5wdXNoKC4uLmN1cnJlbnQuY2hpbGRyZW4pOwogICAgICAgICAgICBjb250aW51ZTsKICAgICAgICB9CgogICAgICAgIGlmIChjdXJyZW50IGluc3RhbmNlb2YgVEZpbGUpIHsKICAgICAgICAgICAgZmlsZXMucHVzaChjdXJyZW50KTsKICAgICAgICB9CiAgICB9CgogICAgZm9sZGVycy5zb3J0KChhLCBiKSA9PiBhLnBhdGgubG9jYWxlQ29tcGFyZShiLnBhdGgpKTsKICAgIGZpbGVzLnNvcnQoKGEsIGIpID0+IGEucGF0aC5sb2NhbGVDb21wYXJlKGIucGF0aCkpOwoKICAgIHJldHVybiB7IGZvbGRlcnMsIGZpbGVzIH07Cn0KCmZ1bmN0aW9uIGJ1aWxkUGxhbihzb3VyY2VSb290LCBkZXN0aW5hdGlvblJvb3QsIGZvbGRlcnMsIGZpbGVzLCBub3JtYWxpemVQYXRoKSB7CiAgICBjb25zdCBmb2xkZXJQYXRocyA9IFtkZXN0aW5hdGlvblJvb3RdOwogICAgY29uc3QgZmlsZVBsYW5zID0gW107CiAgICBjb25zdCBpbnZhbGlkUGF0aHMgPSBbXTsKCiAgICBmb3IgKGNvbnN0IGZvbGRlciBvZiBmb2xkZXJzKSB7CiAgICAgICAgY29uc3QgcmVsYXRpdmUgPSBmb2xkZXIucGF0aC5zbGljZShzb3VyY2VSb290Lmxlbmd0aCArIDEpOwogICAgICAgIGNvbnN0IHRhcmdldFBhdGggPSBub3JtYWxpemVQYXRoKGAke2Rlc3RpbmF0aW9uUm9vdH0vJHtyZWxhdGl2ZX1gKTsKCiAgICAgICAgaWYgKCFpc1N1YlBhdGgoZGVzdGluYXRpb25Sb290LCB0YXJnZXRQYXRoKSAmJiB0YXJnZXRQYXRoICE9PSBkZXN0aW5hdGlvblJvb3QpIHsKICAgICAgICAgICAgaW52YWxpZFBhdGhzLnB1c2godGFyZ2V0UGF0aCk7CiAgICAgICAgICAgIGNvbnRpbnVlOwogICAgICAgIH0KCiAgICAgICAgZm9sZGVyUGF0aHMucHVzaCh0YXJnZXRQYXRoKTsKICAgIH0KCiAgICBmb3IgKGNvbnN0IGZpbGUgb2YgZmlsZXMpIHsKICAgICAgICBjb25zdCByZWxhdGl2ZSA9IGZpbGUucGF0aC5zbGljZShzb3VyY2VSb290Lmxlbmd0aCArIDEpOwogICAgICAgIGNvbnN0IHRhcmdldFBhdGggPSBub3JtYWxpemVQYXRoKGAke2Rlc3RpbmF0aW9uUm9vdH0vJHtyZWxhdGl2ZX1gKTsKCiAgICAgICAgaWYgKCFpc1N1YlBhdGgoZGVzdGluYXRpb25Sb290LCB0YXJnZXRQYXRoKSkgewogICAgICAgICAgICBpbnZhbGlkUGF0aHMucHVzaCh0YXJnZXRQYXRoKTsKICAgICAgICAgICAgY29udGludWU7CiAgICAgICAgfQoKICAgICAgICBmaWxlUGxhbnMucHVzaCh7IHNvdXJjZUZpbGU6IGZpbGUsIHRhcmdldFBhdGggfSk7CiAgICB9CgogICAgZm9sZGVyUGF0aHMuc29ydChzb3J0QnlEZXB0aFRoZW5QYXRoKTsKCiAgICByZXR1cm4geyBmb2xkZXJQYXRocywgZmlsZVBsYW5zLCBpbnZhbGlkUGF0aHMgfTsKfQoKYXN5bmMgZnVuY3Rpb24gZmluZEV4aXN0aW5nVGFyZ2V0cyhhcHAsIHBsYW4pIHsKICAgIGNvbnN0IGZvbGRlclBhdGhzID0gW107CiAgICBjb25zdCBmaWxlUGF0aHMgPSBbXTsKCiAgICBmb3IgKGNvbnN0IGZvbGRlclBhdGggb2YgcGxhbi5mb2xkZXJQYXRocykgewogICAgICAgIGlmIChhd2FpdCBhcHAudmF1bHQuYWRhcHRlci5leGlzdHMoZm9sZGVyUGF0aCkpIHsKICAgICAgICAgICAgZm9sZGVyUGF0aHMucHVzaChmb2xkZXJQYXRoKTsKICAgICAgICB9CiAgICB9CgogICAgZm9yIChjb25zdCBmaWxlUGxhbiBvZiBwbGFuLmZpbGVQbGFucykgewogICAgICAgIGlmIChhd2FpdCBhcHAudmF1bHQuYWRhcHRlci5leGlzdHMoZmlsZVBsYW4udGFyZ2V0UGF0aCkpIHsKICAgICAgICAgICAgZmlsZVBhdGhzLnB1c2goZmlsZVBsYW4udGFyZ2V0UGF0aCk7CiAgICAgICAgfQogICAgfQoKICAgIHJldHVybiB7IGZvbGRlclBhdGhzLCBmaWxlUGF0aHMgfTsKfQoKYXN5bmMgZnVuY3Rpb24gZXhlY3V0ZVBsYW4ob3B0aW9ucykgewogICAgY29uc3QgewogICAgICAgIGFwcCwKICAgICAgICBwbGFuLAogICAgICAgIGV4aXN0aW5nVGFyZ2V0cywKICAgICAgICBza2lwRXhpc3RpbmdGaWxlcywKICAgICAgICBzaG91bGRSZXBsYWNlSW5NYXJrZG93biwKICAgICAgICBmaW5kVGV4dCwKICAgICAgICByZXBsYWNlVGV4dCwKICAgICAgICBkcnlSdW4sCiAgICB9ID0gb3B0aW9uczsKCiAgICBjb25zdCBleGlzdGluZ0ZpbGVTZXQgPSBuZXcgU2V0KGV4aXN0aW5nVGFyZ2V0cy5maWxlUGF0aHMpOwogICAgbGV0IGZvbGRlcnNDcmVhdGVkID0gMDsKICAgIGxldCBmaWxlc0NvcGllZCA9IDA7CiAgICBsZXQgZmlsZXNTa2lwcGVkID0gMDsKICAgIGxldCBtYXJrZG93bkZpbGVzVXBkYXRlZCA9IDA7CgogICAgZm9yIChjb25zdCBmb2xkZXJQYXRoIG9mIHBsYW4uZm9sZGVyUGF0aHMpIHsKICAgICAgICBjb25zdCBhbHJlYWR5RXhpc3RzID0gYXdhaXQgYXBwLnZhdWx0LmFkYXB0ZXIuZXhpc3RzKGZvbGRlclBhdGgpOwogICAgICAgIGlmIChhbHJlYWR5RXhpc3RzKSBjb250aW51ZTsKCiAgICAgICAgaWYgKGRyeVJ1bikgewogICAgICAgICAgICBmb2xkZXJzQ3JlYXRlZCArPSAxOwogICAgICAgICAgICBjb250aW51ZTsKICAgICAgICB9CgogICAgICAgIGF3YWl0IGFwcC52YXVsdC5jcmVhdGVGb2xkZXIoZm9sZGVyUGF0aCk7CiAgICAgICAgZm9sZGVyc0NyZWF0ZWQgKz0gMTsKICAgIH0KCiAgICBmb3IgKGNvbnN0IGZpbGVQbGFuIG9mIHBsYW4uZmlsZVBsYW5zKSB7CiAgICAgICAgY29uc3QgeyBzb3VyY2VGaWxlLCB0YXJnZXRQYXRoIH0gPSBmaWxlUGxhbjsKICAgICAgICBjb25zdCB0YXJnZXRFeGlzdHMgPSBleGlzdGluZ0ZpbGVTZXQuaGFzKHRhcmdldFBhdGgpIHx8CiAgICAgICAgICAgIChhd2FpdCBhcHAudmF1bHQuYWRhcHRlci5leGlzdHModGFyZ2V0UGF0aCkpOwoKICAgICAgICBpZiAodGFyZ2V0RXhpc3RzKSB7CiAgICAgICAgICAgIGlmIChza2lwRXhpc3RpbmdGaWxlcykgewogICAgICAgICAgICAgICAgZmlsZXNTa2lwcGVkICs9IDE7CiAgICAgICAgICAgICAgICBjb250aW51ZTsKICAgICAgICAgICAgfQoKICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBUYXJnZXQgZmlsZSBhbHJlYWR5IGV4aXN0czogJHt0YXJnZXRQYXRofWApOwogICAgICAgIH0KCiAgICAgICAgaWYgKGRyeVJ1bikgewogICAgICAgICAgICBmaWxlc0NvcGllZCArPSAxOwogICAgICAgICAgICBjb250aW51ZTsKICAgICAgICB9CgogICAgICAgIGlmIChzb3VyY2VGaWxlLmV4dGVuc2lvbi50b0xvd2VyQ2FzZSgpID09PSAibWQiKSB7CiAgICAgICAgICAgIGxldCBjb250ZW50ID0gYXdhaXQgYXBwLnZhdWx0LnJlYWQoc291cmNlRmlsZSk7CgogICAgICAgICAgICBpZiAoc2hvdWxkUmVwbGFjZUluTWFya2Rvd24gJiYgY29udGVudC5pbmNsdWRlcyhmaW5kVGV4dCkpIHsKICAgICAgICAgICAgICAgIGNvbnRlbnQgPSBjb250ZW50LnNwbGl0KGZpbmRUZXh0KS5qb2luKHJlcGxhY2VUZXh0KTsKICAgICAgICAgICAgICAgIG1hcmtkb3duRmlsZXNVcGRhdGVkICs9IDE7CiAgICAgICAgICAgIH0KCiAgICAgICAgICAgIGF3YWl0IGFwcC52YXVsdC5jcmVhdGUodGFyZ2V0UGF0aCwgY29udGVudCk7CiAgICAgICAgICAgIGZpbGVzQ29waWVkICs9IDE7CiAgICAgICAgICAgIGNvbnRpbnVlOwogICAgICAgIH0KCiAgICAgICAgY29uc3QgYnl0ZXMgPSBhd2FpdCBhcHAudmF1bHQucmVhZEJpbmFyeShzb3VyY2VGaWxlKTsKICAgICAgICBhd2FpdCBhcHAudmF1bHQuY3JlYXRlQmluYXJ5KHRhcmdldFBhdGgsIGJ5dGVzKTsKICAgICAgICBmaWxlc0NvcGllZCArPSAxOwogICAgfQoKICAgIHJldHVybiB7CiAgICAgICAgZm9sZGVyc0NyZWF0ZWQsCiAgICAgICAgZmlsZXNDb3BpZWQsCiAgICAgICAgZmlsZXNTa2lwcGVkLAogICAgICAgIG1hcmtkb3duRmlsZXNVcGRhdGVkLAogICAgfTsKfQoKZnVuY3Rpb24gc29ydEJ5RGVwdGhUaGVuUGF0aChhLCBiKSB7CiAgICBjb25zdCBkZXB0aEEgPSBhLnNwbGl0KCIvIikubGVuZ3RoOwogICAgY29uc3QgZGVwdGhCID0gYi5zcGxpdCgiLyIpLmxlbmd0aDsKCiAgICBpZiAoZGVwdGhBICE9PSBkZXB0aEIpIHsKICAgICAgICByZXR1cm4gZGVwdGhBIC0gZGVwdGhCOwogICAgfQoKICAgIHJldHVybiBhLmxvY2FsZUNvbXBhcmUoYik7Cn0KCmZ1bmN0aW9uIGlzU3ViUGF0aChwYXJlbnRQYXRoLCBjaGlsZFBhdGgpIHsKICAgIGlmICghcGFyZW50UGF0aCB8fCAhY2hpbGRQYXRoKSByZXR1cm4gZmFsc2U7CiAgICByZXR1cm4gY2hpbGRQYXRoLnN0YXJ0c1dpdGgoYCR7cGFyZW50UGF0aH0vYCk7Cn0KCmZ1bmN0aW9uIGlzU2FmZVZhdWx0UGF0aChwYXRoKSB7CiAgICBpZiAoIXBhdGgpIHJldHVybiBmYWxzZTsKICAgIGlmIChwYXRoID09PSAiLiIgfHwgcGF0aCA9PT0gIi4uIikgcmV0dXJuIGZhbHNlOwogICAgaWYgKHBhdGguc3RhcnRzV2l0aCgiLi4vIikgfHwgcGF0aC5pbmNsdWRlcygiLy4uLyIpKSByZXR1cm4gZmFsc2U7CiAgICBpZiAocGF0aC5pbmNsdWRlcygiXFwiKSkgcmV0dXJuIGZhbHNlOwogICAgaWYgKHBhdGguc3RhcnRzV2l0aCgiLyIpKSByZXR1cm4gZmFsc2U7CiAgICByZXR1cm4gdHJ1ZTsKfQo="
}
]
}
/**
* Duplicate a folder (including subfolders and files) inside the current vault.
*
* Safe defaults:
* - Never overwrites existing files.
* - Blocks overlapping source/destination paths.
* - Optional dry-run mode.
* - Optional markdown text replacement for templating.
*/
module.exports = {
entry: start,
settings: {
name: "Duplicate Folder",
author: "QuickAdd",
options: {
"Default Source Folder": {
type: "text",
defaultValue: "",
placeholder: "Projects/Template",
description: "Optional default source folder to preselect"
},
"Destination Suffix": {
type: "text",
defaultValue: " (Copy)",
placeholder: " (Copy)",
description: "Default suffix added to the suggested destination"
},
"Allow Existing Destination Folder": {
type: "toggle",
defaultValue: false,
description: "If disabled, abort when destination folder already exists"
},
"Skip Existing Files": {
type: "toggle",
defaultValue: false,
description: "If enabled, skip file collisions instead of aborting"
},
"Find Text in Markdown": {
type: "text",
defaultValue: "",
placeholder: "Old Project Name",
description: "Optional plain text to replace in .md files"
},
"Replace Text in Markdown": {
type: "text",
defaultValue: "",
placeholder: "New Project Name",
description: "Replacement text for markdown notes"
},
"Dry Run": {
type: "toggle",
defaultValue: false,
description: "Preview actions without writing files"
}
}
}
};
async function start(params, settings) {
const { app, obsidian, quickAddApi, variables } = params;
const { Notice, TFile, TFolder, normalizePath } = obsidian;
const sourceFolderPath = await selectSourceFolder(
app,
quickAddApi,
settings["Default Source Folder"],
normalizePath
);
if (!sourceFolderPath) {
new Notice("Folder duplication cancelled.");
return;
}
const sourceFolder = app.vault.getAbstractFileByPath(sourceFolderPath);
if (!(sourceFolder instanceof TFolder)) {
new Notice(`Source folder does not exist: ${sourceFolderPath}`);
return;
}
const suggestedDestination = normalizePath(
`${sourceFolderPath}${settings["Destination Suffix"] || " (Copy)"}`
);
const destinationInput = await quickAddApi.inputPrompt(
"Destination folder",
"Where should the copy be created?",
suggestedDestination
);
if (!destinationInput || !destinationInput.trim()) {
new Notice("Folder duplication cancelled.");
return;
}
const destinationFolderPath = normalizePath(destinationInput.trim());
if (!isSafeVaultPath(destinationFolderPath)) {
new Notice("Destination path is invalid.");
return;
}
if (destinationFolderPath === sourceFolderPath) {
new Notice("Destination cannot be the same as source.");
return;
}
if (
isSubPath(sourceFolderPath, destinationFolderPath) ||
isSubPath(destinationFolderPath, sourceFolderPath)
) {
new Notice(
"Source and destination folders cannot overlap (parent/child)."
);
return;
}
const allowExistingDestination = !!settings["Allow Existing Destination Folder"];
const skipExistingFiles = !!settings["Skip Existing Files"];
const dryRun = !!settings["Dry Run"];
const destinationExists = await app.vault.adapter.exists(destinationFolderPath);
if (destinationExists && !allowExistingDestination) {
new Notice(
`Destination already exists: ${destinationFolderPath}. ` +
"Enable 'Allow Existing Destination Folder' to continue."
);
return;
}
const scan = scanFolder(sourceFolder, TFolder, TFile);
const plan = buildPlan(
sourceFolderPath,
destinationFolderPath,
scan.folders,
scan.files,
normalizePath
);
if (plan.invalidPaths.length > 0) {
new Notice(
`Aborted due to invalid path mapping: ${plan.invalidPaths[0]}`
);
return;
}
const existingTargets = await findExistingTargets(app, plan);
if (existingTargets.filePaths.length > 0 && !skipExistingFiles) {
new Notice(
`Aborted: ${existingTargets.filePaths.length} file(s) already exist in destination.`
);
return;
}
const findText = `${settings["Find Text in Markdown"] || ""}`;
const replaceText = `${settings["Replace Text in Markdown"] || ""}`;
const shouldReplaceInMarkdown = findText.length > 0;
const previewLines = [
`Source: ${sourceFolderPath}`,
`Destination: ${destinationFolderPath}`,
`Folders to create: ${plan.folderPaths.length}`,
`Files to copy: ${plan.filePlans.length}`,
`Existing files: ${existingTargets.filePaths.length}`,
`Dry run: ${dryRun ? "yes" : "no"}`,
];
if (shouldReplaceInMarkdown) {
previewLines.push(
`Markdown replacement: \"${findText}\" → \"${replaceText}\"`
);
}
const shouldContinue = await quickAddApi.yesNoPrompt(
"Duplicate folder?",
previewLines.join("\n")
);
if (!shouldContinue) {
new Notice("Folder duplication cancelled.");
return;
}
let result;
try {
result = await executePlan({
app,
plan,
existingTargets,
skipExistingFiles,
shouldReplaceInMarkdown,
findText,
replaceText,
dryRun,
});
} catch (error) {
const message = error?.message || `${error}`;
new Notice(`Folder duplication failed: ${message}`, 8000);
throw error;
}
variables.duplicatedSourceFolder = sourceFolderPath;
variables.duplicatedDestinationFolder = destinationFolderPath;
variables.duplicatedFoldersCreated = result.foldersCreated;
variables.duplicatedFilesCopied = result.filesCopied;
variables.duplicatedFilesSkipped = result.filesSkipped;
variables.duplicatedMarkdownFilesUpdated = result.markdownFilesUpdated;
const summary = dryRun
? `Dry run complete. Planned ${plan.folderPaths.length} folder(s) and ${plan.filePlans.length} file(s).`
: `Duplicated folder successfully. Created ${result.foldersCreated} folder(s), copied ${result.filesCopied} file(s), skipped ${result.filesSkipped}.`;
new Notice(summary, 8000);
}
async function selectSourceFolder(app, quickAddApi, defaultPath, normalizePath) {
const folders = app.vault
.getAllLoadedFiles()
.filter((file) => file && Array.isArray(file.children))
.map((folder) => folder.path)
.filter((path) => !!path)
.sort((a, b) => a.localeCompare(b));
if (folders.length === 0) {
return null;
}
const normalizedDefault = defaultPath ? normalizePath(defaultPath.trim()) : "";
const defaultExists = normalizedDefault && folders.includes(normalizedDefault);
const source = await quickAddApi.suggester(
folders,
folders,
defaultExists
? `Choose source folder (default: ${normalizedDefault})`
: "Choose source folder"
);
return source ? normalizePath(source) : null;
}
function scanFolder(sourceFolder, TFolder, TFile) {
const folders = [];
const files = [];
const queue = [...sourceFolder.children];
while (queue.length > 0) {
const current = queue.shift();
if (!current) continue;
if (current instanceof TFolder) {
folders.push(current);
queue.push(...current.children);
continue;
}
if (current instanceof TFile) {
files.push(current);
}
}
folders.sort((a, b) => a.path.localeCompare(b.path));
files.sort((a, b) => a.path.localeCompare(b.path));
return { folders, files };
}
function buildPlan(sourceRoot, destinationRoot, folders, files, normalizePath) {
const folderPaths = [destinationRoot];
const filePlans = [];
const invalidPaths = [];
for (const folder of folders) {
const relative = folder.path.slice(sourceRoot.length + 1);
const targetPath = normalizePath(`${destinationRoot}/${relative}`);
if (!isSubPath(destinationRoot, targetPath) && targetPath !== destinationRoot) {
invalidPaths.push(targetPath);
continue;
}
folderPaths.push(targetPath);
}
for (const file of files) {
const relative = file.path.slice(sourceRoot.length + 1);
const targetPath = normalizePath(`${destinationRoot}/${relative}`);
if (!isSubPath(destinationRoot, targetPath)) {
invalidPaths.push(targetPath);
continue;
}
filePlans.push({ sourceFile: file, targetPath });
}
folderPaths.sort(sortByDepthThenPath);
return { folderPaths, filePlans, invalidPaths };
}
async function findExistingTargets(app, plan) {
const folderPaths = [];
const filePaths = [];
for (const folderPath of plan.folderPaths) {
if (await app.vault.adapter.exists(folderPath)) {
folderPaths.push(folderPath);
}
}
for (const filePlan of plan.filePlans) {
if (await app.vault.adapter.exists(filePlan.targetPath)) {
filePaths.push(filePlan.targetPath);
}
}
return { folderPaths, filePaths };
}
async function executePlan(options) {
const {
app,
plan,
existingTargets,
skipExistingFiles,
shouldReplaceInMarkdown,
findText,
replaceText,
dryRun,
} = options;
const existingFileSet = new Set(existingTargets.filePaths);
let foldersCreated = 0;
let filesCopied = 0;
let filesSkipped = 0;
let markdownFilesUpdated = 0;
for (const folderPath of plan.folderPaths) {
const alreadyExists = await app.vault.adapter.exists(folderPath);
if (alreadyExists) continue;
if (dryRun) {
foldersCreated += 1;
continue;
}
await app.vault.createFolder(folderPath);
foldersCreated += 1;
}
for (const filePlan of plan.filePlans) {
const { sourceFile, targetPath } = filePlan;
const targetExists = existingFileSet.has(targetPath) ||
(await app.vault.adapter.exists(targetPath));
if (targetExists) {
if (skipExistingFiles) {
filesSkipped += 1;
continue;
}
throw new Error(`Target file already exists: ${targetPath}`);
}
if (dryRun) {
filesCopied += 1;
continue;
}
if (sourceFile.extension.toLowerCase() === "md") {
let content = await app.vault.read(sourceFile);
if (shouldReplaceInMarkdown && content.includes(findText)) {
content = content.split(findText).join(replaceText);
markdownFilesUpdated += 1;
}
await app.vault.create(targetPath, content);
filesCopied += 1;
continue;
}
const bytes = await app.vault.readBinary(sourceFile);
await app.vault.createBinary(targetPath, bytes);
filesCopied += 1;
}
return {
foldersCreated,
filesCopied,
filesSkipped,
markdownFilesUpdated,
};
}
function sortByDepthThenPath(a, b) {
const depthA = a.split("/").length;
const depthB = b.split("/").length;
if (depthA !== depthB) {
return depthA - depthB;
}
return a.localeCompare(b);
}
function isSubPath(parentPath, childPath) {
if (!parentPath || !childPath) return false;
return childPath.startsWith(`${parentPath}/`);
}
function isSafeVaultPath(path) {
if (!path) return false;
if (path === "." || path === "..") return false;
if (path.startsWith("../") || path.includes("/../")) return false;
if (path.includes("\\")) return false;
if (path.startsWith("/")) return false;
return true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment