magicicapsula 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. magicicapsula-0.1.0/LICENSE +21 -0
  2. magicicapsula-0.1.0/PKG-INFO +169 -0
  3. magicicapsula-0.1.0/README.md +141 -0
  4. magicicapsula-0.1.0/magicicapsula/__init__.py +1 -0
  5. magicicapsula-0.1.0/magicicapsula/__main__.py +3 -0
  6. magicicapsula-0.1.0/magicicapsula/assets/logo.txt +27 -0
  7. magicicapsula-0.1.0/magicicapsula/cli.py +38 -0
  8. magicicapsula-0.1.0/magicicapsula/commands/__init__.py +0 -0
  9. magicicapsula-0.1.0/magicicapsula/commands/_style.py +59 -0
  10. magicicapsula-0.1.0/magicicapsula/commands/_util.py +26 -0
  11. magicicapsula-0.1.0/magicicapsula/commands/add.py +18 -0
  12. magicicapsula-0.1.0/magicicapsula/commands/info.py +27 -0
  13. magicicapsula-0.1.0/magicicapsula/commands/init.py +31 -0
  14. magicicapsula-0.1.0/magicicapsula/commands/open.py +29 -0
  15. magicicapsula-0.1.0/magicicapsula/commands/rm.py +17 -0
  16. magicicapsula-0.1.0/magicicapsula/commands/seal.py +66 -0
  17. magicicapsula-0.1.0/magicicapsula/commands/status.py +37 -0
  18. magicicapsula-0.1.0/magicicapsula/commands/verify.py +18 -0
  19. magicicapsula-0.1.0/magicicapsula/commands/version.py +14 -0
  20. magicicapsula-0.1.0/magicicapsula/core/__init__.py +5 -0
  21. magicicapsula-0.1.0/magicicapsula/core/capsule.py +165 -0
  22. magicicapsula-0.1.0/magicicapsula/core/crypto.py +69 -0
  23. magicicapsula-0.1.0/magicicapsula/core/draft.py +108 -0
  24. magicicapsula-0.1.0/magicicapsula/core/errors.py +29 -0
  25. magicicapsula-0.1.0/magicicapsula.egg-info/PKG-INFO +169 -0
  26. magicicapsula-0.1.0/magicicapsula.egg-info/SOURCES.txt +30 -0
  27. magicicapsula-0.1.0/magicicapsula.egg-info/dependency_links.txt +1 -0
  28. magicicapsula-0.1.0/magicicapsula.egg-info/entry_points.txt +2 -0
  29. magicicapsula-0.1.0/magicicapsula.egg-info/requires.txt +1 -0
  30. magicicapsula-0.1.0/magicicapsula.egg-info/top_level.txt +1 -0
  31. magicicapsula-0.1.0/pyproject.toml +46 -0
  32. magicicapsula-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 iDavi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: magicicapsula
3
+ Version: 0.1.0
4
+ Summary: seal files now, open them later
5
+ Author-email: iDavi <odavi20527@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/iDavi/magicicapsula
8
+ Project-URL: Repository, https://github.com/iDavi/magicicapsula
9
+ Project-URL: Issues, https://github.com/iDavi/magicicapsula/issues
10
+ Keywords: time-capsule,encryption,cli,archive,vault
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security :: Cryptography
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: cryptography>=42
27
+ Dynamic: license-file
28
+
29
+ # magicicapsula
30
+
31
+ ## install
32
+
33
+ ```
34
+ pip install -e .
35
+ ```
36
+
37
+ needs python 3.10+. one dependency: `cryptography`.
38
+
39
+ ## how it works
40
+
41
+ the workflow is staged, so you don't have to add everything at once:
42
+
43
+ ```
44
+ magicicapsula init -u 2030-01-01 # start a draft here
45
+ magicicapsula add letter.txt photos/ # stage files/folders
46
+ magicicapsula add diary.txt # add more later
47
+ magicicapsula status # see what's staged
48
+ magicicapsula seal # pack it all into capsule.mcap
49
+ ```
50
+
51
+ the draft lives in a `.capsule/` directory (found by walking up from the
52
+ current dir). `seal` reads everything staged and writes the `.mcap` file.
53
+
54
+ later, when the date has passed:
55
+
56
+ ```
57
+ magicicapsula open capsule.mcap -d ./out
58
+ ```
59
+
60
+ `.mcap` is one portable binary file. store it anywhere, copy it around. it
61
+ holds any file type (images, pdfs, binaries) byte for byte, not just text.
62
+
63
+ ## passwords
64
+
65
+ - with a password (default): contents are encrypted (aes-128 via `cryptography`),
66
+ unreadable without the password. `open` and `verify` prompt for it.
67
+ - without a password (`seal --no-password`): no encryption. the unlock date is
68
+ the only gate, so anyone with the file can open it after that date. don't put
69
+ anything private in a no-password capsule.
70
+
71
+ note: the unlock date is enforced by the tool, not by cryptography. if you hold
72
+ the password you could decrypt early with other means. the date stops casual
73
+ early opening, not a determined holder.
74
+
75
+ ## commands
76
+
77
+ ### init
78
+ start a new capsule draft in the current directory.
79
+
80
+ ```
81
+ magicicapsula init [-u DATE] [-n NOTE] [-o OUT]
82
+
83
+ -u, --unlock DATE unlock date, can also be set at seal
84
+ -n, --note NOTE plaintext note shown by info
85
+ -o, --out OUT output file name (default: capsule.mcap)
86
+ ```
87
+
88
+ ### add
89
+ stage files or folders to put in the capsule.
90
+
91
+ ```
92
+ magicicapsula add <paths...>
93
+ ```
94
+
95
+ ### status
96
+ show the draft: unlock date and staged files.
97
+
98
+ ```
99
+ magicicapsula status
100
+ ```
101
+
102
+ ### rm
103
+ unstage files. does not delete them from disk.
104
+
105
+ ```
106
+ magicicapsula rm <paths...>
107
+ ```
108
+
109
+ ### seal
110
+ seal everything staged into a capsule file. flags override the draft's
111
+ settings and stick.
112
+
113
+ ```
114
+ magicicapsula seal [-u DATE] [-o FILE] [-n NOTE] [-f] [-P]
115
+
116
+ -u, --unlock DATE unlock date, overrides the draft's
117
+ -o, --out FILE output capsule file, overrides the draft's
118
+ -n, --note NOTE plaintext note, overrides the draft's
119
+ -f, --force overwrite the output if it exists
120
+ -P, --no-password seal without a password (anyone can open it after the date)
121
+ ```
122
+
123
+ ### info
124
+ show a capsule's dates and status. no password needed.
125
+
126
+ ```
127
+ magicicapsula info <file>
128
+ ```
129
+
130
+ ### open
131
+ open a capsule and extract it, once the unlock date has passed.
132
+
133
+ ```
134
+ magicicapsula open [-d DEST] <file>
135
+
136
+ -d, --dest DEST directory to extract into (default: current dir)
137
+ ```
138
+
139
+ ### verify
140
+ check a capsule's integrity (and the password, if any) without opening it.
141
+
142
+ ```
143
+ magicicapsula verify <file>
144
+ ```
145
+
146
+ ### version
147
+ show the version and logo.
148
+
149
+ ```
150
+ magicicapsula version
151
+ ```
152
+
153
+ ## date format
154
+
155
+ `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM`, read as local time. examples:
156
+ `2030-01-01`, `2030-01-01T08:00`.
157
+
158
+ ## colors
159
+
160
+ output is colored in a terminal and plain when piped or redirected. set
161
+ `NO_COLOR=1` to turn colors off.
162
+
163
+ ## dates and gotchas
164
+
165
+ - staged entries are paths, read at seal time, not copied when you add them.
166
+ if a staged file is moved or deleted before sealing, `status` marks it
167
+ `(missing)` and `seal` refuses until it's fixed.
168
+ - files are stored under their base name, so two staged files with the same
169
+ name would collide.
@@ -0,0 +1,141 @@
1
+ # magicicapsula
2
+
3
+ ## install
4
+
5
+ ```
6
+ pip install -e .
7
+ ```
8
+
9
+ needs python 3.10+. one dependency: `cryptography`.
10
+
11
+ ## how it works
12
+
13
+ the workflow is staged, so you don't have to add everything at once:
14
+
15
+ ```
16
+ magicicapsula init -u 2030-01-01 # start a draft here
17
+ magicicapsula add letter.txt photos/ # stage files/folders
18
+ magicicapsula add diary.txt # add more later
19
+ magicicapsula status # see what's staged
20
+ magicicapsula seal # pack it all into capsule.mcap
21
+ ```
22
+
23
+ the draft lives in a `.capsule/` directory (found by walking up from the
24
+ current dir). `seal` reads everything staged and writes the `.mcap` file.
25
+
26
+ later, when the date has passed:
27
+
28
+ ```
29
+ magicicapsula open capsule.mcap -d ./out
30
+ ```
31
+
32
+ `.mcap` is one portable binary file. store it anywhere, copy it around. it
33
+ holds any file type (images, pdfs, binaries) byte for byte, not just text.
34
+
35
+ ## passwords
36
+
37
+ - with a password (default): contents are encrypted (aes-128 via `cryptography`),
38
+ unreadable without the password. `open` and `verify` prompt for it.
39
+ - without a password (`seal --no-password`): no encryption. the unlock date is
40
+ the only gate, so anyone with the file can open it after that date. don't put
41
+ anything private in a no-password capsule.
42
+
43
+ note: the unlock date is enforced by the tool, not by cryptography. if you hold
44
+ the password you could decrypt early with other means. the date stops casual
45
+ early opening, not a determined holder.
46
+
47
+ ## commands
48
+
49
+ ### init
50
+ start a new capsule draft in the current directory.
51
+
52
+ ```
53
+ magicicapsula init [-u DATE] [-n NOTE] [-o OUT]
54
+
55
+ -u, --unlock DATE unlock date, can also be set at seal
56
+ -n, --note NOTE plaintext note shown by info
57
+ -o, --out OUT output file name (default: capsule.mcap)
58
+ ```
59
+
60
+ ### add
61
+ stage files or folders to put in the capsule.
62
+
63
+ ```
64
+ magicicapsula add <paths...>
65
+ ```
66
+
67
+ ### status
68
+ show the draft: unlock date and staged files.
69
+
70
+ ```
71
+ magicicapsula status
72
+ ```
73
+
74
+ ### rm
75
+ unstage files. does not delete them from disk.
76
+
77
+ ```
78
+ magicicapsula rm <paths...>
79
+ ```
80
+
81
+ ### seal
82
+ seal everything staged into a capsule file. flags override the draft's
83
+ settings and stick.
84
+
85
+ ```
86
+ magicicapsula seal [-u DATE] [-o FILE] [-n NOTE] [-f] [-P]
87
+
88
+ -u, --unlock DATE unlock date, overrides the draft's
89
+ -o, --out FILE output capsule file, overrides the draft's
90
+ -n, --note NOTE plaintext note, overrides the draft's
91
+ -f, --force overwrite the output if it exists
92
+ -P, --no-password seal without a password (anyone can open it after the date)
93
+ ```
94
+
95
+ ### info
96
+ show a capsule's dates and status. no password needed.
97
+
98
+ ```
99
+ magicicapsula info <file>
100
+ ```
101
+
102
+ ### open
103
+ open a capsule and extract it, once the unlock date has passed.
104
+
105
+ ```
106
+ magicicapsula open [-d DEST] <file>
107
+
108
+ -d, --dest DEST directory to extract into (default: current dir)
109
+ ```
110
+
111
+ ### verify
112
+ check a capsule's integrity (and the password, if any) without opening it.
113
+
114
+ ```
115
+ magicicapsula verify <file>
116
+ ```
117
+
118
+ ### version
119
+ show the version and logo.
120
+
121
+ ```
122
+ magicicapsula version
123
+ ```
124
+
125
+ ## date format
126
+
127
+ `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM`, read as local time. examples:
128
+ `2030-01-01`, `2030-01-01T08:00`.
129
+
130
+ ## colors
131
+
132
+ output is colored in a terminal and plain when piped or redirected. set
133
+ `NO_COLOR=1` to turn colors off.
134
+
135
+ ## dates and gotchas
136
+
137
+ - staged entries are paths, read at seal time, not copied when you add them.
138
+ if a staged file is moved or deleted before sealing, `status` marks it
139
+ `(missing)` and `seal` refuses until it's fixed.
140
+ - files are stored under their base name, so two staged files with the same
141
+ name would collide.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from magicicapsula.cli import main
2
+
3
+ main()
@@ -0,0 +1,27 @@
1
+
2
+ ▓▓▓▓▓▓▓▓▓▓▓░░░░░
3
+ ▓▓▓ ▓ ░░
4
+ ▓▓ ▓ ░░░ ░░
5
+ ▓ ▓ ▓ ░▓▓▓ ░
6
+ ▓▓ ▓ ▓ ▓ ░▓▓░▓▓ ░
7
+ ▓ ▓ ▓ ▓▓ ▓░░░▓ ░
8
+ ▓ ▓ ▓ ▓█▒ ▓▓░▓▓ ░
9
+ ▓ ▓ ▓▓█▒ ▓▓▓ ░
10
+ ▓ ▓█▒█▒ ░
11
+ ▓ ▓ ▓ ▓█▒█▒ ░░ ░
12
+ ▓ ▓ ▓ ▓▒█▒█▒ ░░ ░
13
+ ▓ ▓ ▓ ▓▒█▒█▒ ░░░░░░
14
+ ▓ ▓ ▓ ▓█▒█▒█▒ ░
15
+ ▓ ▓▓░░░░░░░░░░░░░░░░░
16
+ ▓ ▓▓ ░▓░ ░▓░ ░▓░
17
+ ▓ ▓▓▓ ░▓░ ░▓░ ░▓░
18
+ ▓▓ ░▓▓░░▓▓░░▓▓░
19
+ ░▓▓░░▓▓░░▓▓░
20
+ ░░ ░░ ░░
21
+
22
+
23
+
24
+
25
+
26
+
27
+
@@ -0,0 +1,38 @@
1
+ import argparse
2
+ import importlib
3
+ import pkgutil
4
+ import sys
5
+
6
+ from magicicapsula import commands
7
+ from magicicapsula.commands import _style
8
+ from magicicapsula.core.errors import CapsuleError
9
+
10
+
11
+ def build_parser():
12
+ parser = argparse.ArgumentParser(
13
+ prog="magicicapsula",
14
+ description="seal files now, open them later",
15
+ )
16
+ sub = parser.add_subparsers(dest="command", metavar="<command>")
17
+ sub.required = True
18
+
19
+ # every non-underscore module in commands/ with a register() becomes a command.
20
+ # drop in a new file and it shows up, nothing else to wire.
21
+ for _, name, _ in pkgutil.iter_modules(commands.__path__):
22
+ if name.startswith("_"):
23
+ continue
24
+ mod = importlib.import_module(f"magicicapsula.commands.{name}")
25
+ if hasattr(mod, "register"):
26
+ mod.register(sub)
27
+
28
+ return parser
29
+
30
+
31
+ def main():
32
+ args = build_parser().parse_args()
33
+ try:
34
+ args.func(args)
35
+ except CapsuleError as exc:
36
+ sys.exit(_style.red(f"error: {exc}"))
37
+ except FileNotFoundError as exc:
38
+ sys.exit(_style.red(f"error: no such file: {exc}"))
File without changes
@@ -0,0 +1,59 @@
1
+ """colors and the logo. presentation only, so it stays in the cli layer.
2
+
3
+ colors switch off automatically when output isn't a terminal, or when
4
+ NO_COLOR is set, so piped/redirected output stays clean.
5
+ """
6
+
7
+ import os
8
+ import re
9
+ import sys
10
+ from importlib import resources
11
+
12
+ _ANSI = re.compile(r"\x1b\[[0-9;]*m")
13
+
14
+
15
+ def enabled():
16
+ return (
17
+ sys.stdout.isatty()
18
+ and os.environ.get("NO_COLOR") is None
19
+ and os.environ.get("TERM") != "dumb"
20
+ )
21
+
22
+
23
+ def paint(text, code):
24
+ return f"\x1b[{code}m{text}\x1b[0m" if enabled() else text
25
+
26
+
27
+ def bold(t):
28
+ return paint(t, "1")
29
+
30
+
31
+ def dim(t):
32
+ return paint(t, "2")
33
+
34
+
35
+ def red(t):
36
+ return paint(t, "31")
37
+
38
+
39
+ def green(t):
40
+ return paint(t, "32")
41
+
42
+
43
+ def yellow(t):
44
+ return paint(t, "33")
45
+
46
+
47
+ def cyan(t):
48
+ return paint(t, "36")
49
+
50
+
51
+ def logo():
52
+ text = resources.files("magicicapsula").joinpath("assets/logo.txt").read_text(encoding="utf-8")
53
+ lines = text.splitlines()
54
+ while lines and not lines[0].strip():
55
+ lines.pop(0)
56
+ while lines and not lines[-1].strip():
57
+ lines.pop()
58
+ text = "\n".join(lines)
59
+ return text if enabled() else _ANSI.sub("", text)
@@ -0,0 +1,26 @@
1
+ """small helpers shared by the commands. underscore name so it isn't a command."""
2
+
3
+ import getpass
4
+ from datetime import timedelta
5
+
6
+
7
+ def read_capsule(path):
8
+ with open(path, "rb") as fh:
9
+ return fh.read()
10
+
11
+
12
+ def ask_password(confirm=False):
13
+ pw = getpass.getpass("password: ")
14
+ if not pw:
15
+ raise SystemExit("error: empty password")
16
+ if confirm and pw != getpass.getpass("confirm password: "):
17
+ raise SystemExit("error: passwords do not match")
18
+ return pw
19
+
20
+
21
+ def fmt_remaining(delta: timedelta) -> str:
22
+ secs = max(int(delta.total_seconds()), 0)
23
+ days, secs = divmod(secs, 86400)
24
+ hours, secs = divmod(secs, 3600)
25
+ mins = secs // 60
26
+ return f"{days}d {hours}h {mins}m"
@@ -0,0 +1,18 @@
1
+ from magicicapsula.core import draft
2
+
3
+
4
+ def register(sub):
5
+ p = sub.add_parser("add", help="stage files or folders to put in the capsule")
6
+ p.add_argument("paths", nargs="+", help="files or folders to stage")
7
+ p.set_defaults(func=run)
8
+
9
+
10
+ def run(args):
11
+ d = draft.load()
12
+ added = draft.add(d, args.paths)
13
+ if not added:
14
+ print("nothing new to stage")
15
+ return
16
+ for p in added:
17
+ print(f" staged {p}")
18
+ print(f"{len(d.staged)} item(s) staged in total")
@@ -0,0 +1,27 @@
1
+ from datetime import datetime, timezone
2
+
3
+ from magicicapsula.core import capsule
4
+ from magicicapsula.commands import _style
5
+ from magicicapsula.commands._util import fmt_remaining, read_capsule
6
+
7
+
8
+ def register(sub):
9
+ p = sub.add_parser("info", help="show a capsule's dates and status (no password needed)")
10
+ p.add_argument("file", help="capsule file")
11
+ p.set_defaults(func=run)
12
+
13
+
14
+ def run(args):
15
+ info = capsule.inspect(read_capsule(args.file))
16
+ now = datetime.now(timezone.utc)
17
+ print(f"created: {info.created_at.astimezone().isoformat()}")
18
+ print(f"unlocks: {info.unlock_at.astimezone().isoformat()}")
19
+ print(f"cipher: {info.cipher}")
20
+ if info.cipher == "none":
21
+ print(_style.dim(" no password, opens for anyone after the unlock date"))
22
+ if info.note:
23
+ print(f"note: {info.note}")
24
+ if info.is_open(now):
25
+ print(f"status: {_style.green('open')}, the unlock date has passed")
26
+ else:
27
+ print(f"status: {_style.yellow('locked')}, {fmt_remaining(info.unlock_at - now)} remaining")
@@ -0,0 +1,31 @@
1
+ from datetime import datetime
2
+
3
+ from magicicapsula.core import draft
4
+
5
+
6
+ def register(sub):
7
+ p = sub.add_parser("init", help="start a new capsule draft in the current directory")
8
+ p.add_argument("-u", "--unlock", metavar="DATE", help="unlock date, can also be set at seal")
9
+ p.add_argument("-n", "--note", default="", help="plaintext note shown by info")
10
+ p.add_argument("-o", "--out", default="capsule.mcap", help="output file name")
11
+ p.set_defaults(func=run)
12
+
13
+
14
+ def run(args):
15
+ try:
16
+ d = draft.init()
17
+ except FileExistsError:
18
+ raise SystemExit("error: a capsule draft already exists here (.capsule/)")
19
+
20
+ if args.unlock:
21
+ try:
22
+ datetime.fromisoformat(args.unlock)
23
+ except ValueError:
24
+ raise SystemExit(f"error: bad date {args.unlock!r} (use YYYY-MM-DD or YYYY-MM-DDTHH:MM)")
25
+ d.unlock_at = args.unlock
26
+ d.note = args.note
27
+ d.out = args.out
28
+ draft.save(d)
29
+
30
+ print(f"new capsule draft in {d.dir}")
31
+ print("next: magicicapsula add <files...>")
@@ -0,0 +1,29 @@
1
+ from datetime import datetime, timezone
2
+
3
+ from magicicapsula.core import capsule
4
+ from magicicapsula.commands import _style
5
+ from magicicapsula.commands._util import ask_password, fmt_remaining, read_capsule
6
+
7
+
8
+ def register(sub):
9
+ p = sub.add_parser("open", help="open a capsule and extract it once the unlock date has passed")
10
+ p.add_argument("file", help="capsule file")
11
+ p.add_argument("-d", "--dest", default=".", help="directory to extract into")
12
+ p.set_defaults(func=run)
13
+
14
+
15
+ def run(args):
16
+ blob = read_capsule(args.file)
17
+ info = capsule.inspect(blob)
18
+ now = datetime.now(timezone.utc)
19
+ if not info.is_open(now):
20
+ raise SystemExit(
21
+ f"error: locked until {info.unlock_at.astimezone().isoformat()} "
22
+ f"({fmt_remaining(info.unlock_at - now)} remaining)"
23
+ )
24
+
25
+ pw = None if info.cipher == "none" else ask_password()
26
+ names = capsule.open_capsule(blob, pw, args.dest)
27
+ print(_style.green(f"opened into {args.dest}/"))
28
+ for name in names:
29
+ print(f" {_style.dim(name)}")
@@ -0,0 +1,17 @@
1
+ from magicicapsula.core import draft
2
+
3
+
4
+ def register(sub):
5
+ p = sub.add_parser("rm", help="unstage files (does not delete them from disk)")
6
+ p.add_argument("paths", nargs="+", help="staged paths to drop from the capsule")
7
+ p.set_defaults(func=run)
8
+
9
+
10
+ def run(args):
11
+ d = draft.load()
12
+ removed = draft.remove(d, args.paths)
13
+ if not removed:
14
+ print("none of those were staged")
15
+ return
16
+ for p in removed:
17
+ print(f" unstaged {p}")