jsonpit 4.5.4__tar.gz → 4.5.8__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.
@@ -1,4 +1,4 @@
1
- # jsonpit API Reference (v4.5.4)
1
+ # jsonpit API Reference (v4.5.5)
2
2
 
3
3
  This document provides a foldable, searchable reference for the public `jsonpit` Python API. Designed for 100% C# JsonPit parity, zero third-party runtime dependencies, and Smalltalk-grade object-oriented architecture.
4
4
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: jsonpit
3
- Version: 4.5.4
3
+ Version: 4.5.8
4
4
  Summary: Daemon-free distributed storage over Cloud Drives for developers and AI agents — multi-process, immutable history, zero dependencies.
5
5
  Author: Dr. Rainer Burkhardt
6
6
  License-Expression: Apache-2.0
@@ -35,9 +35,9 @@ The canonical **reference (lead) implementation** is C# `JsonPit` and the `pits`
35
35
 
36
36
  ---
37
37
 
38
- ## Cross-Tool & Cross-Language Version Alignment (`v4.5.4`)
38
+ ## Cross-Tool & Cross-Language Version Alignment (`v4.5.5`)
39
39
 
40
- `jsonpit` and the `jpit` CLI are version-synchronized to **`v4.5.4`** in direct lockstep with C# `JsonPit` in [RAIkeep](https://github.com/Burkhardt/RAIkeep).
40
+ `jsonpit` and the `jpit` CLI are version-synchronized to **`v4.5.5`** in direct lockstep with C# `JsonPit` in [RAIkeep](https://github.com/Burkhardt/RAIkeep).
41
41
 
42
42
  ### Why Harmonized Versioning is Essential:
43
43
  - **Instant Compatibility Clarity:** Operators, DevOps pipelines, and autonomous AI agents immediately know whether `jpit` and `pits` share the exact same distributed protocol revision. If both tools report `4.4.x`, they guarantee identical master leasing rules, receipt mechanisms, 7-digit UTC timestamp parsing, and CR041 clean change file conventions.
@@ -107,7 +107,7 @@ pipx install jsonpit
107
107
  # (or via companion alias)
108
108
  pipx install jpit
109
109
 
110
- # Upgrade to the latest lockstep release (v4.5.4)
110
+ # Upgrade to the latest lockstep release (v4.5.5)
111
111
  pipx upgrade jsonpit
112
112
  # (or)
113
113
  pipx upgrade jpit
@@ -13,9 +13,9 @@ The canonical **reference (lead) implementation** is C# `JsonPit` and the `pits`
13
13
 
14
14
  ---
15
15
 
16
- ## Cross-Tool & Cross-Language Version Alignment (`v4.5.4`)
16
+ ## Cross-Tool & Cross-Language Version Alignment (`v4.5.5`)
17
17
 
18
- `jsonpit` and the `jpit` CLI are version-synchronized to **`v4.5.4`** in direct lockstep with C# `JsonPit` in [RAIkeep](https://github.com/Burkhardt/RAIkeep).
18
+ `jsonpit` and the `jpit` CLI are version-synchronized to **`v4.5.5`** in direct lockstep with C# `JsonPit` in [RAIkeep](https://github.com/Burkhardt/RAIkeep).
19
19
 
20
20
  ### Why Harmonized Versioning is Essential:
21
21
  - **Instant Compatibility Clarity:** Operators, DevOps pipelines, and autonomous AI agents immediately know whether `jpit` and `pits` share the exact same distributed protocol revision. If both tools report `4.4.x`, they guarantee identical master leasing rules, receipt mechanisms, 7-digit UTC timestamp parsing, and CR041 clean change file conventions.
@@ -85,7 +85,7 @@ pipx install jsonpit
85
85
  # (or via companion alias)
86
86
  pipx install jpit
87
87
 
88
- # Upgrade to the latest lockstep release (v4.5.4)
88
+ # Upgrade to the latest lockstep release (v4.5.5)
89
89
  pipx upgrade jsonpit
90
90
  # (or)
91
91
  pipx upgrade jpit
@@ -38,7 +38,7 @@ from .store import (
38
38
  parse_and_validate_seed_payload,
39
39
  )
