memorysync-cli 1.6.0__py3-none-any.whl → 1.6.1__py3-none-any.whl

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.
@@ -1,13 +1,13 @@
1
- """MemorySync command-line interface.
2
-
3
- A Python implementation of the same CLI as the npm package ``memorysync-cli``,
4
- with the same commands, flags, output formats and exit codes. The two are
5
- interchangeable: whichever is on PATH behaves identically, so you only need one.
6
-
7
- Both read their command surface from one generated declaration, so ``help --json``
8
- is byte-identical between them rather than kept aligned by review.
9
- """
10
-
11
- from ._version import __version__
12
-
13
- __all__ = ["__version__"]
1
+ """MemorySync command-line interface.
2
+
3
+ A Python implementation of the same CLI as the npm package ``memorysync-cli``,
4
+ with the same commands, flags, output formats and exit codes. The two are
5
+ interchangeable: whichever is on PATH behaves identically, so you only need one.
6
+
7
+ Both read their command surface from one generated declaration, so ``help --json``
8
+ is byte-identical between them rather than kept aligned by review.
9
+ """
10
+
11
+ from ._version import __version__
12
+
13
+ __all__ = ["__version__"]
@@ -1,9 +1,9 @@
1
- """Allow ``python -m memorysync_cli`` alongside the installed console scripts.
2
-
3
- Useful during development and in environments where a wheel's scripts directory is
4
- not on PATH, which is common inside CI containers.
5
- """
6
-
7
- from .main import run
8
-
9
- run()
1
+ """Allow ``python -m memorysync_cli`` alongside the installed console scripts.
2
+
3
+ Useful during development and in environments where a wheel's scripts directory is
4
+ not on PATH, which is common inside CI containers.
5
+ """
6
+
7
+ from .main import run
8
+
9
+ run()
@@ -1 +1 @@
1
- __version__ = "1.6.0"
1
+ __version__ = "1.6.1"
memorysync_cli/args.py CHANGED
@@ -1,231 +1,231 @@
1
- """Argument parsing.
2
-
3
- Hand-rolled rather than using argparse, and deliberately so.
4
-
5
- The grammar is small, but it has to match the Node CLI token for token, because
6
- the two CLIs are interchangeable and a script must not behave differently
7
- depending on which one is on PATH. argparse has its own opinions - about prefix
8
- matching, about how it reports errors, about exiting with code 2 on its own terms
9
- - and bending it into agreement would be more work than owning ninety lines.
10
-
11
- Owning it also means `--help`, `help --json` and the completion scripts all come
12
- from the shared registry rather than from a library's internal state, which is how
13
- they stay consistent with what the parser actually accepts.
14
-
15
- The grammar, matching ``sdk/cli/src/args.mjs``:
16
-
17
- --flag value --flag=value -f value -f=value --no-flag
18
- Bundled booleans: -qv. A valued short flag may only come last, as in getopt.
19
- Everything after ``--`` is positional, so text containing dashes survives.
20
- Unknown flags are refused, never ignored.
21
-
22
- That last rule earns its keep: silently dropping a misspelled ``--user`` would
23
- scope memory to the wrong person, and the caller would not find out.
24
- """
25
-
26
- from __future__ import annotations
27
-
28
- from typing import Any
29
-
30
- from . import registry
31
- from .errors import usage_error
32
-
33
-
34
- def _collect_valued() -> set[str]:
35
- """Flags that take a value. Everything else is boolean.
36
-
37
- Derived from the shared registry rather than hand-listed, matching
38
- ``sdk/cli/src/args.mjs``. The registry already marks a value-taking flag with
39
- ``value``, and keeping a second copy of that fact here meant the two could
40
- disagree - which they did: ``--email``, ``--code``, ``--password`` and
41
- ``--agent-caller`` were declared with a ``value`` but missing from the set, so
42
- ``--email you@example.com`` parsed as ``{"email": True}`` and sent the string
43
- "true" to the API. ``--password`` was worse: the account password became
44
- "true" and the real one was left in shell history as a positional.
45
-
46
- A flag is valued if any declaration of it says so, including on a subcommand.
47
- """
48
- valued: set[str] = set()
49
-
50
- def visit(flags: list[dict[str, Any]] | None) -> None:
51
- for flag in flags or []:
52
- if flag.get("value"):
53
- valued.add(flag["name"].lstrip("-"))
54
-
55
- visit(registry.global_flags())
56
- for spec in registry.commands().values():
57
- visit(spec.get("flags"))
58
- for sub in spec.get("subcommands", []):
59
- visit(sub.get("flags"))
60
- return valued
61
-
62
-
63
- VALUED = _collect_valued()
64
-
65
- SHORT = {
66
- "p": "profile",
67
- "u": "user",
68
- "o": "output",
69
- "y": "yes",
70
- "q": "quiet",
71
- "v": "verbose",
72
- "h": "help",
73
- "m": "metadata",
74
- "f": "file",
75
- "k": "limit",
76
- }
77
-
78
-
79
- def _key(name: str) -> str:
80
- """Flag name to dict key.
81
-
82
- ``--dry-run`` becomes ``dry_run``. The Node CLI uses ``dryRun``; the surface
83
- both expose is the flag itself, so each side reads naturally in its own
84
- language. Only the command-line spelling has to agree.
85
- """
86
- return name.replace("-", "_")
87
-
88
-
89
- def parse_args(argv: list[str]) -> tuple[list[str], dict[str, Any]]:
90
- """Split argv into positionals and flags.
91
-
92
- Returns them separately rather than as one namespace so the dispatcher can
93
- tell ``memorysync add --user x`` from ``memorysync --user x add`` without
94
- re-parsing.
95
- """
96
- positionals: list[str] = []
97
- flags: dict[str, Any] = {}
98
- passthrough = False
99
-
100
- index = 0
101
- while index < len(argv):
102
- token = argv[index]
103
-
104
- if passthrough:
105
- positionals.append(token)
106
- index += 1
107
- continue
108
-
109
- if token == "--":
110
- passthrough = True
111
- index += 1
112
- continue
113
-
114
- if token.startswith("--"):
115
- name = token[2:]
116
- value: str | None = None
117
-
118
- if "=" in name:
119
- name, _, value = name.partition("=")
120
-
121
- if name.startswith("no-") and value is None:
122
- positive = name[3:]
123
- if positive in VALUED:
124
- raise usage_error(
125
- f"--{name} is not a flag. --{positive} takes a value."
126
- )
127
- flags[_key(positive)] = False
128
- index += 1
129
- continue
130
-
131
- if name in VALUED:
132
- if value is None:
133
- following = argv[index + 1] if index + 1 < len(argv) else None
134
- if following is None or following.startswith("-"):
135
- raise usage_error(f"--{name} needs a value.")
136
- value = following
137
- index += 1
138
- flags[_key(name)] = value
139
- index += 1
140
- continue
141
-
142
- if value is not None:
143
- # `--verbose=true` is accepted so a script generating flags
144
- # programmatically does not have to special-case booleans.
145
- flags[_key(name)] = value not in {"false", "0"}
146
- index += 1
147
- continue
148
-
149
- flags[_key(name)] = True
150
- index += 1
151
- continue
152
-
153
- if len(token) > 1 and token.startswith("-"):
154
- body = token[1:]
155
- letters, _, inline = body.partition("=")
156
- has_inline = "=" in body
157
-
158
- for position, letter in enumerate(letters):
159
- long = SHORT.get(letter)
160
- if long is None:
161
- raise usage_error(f"Unknown flag -{letter}.")
162
-
163
- is_last = position == len(letters) - 1
164
-
165
- if long in VALUED:
166
- if not is_last:
167
- raise usage_error(
168
- f"-{letter} takes a value, so it must come last in -{letters}."
169
- )
170
- if has_inline:
171
- value = inline
172
- else:
173
- following = argv[index + 1] if index + 1 < len(argv) else None
174
- if following is None or following.startswith("-"):
175
- raise usage_error(f"-{letter} needs a value.")
176
- value = following
177
- index += 1
178
- if value == "":
179
- raise usage_error(f"-{letter} needs a value.")
180
- flags[_key(long)] = value
181
- else:
182
- flags[_key(long)] = True
183
-
184
- index += 1
185
- continue
186
-
187
- positionals.append(token)
188
- index += 1
189
-
190
- return positionals, flags
191
-
192
-
193
- def levenshtein(left: str, right: str) -> int:
194
- """Edit distance, for "did you mean". Inputs are command names, so the naive
195
- row-by-row version is more than fast enough."""
196
- if left == right:
197
- return 0
198
- if not left:
199
- return len(right)
200
- if not right:
201
- return len(left)
202
-
203
- previous = list(range(len(right) + 1))
204
- for i, lc in enumerate(left, start=1):
205
- current = [i]
206
- for j, rc in enumerate(right, start=1):
207
- current.append(
208
- min(
209
- previous[j] + 1,
210
- current[j - 1] + 1,
211
- previous[j - 1] + (0 if lc == rc else 1),
212
- )
213
- )
214
- previous = current
215
- return previous[-1]
216
-
217
-
218
- def suggest(unknown: str, candidates: list[str]) -> str | None:
219
- """Closest candidate, when one is close enough to be worth offering.
220
-
221
- A third of the length is the threshold: close enough to catch a typo, far
222
- enough that ``memorysync foo`` is not answered with a confident wrong guess.
223
- """
224
- best, best_distance = None, None
225
- for candidate in candidates:
226
- distance = levenshtein(unknown, candidate)
227
- if best_distance is None or distance < best_distance:
228
- best, best_distance = candidate, distance
229
- if best is None or best_distance is None:
230
- return None
231
- return best if best_distance <= max(1, len(unknown) // 3) else None
1
+ """Argument parsing.
2
+
3
+ Hand-rolled rather than using argparse, and deliberately so.
4
+
5
+ The grammar is small, but it has to match the Node CLI token for token, because
6
+ the two CLIs are interchangeable and a script must not behave differently
7
+ depending on which one is on PATH. argparse has its own opinions - about prefix
8
+ matching, about how it reports errors, about exiting with code 2 on its own terms
9
+ - and bending it into agreement would be more work than owning ninety lines.
10
+
11
+ Owning it also means `--help`, `help --json` and the completion scripts all come
12
+ from the shared registry rather than from a library's internal state, which is how
13
+ they stay consistent with what the parser actually accepts.
14
+
15
+ The grammar, matching ``sdk/cli/src/args.mjs``:
16
+
17
+ --flag value --flag=value -f value -f=value --no-flag
18
+ Bundled booleans: -qv. A valued short flag may only come last, as in getopt.
19
+ Everything after ``--`` is positional, so text containing dashes survives.
20
+ Unknown flags are refused, never ignored.
21
+
22
+ That last rule earns its keep: silently dropping a misspelled ``--user`` would
23
+ scope memory to the wrong person, and the caller would not find out.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import Any
29
+
30
+ from . import registry
31
+ from .errors import usage_error
32
+
33
+
34
+ def _collect_valued() -> set[str]:
35
+ """Flags that take a value. Everything else is boolean.
36
+
37
+ Derived from the shared registry rather than hand-listed, matching
38
+ ``sdk/cli/src/args.mjs``. The registry already marks a value-taking flag with
39
+ ``value``, and keeping a second copy of that fact here meant the two could
40
+ disagree - which they did: ``--email``, ``--code``, ``--password`` and
41
+ ``--agent-caller`` were declared with a ``value`` but missing from the set, so
42
+ ``--email you@example.com`` parsed as ``{"email": True}`` and sent the string
43
+ "true" to the API. ``--password`` was worse: the account password became
44
+ "true" and the real one was left in shell history as a positional.
45
+
46
+ A flag is valued if any declaration of it says so, including on a subcommand.
47
+ """
48
+ valued: set[str] = set()
49
+
50
+ def visit(flags: list[dict[str, Any]] | None) -> None:
51
+ for flag in flags or []:
52
+ if flag.get("value"):
53
+ valued.add(flag["name"].lstrip("-"))
54
+
55
+ visit(registry.global_flags())
56
+ for spec in registry.commands().values():
57
+ visit(spec.get("flags"))
58
+ for sub in spec.get("subcommands", []):
59
+ visit(sub.get("flags"))
60
+ return valued
61
+
62
+
63
+ VALUED = _collect_valued()
64
+
65
+ SHORT = {
66
+ "p": "profile",
67
+ "u": "user",
68
+ "o": "output",
69
+ "y": "yes",
70
+ "q": "quiet",
71
+ "v": "verbose",
72
+ "h": "help",
73
+ "m": "metadata",
74
+ "f": "file",
75
+ "k": "limit",
76
+ }
77
+
78
+
79
+ def _key(name: str) -> str:
80
+ """Flag name to dict key.
81
+
82
+ ``--dry-run`` becomes ``dry_run``. The Node CLI uses ``dryRun``; the surface
83
+ both expose is the flag itself, so each side reads naturally in its own
84
+ language. Only the command-line spelling has to agree.
85
+ """
86
+ return name.replace("-", "_")
87
+
88
+
89
+ def parse_args(argv: list[str]) -> tuple[list[str], dict[str, Any]]:
90
+ """Split argv into positionals and flags.
91
+
92
+ Returns them separately rather than as one namespace so the dispatcher can
93
+ tell ``memorysync add --user x`` from ``memorysync --user x add`` without
94
+ re-parsing.
95
+ """
96
+ positionals: list[str] = []
97
+ flags: dict[str, Any] = {}
98
+ passthrough = False
99
+
100
+ index = 0
101
+ while index < len(argv):
102
+ token = argv[index]
103
+
104
+ if passthrough:
105
+ positionals.append(token)
106
+ index += 1
107
+ continue
108
+
109
+ if token == "--":
110
+ passthrough = True
111
+ index += 1
112
+ continue
113
+
114
+ if token.startswith("--"):
115
+ name = token[2:]
116
+ value: str | None = None
117
+
118
+ if "=" in name:
119
+ name, _, value = name.partition("=")
120
+
121
+ if name.startswith("no-") and value is None:
122
+ positive = name[3:]
123
+ if positive in VALUED:
124
+ raise usage_error(
125
+ f"--{name} is not a flag. --{positive} takes a value."
126
+ )
127
+ flags[_key(positive)] = False
128
+ index += 1
129
+ continue
130
+
131
+ if name in VALUED:
132
+ if value is None:
133
+ following = argv[index + 1] if index + 1 < len(argv) else None
134
+ if following is None or following.startswith("-"):
135
+ raise usage_error(f"--{name} needs a value.")
136
+ value = following
137
+ index += 1
138
+ flags[_key(name)] = value
139
+ index += 1
140
+ continue
141
+
142
+ if value is not None:
143
+ # `--verbose=true` is accepted so a script generating flags
144
+ # programmatically does not have to special-case booleans.
145
+ flags[_key(name)] = value not in {"false", "0"}
146
+ index += 1
147
+ continue
148
+
149
+ flags[_key(name)] = True
150
+ index += 1
151
+ continue
152
+
153
+ if len(token) > 1 and token.startswith("-"):
154
+ body = token[1:]
155
+ letters, _, inline = body.partition("=")
156
+ has_inline = "=" in body
157
+
158
+ for position, letter in enumerate(letters):
159
+ long = SHORT.get(letter)
160
+ if long is None:
161
+ raise usage_error(f"Unknown flag -{letter}.")
162
+
163
+ is_last = position == len(letters) - 1
164
+
165
+ if long in VALUED:
166
+ if not is_last:
167
+ raise usage_error(
168
+ f"-{letter} takes a value, so it must come last in -{letters}."
169
+ )
170
+ if has_inline:
171
+ value = inline
172
+ else:
173
+ following = argv[index + 1] if index + 1 < len(argv) else None
174
+ if following is None or following.startswith("-"):
175
+ raise usage_error(f"-{letter} needs a value.")
176
+ value = following
177
+ index += 1
178
+ if value == "":
179
+ raise usage_error(f"-{letter} needs a value.")
180
+ flags[_key(long)] = value
181
+ else:
182
+ flags[_key(long)] = True
183
+
184
+ index += 1
185
+ continue
186
+
187
+ positionals.append(token)
188
+ index += 1
189
+
190
+ return positionals, flags
191
+
192
+
193
+ def levenshtein(left: str, right: str) -> int:
194
+ """Edit distance, for "did you mean". Inputs are command names, so the naive
195
+ row-by-row version is more than fast enough."""
196
+ if left == right:
197
+ return 0
198
+ if not left:
199
+ return len(right)
200
+ if not right:
201
+ return len(left)
202
+
203
+ previous = list(range(len(right) + 1))
204
+ for i, lc in enumerate(left, start=1):
205
+ current = [i]
206
+ for j, rc in enumerate(right, start=1):
207
+ current.append(
208
+ min(
209
+ previous[j] + 1,
210
+ current[j - 1] + 1,
211
+ previous[j - 1] + (0 if lc == rc else 1),
212
+ )
213
+ )
214
+ previous = current
215
+ return previous[-1]
216
+
217
+
218
+ def suggest(unknown: str, candidates: list[str]) -> str | None:
219
+ """Closest candidate, when one is close enough to be worth offering.
220
+
221
+ A third of the length is the threshold: close enough to catch a typo, far
222
+ enough that ``memorysync foo`` is not answered with a confident wrong guess.
223
+ """
224
+ best, best_distance = None, None
225
+ for candidate in candidates:
226
+ distance = levenshtein(unknown, candidate)
227
+ if best_distance is None or distance < best_distance:
228
+ best, best_distance = candidate, distance
229
+ if best is None or best_distance is None:
230
+ return None
231
+ return best if best_distance <= max(1, len(unknown) // 3) else None
@@ -1,6 +1,6 @@
1
- """Command implementations, grouped the same way the Node CLI groups them.
2
-
3
- The split mirrors ``sdk/cli/src/commands/`` file for file - memory, admin, init,
4
- source, tooling - so that a change on one side has an obvious counterpart on the
5
- other. Parity is easier to keep when the two trees look alike in review.
6
- """
1
+ """Command implementations, grouped the same way the Node CLI groups them.
2
+
3
+ The split mirrors ``sdk/cli/src/commands/`` file for file - memory, admin, init,
4
+ source, tooling - so that a change on one side has an obvious counterpart on the
5
+ other. Parity is easier to keep when the two trees look alike in review.
6
+ """