40
40
 
41
- __version__ = "4.5.4"
41
+ __version__ = "4.5.8"
42
42
 
43
43
  __all__ = [
44
44
  "Pit",
@@ -27,7 +27,7 @@ from .exceptions import (
27
27
  ProtectedAttributeError,
28
28
  StrictPatchValidationError,
29
29
  )
30
- from .fs import resolve_pit_target
30
+ from .fs import resolve_command_pit_root, resolve_pit_target
31
31
  from .icons import Icons
32
32
  from .store import (
33
33
  Pit,
@@ -59,7 +59,7 @@ def run_jq_filter(json_text: str, jq_filter: str) -> None:
59
59
 
60
60
  def discover_pits(
61
61
  target: str,
62
- cloud: str | None = "OneDrive",
62
+ cloud: str | None = None,
63
63
  root: str | None = None,
64
64
  ) -> list[tuple[Path, str]]:
65
65
  """
@@ -133,38 +133,9 @@ def discover_pits(
133
133
  return []
134
134
 
135
135
 
136
- def discover_root_pits(root: str | None, cloud: str | None = "OneDrive") -> list[str]:
137
- """Discovers all pit names under a resolved tenant root directory."""
138
- if not root:
139
- return []
140
- raw_str = str(root).strip()
141
- p = Path(os.path.expanduser(raw_str))
142
- if p.is_dir():
143
- candidate: Path | None = p
144
- else:
145
- cfg = OsConfig.load()
146
- if not cfg.is_config_loaded:
147
- raise PitNotFoundError(missing_configuration_diagnostic())
148
- cloud_root = cfg.get_cloud_root(cloud or "OneDrive")
149
- candidate = cloud_root / root if cloud_root else None
150
-
151
- if not candidate or not candidate.is_dir():
152
- return []
153
-
154
- found: list[str] = []
155
- for child in sorted(candidate.iterdir()):
156
- if child.is_dir():
157
- pit_file = child / f"{child.name}.pit"
158
- if pit_file.is_file():
159
- found.append(child.name)
160
- elif child.suffix.lower() == ".pit":
161
- found.append(child.stem)
162
- return sorted(list(dict.fromkeys(found)))
163
-
164
-
165
- def extract_cloud_and_root(argv: list[str]) -> tuple[str, str | None]:
136
+ def extract_cloud_and_root(argv: list[str]) -> tuple[str | None, str | None]:
166
137
  """Extracts -c/--cloud and -r/--root options from CLI token list."""
167
- cloud = "OneDrive"
138
+ cloud: str | None = None
168
139
  root = None
169
140
  i = 0
170
141
  while i < len(argv):
@@ -186,6 +157,19 @@ def extract_cloud_and_root(argv: list[str]) -> tuple[str, str | None]:
186
157
  return cloud, root
187
158
 
188
159
 
160
+ def expand_list_flag_bundles(argv: list[str]) -> list[str]:
161
+ """Expands the supported POSIX list bundles without changing other commands."""
162
+ if not any(argument in ("list", "ls") for argument in argv):
163
+ return argv
164
+ expanded: list[str] = []
165
+ for argument in argv:
166
+ if argument in ("-la", "-al"):
167
+ expanded.extend(["-l", "-a"])
168
+ else:
169
+ expanded.append(argument)
170
+ return expanded
171
+
172
+
189
173
  def cmd_grep(args: argparse.Namespace) -> int:
190
174
  """
191
175
  Pit-Grep: Living-state semantic search across single or multiple pits.
@@ -342,34 +326,144 @@ def cmd_history(args: argparse.Namespace) -> int:
342
326
  return 0
343
327
 
344
328
 
345
- def cmd_pits(args: argparse.Namespace) -> int:
346
- """Discovers and lists available pits under the tenant root (-r is required)."""
347
- use_color = should_color()
329
+ def _is_explicit_local_directory(value: str) -> bool:
330
+ """Returns whether value identifies a local filesystem path rather than a tenant name."""
331
+ return (
332
+ value.startswith(("/", "~", ".", "\\"))
333
+ or "/" in value
334
+ or "\\" in value
335
+ or Path(os.path.expanduser(value)).is_dir()
336
+ )
337
+
338
+
339
+ def _discover_pit_names(directory: Path) -> list[str]:
340
+ if not directory.is_dir():
341
+ return []
342
+ return sorted({
343
+ child.name
344
+ for child in directory.iterdir()
345
+ if child.is_dir() and (child / f"{child.name}.pit").is_file()
346
+ } | {
347
+ file.stem for file in directory.glob("*.pit") if file.is_file()
348
+ })
349
+
350
+
351
+ def _format_pit_listing(directory: Path, name: str, long_listing: bool) -> str:
352
+ if not long_listing:
353
+ return f" {name}"
354
+ nested = directory / name / f"{name}.pit"
355
+ flat = directory / f"{name}.pit"
356
+ metadata = (nested if nested.is_file() else flat).stat()
357
+ size = f"{metadata.st_size / 1024:.1f} KB"
358
+ modified = datetime.datetime.fromtimestamp(metadata.st_mtime).strftime("%Y-%m-%d %H:%M")
359
+ return f" {name:<12} {size:>9} {modified}"
360
+
361
+
362
+ def _format_discovery_directory(directory: Path) -> str:
363
+ """Matches pits' RaiPath display form, including its terminal separator."""
364
+ text = str(directory)
365
+ return text if text.endswith(os.sep) else text + os.sep
366
+
367
+
368
+ def _render_discovery(
369
+ provider: str | None,
370
+ directory: Path,
371
+ pits: list[str],
372
+ all_clouds: bool,
373
+ long_listing: bool = False,
374
+ ) -> str:
375
+ display_directory = _format_discovery_directory(directory)
376
+ if all_clouds:
377
+ if not pits:
378
+ return f"[{provider}] ({display_directory}): No pits found."
379
+ heading = f"[{provider}] ({display_directory}): {len(pits)} pit(s) found"
380
+ else:
381
+ heading = f"Found {len(pits)} pit(s) in cloud '{provider}' ({display_directory}):"
382
+ return "\n".join([heading, *[_format_pit_listing(directory, name, long_listing) for name in pits]])
383
+
384
+
385
+ def _discover_tenant_roots(root: str, cloud: str | None, all_clouds: bool) -> list[tuple[str | None, Path, list[str]]]:
386
+ """Discovers a local root or configured cloud tenant roots without guessing a provider."""
387
+ if cloud:
388
+ candidate = resolve_command_pit_root(root=root, cloud=cloud, target_name=root, operation="list")
389
+ return [(cloud, candidate, _discover_pit_names(candidate))]
390
+ if _is_explicit_local_directory(root):
391
+ candidate = Path(os.path.abspath(os.path.expanduser(root)))
392
+ return [(None, candidate, _discover_pit_names(candidate))]
393
+
394
+ cfg = OsConfig.load()
395
+ if not cfg.is_config_loaded:
396
+ raise PitNotFoundError(missing_configuration_diagnostic())
397
+ providers = [provider for provider in cfg.default_cloud_order if cfg.get_cloud_root(provider) is not None]
398
+ if not providers:
399
+ raise PitNotFoundError("No configured DefaultCloudOrder providers are available for tenant discovery.")
400
+ discoveries = [
401
+ (provider, cfg.get_cloud_root(provider) / root, _discover_pit_names(cfg.get_cloud_root(provider) / root))
402
+ for provider in providers
403
+ ]
404
+ if all_clouds:
405
+ return discoveries
406
+ return [next((discovery for discovery in discoveries if discovery[2]), discoveries[0])]
407
+
408
+
409
+ def _require_explicit_mutation_location(args: argparse.Namespace) -> None:
410
+ """Prevents mutations from silently selecting a provider from DefaultCloudOrder."""
411
+ if getattr(args, "cloud", None):
412
+ return
348
413
  root = getattr(args, "root", None)
349
- cloud = getattr(args, "cloud", "OneDrive")
414
+ if root and _is_explicit_local_directory(root):
415
+ return
416
+ pit = getattr(args, "pit", None)
417
+ if pit and _is_explicit_local_directory(str(pit)):
418
+ return
419
+ label = root or pit or "the requested Pit"
420
+ raise JsonPitError(
421
+ f"Mutation refuses to guess a cloud for '{label}'. Supply -c <provider> or an explicit local directory path."
422
+ )
423
+
424
+
425
+ def cmd_list_discovery(args: argparse.Namespace) -> int:
426
+ """Lists tenant pits with mandatory cloud and directory provenance."""
427
+ root = getattr(args, "root", None)
428
+ cloud = getattr(args, "cloud", None)
429
+ all_clouds = bool(getattr(args, "all", False))
430
+ long_listing = bool(getattr(args, "long", False))
350
431
  if not root:
351
- err_content = color(f"{Icons.ERROR} error: -r/--root is required to list pits (e.g. -r AIA)", C_BOLD + C_RED, use_color)
352
- sys.stderr.write(f"jpit pits: {err_content}\n")
353
- return 1
432
+ raise JsonPitError("list requires -r or --root <tenant-or-path>.")
433
+ if all_clouds and cloud:
434
+ raise JsonPitError("--all scans DefaultCloudOrder and cannot be combined with -c or --cloud.")
354
435
 
355
- pits = discover_root_pits(root, cloud)
436
+ discoveries = _discover_tenant_roots(root, cloud, all_clouds)
356
437
  if getattr(args, "json", False):
357
- sys.stdout.write(json.dumps(pits, indent=2) + "\n")
438
+ payload = [
439
+ {"cloud": provider, "path": str(directory), "pits": pits}
440
+ for provider, directory, pits in discoveries
441
+ ]
442
+ sys.stdout.write(json.dumps(payload, indent=2) + "\n")
358
443
  return 0
359
444
 
360
- if not pits:
361
- sys.stderr.write(f"No pits found under tenant root '{root}'.\n")
362
- return 1
445
+ if all_clouds:
446
+ for provider, directory, pits in discoveries:
447
+ sys.stdout.write(_render_discovery(provider, directory, pits, all_clouds=True, long_listing=long_listing) + "\n")
448
+ return 0
363
449
 
364
- for p in pits:
365
- sys.stdout.write(f"{p}\n")
450
+ provider, directory, pits = discoveries[0]
451
+ if provider is None:
452
+ sys.stdout.write(f"Found {len(pits)} pit(s) in local directory '{_format_discovery_directory(directory)}':\n")
453
+ for name in pits:
454
+ sys.stdout.write(_format_pit_listing(directory, name, long_listing) + "\n")
455
+ return 0
456
+ if not pits and cloud is None:
457
+ sys.stdout.write(f"No pits found under tenant root '{root}' across configured clouds.\n")
458
+ return 0
459
+ sys.stdout.write(_render_discovery(provider, directory, pits, all_clouds=False, long_listing=long_listing) + "\n")
366
460
  return 0
367
461
 
368
462
 
369
463
  def cmd_list(args: argparse.Namespace) -> int:
370
464
  """Lists active entities in a Pit, or available pits if <pit> is omitted."""
371
465
  if not getattr(args, "pit", None):
372
- return cmd_pits(args)
466
+ return cmd_list_discovery(args)
373
467
 
374
468
  with Pit.open(
375
469
  args.pit,
@@ -836,21 +930,11 @@ def cmd_maintain(args: argparse.Namespace) -> int:
836
930
  targets: list[tuple[Path, Path, str]] = []
837
931
 
838
932
  if wwwa:
839
- base_dir: Path | None = None
840
- if root:
841
- r_path = Path(os.path.expanduser(root))
842
- if r_path.is_dir():
843
- base_dir = r_path
844
- if base_dir is None:
845
- cfg = OsConfig.load()
846
- if not cfg.is_config_loaded:
847
- sys.stderr.write(f"[jpit] Error: {missing_configuration_diagnostic()}\n")
848
- return 1
849
- cloud_root = cfg.get_cloud_root(cloud)
850
- if not cloud_root or not cloud_root.is_dir():
851
- sys.stderr.write(f"[jpit] Error: Cloud provider '{cloud}' root directory not found.\n")
852
- return 1
853
- base_dir = cloud_root / root.strip("/\\") if root else cloud_root
933
+ try:
934
+ base_dir = resolve_command_pit_root(root=root, cloud=cloud, target_name="WWWA", operation="maintenance")
935
+ except Exception as ex:
936
+ sys.stderr.write(f"[jpit] Error: {ex}\n")
937
+ return 1
854
938
 
855
939
  if not base_dir.is_dir():
856
940
  sys.stderr.write(f"[jpit] Error: The maintenance root does not exist: {base_dir}\n")
@@ -952,21 +1036,11 @@ def cmd_audit(args: argparse.Namespace) -> int:
952
1036
  directories: list[Path] = []
953
1037
 
954
1038
  if wwwa:
955
- base_dir: Path | None = None
956
- if root:
957
- r_path = Path(os.path.expanduser(root))
958
- if r_path.is_dir():
959
- base_dir = r_path
960
- if base_dir is None:
961
- cfg = OsConfig.load()
962
- if not cfg.is_config_loaded:
963
- sys.stderr.write(f"[jpit] Error: {missing_configuration_diagnostic()}\n")
964
- return 1
965
- cloud_root = cfg.get_cloud_root(cloud)
966
- if not cloud_root or not cloud_root.is_dir():
967
- sys.stderr.write(f"[jpit] Error: Cloud provider '{cloud}' root directory not found.\n")
968
- return 1
969
- base_dir = cloud_root / root.strip("/\\") if root else cloud_root
1039
+ try:
1040
+ base_dir = resolve_command_pit_root(root=root, cloud=cloud, target_name="WWWA", operation="audit")
1041
+ except Exception as ex:
1042
+ sys.stderr.write(f"[jpit] Error: {ex}\n")
1043
+ return 1
970
1044
 
971
1045
  for name in WWWA_PITS:
972
1046
  directories.append(base_dir / name)
@@ -1033,8 +1107,7 @@ def get_cloud_options_description(use_color: bool = True) -> str:
1033
1107
  glyph = Icons.cloud_provider_icon(name, idx + 1)
1034
1108
  badge_color = OPTION_BADGE_COLORS[idx % len(OPTION_BADGE_COLORS)]
1035
1109
  badge = color(glyph, badge_color, use_color)
1036
- default_str = " (default)" if idx == 0 else ""
1037
- formatted.append(f"{badge} {name}{default_str}")
1110
+ formatted.append(f"{badge} {name}")
1038
1111
 
1039
1112
  return ", ".join(formatted)
1040
1113
 
@@ -1055,7 +1128,8 @@ def print_top_help(
1055
1128
  nologo: bool = False,
1056
1129
  use_color: bool = True,
1057
1130
  root: str | None = None,
1058
- cloud: str | None = "OneDrive",
1131
+ cloud: str | None = None,
1132
+ wwwa: bool = False,
1059
1133
  ) -> None:
1060
1134
  """Prints the branded jpit help screen with Nerd Font glyphs and multi-token ANSI accents."""
1061
1135
  if not nologo:
@@ -1067,14 +1141,14 @@ def print_top_help(
1067
1141
  i_folder = color(Icons.FOLDER, C_YELLOW, use_color)
1068
1142
  i_banner = color(Icons.BANNER, C_CYAN, use_color)
1069
1143
 
1070
- cmd_list = "grep, get, history, list, put, set, del, del-prop, rename, export, status, pits, maintain, audit"
1144
+ cmd_list = "grep, get, history, list (ls), put, set, del, del-prop, rename, export, status, maintain, audit"
1071
1145
  sys.stdout.write(f"{c_cmd}\t{i_info}\t{cmd_list}\n")
1072
1146
 
1073
1147
  commands_spec = [
1074
1148
  ("grep", "<pattern> [<target>] [-i] [-e] [-p <prop>] [--at <ts>] [--json] [--jq <expr>]"),
1075
1149
  ("get", "<PitName> <ItemId> [--at <ts>] [--jq <expr>] [--with-deleted]"),
1076
1150
  ("history", "<PitName> <ItemId> [--jq <expr>]"),
1077
- ("list", "[<PitName>] [--json] [--jq <expr>] (lists pits if <PitName> omitted)"),
1151
+ ("list", "[<PitName>] [-r <tenant|path>] [-c <cloud>] [-a|--all] [-l|--long] [--json]"),
1078
1152
  ("put", "<PitName> [<source>] [-s <source>] (alias: seed)"),
1079
1153
  ("set", "<PitName> <ItemId> <payload>"),
1080
1154
  ("del", "<PitName> <ItemId> [--by <author>]"),
@@ -1082,7 +1156,6 @@ def print_top_help(
1082
1156
  ("rename", "<PitName> <OldId> <NewId> [--by <author>]"),
1083
1157
  ("export", "<PitName> [--out <file>] [--at <ts>] [--jq <expr>]"),
1084
1158
  ("status", "<PitName>"),
1085
- ("pits", "[-r <root>] [-c <cloud>] [--json] (discover available pits)"),
1086
1159
  ("maintain", "(<PitName> | --wwwa) [--apply] [--prune-process-flags --older-than <dur>] [--json]"),
1087
1160
  ("audit", "(<PitName> | --wwwa) [--machine <all|local|name>] [--level <severity>] [--json]"),
1088
1161
  ]
@@ -1114,7 +1187,7 @@ def print_top_help(
1114
1187
  pits_title = color(f"{Icons.INFO} PitNames", C_CYAN, use_color)
1115
1188
  if root:
1116
1189
  try:
1117
- discovered = discover_root_pits(root, cloud)
1190
+ discovered = _discover_tenant_roots(root, cloud, all_clouds=False)[0][2]
1118
1191
  except Exception:
1119
1192
  discovered = []
1120
1193
  if discovered:
@@ -1132,6 +1205,40 @@ def print_top_help(
1132
1205
  hint = "specify -r <root> (e.g. -r AIA) to discover pits"
1133
1206
  sys.stdout.write(f"{pits_title}\t{i_folder}\t{hint}\n")
1134
1207
 
1208
+ if wwwa or _has_wwwa_pit(root, cloud):
1209
+ for name in WWWA_PITS:
1210
+ separator = "\t\t" if name == "Place" else "\t"
1211
+ status, path = _wwwa_pit_status(name, root, cloud)
1212
+ sys.stdout.write(f"{Icons.INFO} {name}{separator}{status}\t{path}\n")
1213
+
1214
+
1215
+ def _wwwa_roots(root: str | None, cloud: str | None) -> list[Path]:
1216
+ """Returns resolved help roots without making a cloud selection implicit."""
1217
+ if not root:
1218
+ return []
1219
+ try:
1220
+ return [directory for _, directory, _ in _discover_tenant_roots(root, cloud, all_clouds=False)]
1221
+ except Exception:
1222
+ return []
1223
+
1224
+
1225
+ def _has_wwwa_pit(root: str | None, cloud: str | None) -> bool:
1226
+ return any(
1227
+ (directory / name / f"{name}.pit").is_file()
1228
+ for directory in _wwwa_roots(root, cloud)
1229
+ for name in WWWA_PITS
1230
+ )
1231
+
1232
+
1233
+ def _wwwa_pit_status(name: str, root: str | None, cloud: str | None) -> tuple[str, str]:
1234
+ """Matches pits' WWWA help line: status glyph plus its resolved Pit file path."""
1235
+ roots = _wwwa_roots(root, cloud)
1236
+ if not roots:
1237
+ return Icons.NOT_AVAILABLE, f"{name}.pit"
1238
+
1239
+ pit_file = roots[0] / name / f"{name}.pit"
1240
+ return (Icons.SUCCESS if pit_file.is_file() else Icons.NOT_AVAILABLE), str(pit_file)
1241
+
1135
1242
 
1136
1243
  def print_command_help(cmd: str, nologo: bool = False, use_color: bool = True) -> None:
1137
1244
  """Prints formatted help for an individual subcommand."""
@@ -1199,14 +1306,6 @@ def print_command_help(cmd: str, nologo: bool = False, use_color: bool = True) -
1199
1306
  ("--jq <expr>", "Convenience pipe through jq filter"),
1200
1307
  ],
1201
1308
  },
1202
- "pits": {
1203
- "usage": "jpit pits [-r <root>] [-c <cloud>] [--json]",
1204
- "desc": "Discover available pits under a tenant root (-r is required).",
1205
- "args": [],
1206
- "opts": [
1207
- ("--json", "Output JSON array of pit names"),
1208
- ],
1209
- },
1210
1309
  "put": {
1211
1310
  "usage": "jpit put <PitName> [<source>] [-s <source>] [--require-existing] [--patch] [options]",
1212
1311
  "desc": "Ingest JSON5 entities from file or stdin pipe (alias: seed).",
@@ -1492,14 +1591,12 @@ def build_parser(prog: str | None = None) -> JsonPitArgumentParser:
1492
1591
  p_hist.add_argument("--jq", help="Convenience pipe through jq filter")
1493
1592
 
1494
1593
  # list
1495
- p_list = subparsers.add_parser("list", parents=[common_parser], help="List active entities in a Pit (or available pits if <pit> omitted)")
1594
+ p_list = subparsers.add_parser("list", aliases=["ls"], parents=[common_parser], help="List active entities or discover tenant pits")
1496
1595
  p_list.add_argument("pit", nargs="?", default=None, help="Pit name (optional: if omitted, lists available pits in -r root)")
1497
1596
  p_list.add_argument("--json", action="store_true", help="Output JSON array")
1498
1597
  p_list.add_argument("--jq", help="Convenience pipe through jq filter")
1499
-
1500
- # pits
1501
- p_pits = subparsers.add_parser("pits", parents=[common_parser], help="Discover available pits under a tenant root (-r is required)")
1502
- p_pits.add_argument("--json", action="store_true", help="Output JSON array of pit names")
1598
+ p_list.add_argument("-a", "--all", action="store_true", help="Discover the tenant in every DefaultCloudOrder provider")
1599
+ p_list.add_argument("-l", "--long", action="store_true", help="Include each pit file's size and last modification time")
1503
1600
 
1504
1601
  # put / seed
1505
1602
  p_put = subparsers.add_parser(
@@ -1603,20 +1700,22 @@ def build_parser(prog: str | None = None) -> JsonPitArgumentParser:
1603
1700
  def main(argv: list[str] | None = None) -> int:
1604
1701
  if argv is None:
1605
1702
  argv = sys.argv[1:]
1703
+ argv = expand_list_flag_bundles(argv)
1606
1704
 
1607
1705
  use_color = should_color()
1608
1706
  nologo = "-n" in argv or "--nologo" in argv
1609
1707
  cloud, root = extract_cloud_and_root(argv)
1708
+ wwwa = "--wwwa" in argv
1610
1709
 
1611
1710
  known_subcommands = {
1612
- "grep", "get", "history", "list", "put", "seed", "set", "del",
1711
+ "grep", "get", "history", "list", "ls", "put", "seed", "set", "del",
1613
1712
  "delete", "delete-item", "del-prop", "delete-property",
1614
- "rename", "export", "status", "pits", "maintain", "audit",
1713
+ "rename", "export", "status", "maintain", "audit",
1615
1714
  }
1616
1715
 
1617
1716
  # Quick check for top-level help or no arguments
1618
1717
  if len(argv) == 0 or (len(argv) == 1 and argv[0] in ("-h", "--help")):
1619
- print_top_help(nologo=nologo, use_color=use_color, root=root, cloud=cloud)
1718
+ print_top_help(nologo=nologo, use_color=use_color, root=root, cloud=cloud, wwwa=wwwa)
1620
1719
  return 0
1621
1720
 
1622
1721
  # Check for command-specific or option-specific help: e.g. jpit --retain-window -h or jpit grep -h
@@ -1629,14 +1728,14 @@ def main(argv: list[str] | None = None) -> int:
1629
1728
  if target_cmd:
1630
1729
  print_command_help(target_cmd, nologo=nologo, use_color=use_color)
1631
1730
  return 0
1632
- print_top_help(nologo=nologo, use_color=use_color, root=root, cloud=cloud)
1731
+ print_top_help(nologo=nologo, use_color=use_color, root=root, cloud=cloud, wwwa=wwwa)
1633
1732
  return 0
1634
1733
 
1635
1734
  parser = build_parser()
1636
1735
  args = parser.parse_args(argv)
1637
1736
 
1638
1737
  if not hasattr(args, "cloud"):
1639
- args.cloud = "OneDrive"
1738
+ args.cloud = None
1640
1739
  if not hasattr(args, "root"):
1641
1740
  args.root = None
1642
1741
 
@@ -1645,7 +1744,7 @@ def main(argv: list[str] | None = None) -> int:
1645
1744
  "get": cmd_get,
1646
1745
  "history": cmd_history,
1647
1746
  "list": cmd_list,
1648
- "pits": cmd_pits,
1747
+ "ls": cmd_list,
1649
1748
  "put": cmd_put,
1650
1749
  "seed": cmd_put,
1651
1750
  "set": cmd_set,
@@ -1667,6 +1766,9 @@ def main(argv: list[str] | None = None) -> int:
1667
1766
  return 1
1668
1767
 
1669
1768
  try:
1769
+ mutating_commands = {"put", "seed", "set", "del", "delete", "delete-item", "del-prop", "delete-property", "rename"}
1770
+ if args.command in mutating_commands or (args.command == "maintain" and args.apply):
1771
+ _require_explicit_mutation_location(args)
1670
1772
  return cmd_func(args)
1671
1773
  except JsonPitError as ex:
1672
1774
  err_icon = color(Icons.ERROR, C_BOLD + C_RED, use_color)
@@ -1682,5 +1784,3 @@ def main(argv: list[str] | None = None) -> int:
1682
1784
 
1683
1785
  if __name__ == "__main__":
1684
1786
  sys.exit(main())
1685
-
1686
-
@@ -73,6 +73,50 @@ def resolve_pit_target(
73
73
  return target_dir, pit_name
74
74
 
75
75
 
76
+ def resolve_command_pit_root(
77
+ root: str | None = None,
78
+ cloud: str | None = None,
79
+ target_name: str = "WWWA",
80
+ operation: str = "maintenance",
81
+ config: OsConfig | None = None,
82
+ ) -> Path:
83
+ """
84
+ Resolves a tenant pit root directory adhering to 100% C# pits CLI parity:
85
+ - If an explicit filesystem path is provided (e.g. starting with /, ~, .), resolves directly.
86
+ - If cloud is provided: resolves cloud root via OsConfig and appends relative root.
87
+ Crucially, does NOT treat relative root strings as local filesystem directories.
88
+ - If cloud is not provided and root is provided: resolves root directly.
89
+ - If neither is provided: raises PitNotFoundError.
90
+ """
91
+ raw_root = str(root).strip() if root else ""
92
+ is_explicit = (
93
+ isinstance(root, Path)
94
+ or raw_root.startswith(("/", "~", ".", "\\"))
95
+ or ("/" in raw_root and not raw_root.startswith("-"))
96
+ or ("\\" in raw_root)
97
+ )
98
+ if is_explicit:
99
+ return Path(os.path.abspath(os.path.expanduser(raw_root)))
100
+
101
+ cfg = config or OsConfig.load()
102
+ if cloud:
103
+ if not cfg.is_config_loaded:
104
+ raise PitNotFoundError(missing_configuration_diagnostic())
105
+ cloud_root = cfg.get_cloud_root(cloud)
106
+ if not cloud_root or not cloud_root.is_dir():
107
+ raise PitNotFoundError(
108
+ f"The requested cloud provider '{cloud}' is missing or empty in {DEFAULT_CONFIG_FILE_LOCATION}."
109
+ )
110
+ return cloud_root / raw_root.strip("/\\") if raw_root else cloud_root
111
+
112
+ if raw_root:
113
+ return Path(os.path.abspath(os.path.expanduser(raw_root)))
114
+
115
+ raise PitNotFoundError(
116
+ f"Cannot resolve {operation} target '{target_name}' without -r or --pitroot, or a configured -c or --cloud provider."
117
+ )
118
+
119
+
76
120
  def ensure_directory(path: Path | str) -> Path:
77
121
  """Ensures the directory exists, creating intermediate parents if needed."""
78
122
  p = Path(path)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "jsonpit"
7
- version = "4.5.4"
7
+ version = "4.5.8"
8
8
  description = "Daemon-free distributed storage over Cloud Drives for developers and AI agents — multi-process, immutable history, zero dependencies."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes