@leejungkiin/awkit 1.7.0 → 1.7.4
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.
- package/bin/awk.js +576 -84
- package/core/CLAUDE.md +1 -1
- package/core/GEMINI.md +148 -167
- package/core/GEMINI.md.bak +149 -116
- package/core/skill-runtime-manifest.json +3 -0
- package/docs/Claude Fable 5.md +3826 -0
- package/docs/android_kotlin_system_instruction.md +210 -0
- package/docs/brainstorm_ponytail_integration.md +146 -0
- package/docs/brainstorm_smart_setup.md +113 -0
- package/docs/deep-research-report (1).md +293 -0
- package/docs/history/GEMINI.v1.md +135 -0
- package/docs/history/brainstorm_antigravity_unified_architecture.v1.md +105 -0
- package/docs/history/implementation_plan.v1.md +58 -0
- package/package.json +4 -1
- package/scripts/artifact-storage.js +130 -0
- package/scripts/automation-gate.js +40 -7
- package/scripts/claude-plan.js +76 -0
- package/scripts/dependency-manager.js +210 -0
- package/scripts/exec-rtk.js +11 -5
- package/scripts/i18n-helper.js +381 -0
- package/scripts/multi-model-pipeline.js +144 -0
- package/skill-packs/mobile-ios/pack.json +4 -2
- package/skill-packs/reverse-engineering/pack.json +1 -0
- package/skills/CATALOG.md +20 -0
- package/skills/GEMINI.md +9 -1
- package/skills/TRIGGER_INDEX.md +10 -0
- package/skills/ai-music/SKILL.md +275 -0
- package/skills/android-re-analyzer/SKILL.md +238 -0
- package/skills/android-re-analyzer/references/api-extraction-patterns.md +119 -0
- package/skills/android-re-analyzer/references/call-flow-analysis.md +176 -0
- package/skills/android-re-analyzer/references/fernflower-usage.md +115 -0
- package/skills/android-re-analyzer/references/jadx-usage.md +116 -0
- package/skills/android-re-analyzer/references/setup-guide.md +221 -0
- package/skills/android-re-analyzer/scripts/check-deps.sh +129 -0
- package/skills/android-re-analyzer/scripts/decompile.sh +375 -0
- package/skills/android-re-analyzer/scripts/find-api-calls.sh +118 -0
- package/skills/android-re-analyzer/scripts/install-dep.sh +448 -0
- package/skills/animal-island-ui-style/SKILL.md +1450 -0
- package/skills/app-store-review-agent/SKILL.md +164 -0
- package/skills/app-store-review-agent/references/guidelines/README.md +154 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/ai_apps.md +37 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/all_apps.md +50 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/crypto_finance.md +31 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/games.md +31 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/health_fitness.md +31 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/kids.md +27 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/macos.md +38 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/social_ugc.md +32 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/subscription_iap.md +34 -0
- package/skills/app-store-review-agent/references/guidelines/by-app-type/vpn.md +18 -0
- package/skills/app-store-review-agent/references/rules/design/minimum_functionality.md +96 -0
- package/skills/app-store-review-agent/references/rules/design/sign_in_with_apple.md +54 -0
- package/skills/app-store-review-agent/references/rules/entitlements/unused_entitlements.md +83 -0
- package/skills/app-store-review-agent/references/rules/metadata/accurate_metadata.md +54 -0
- package/skills/app-store-review-agent/references/rules/metadata/apple_trademark.md +99 -0
- package/skills/app-store-review-agent/references/rules/metadata/china_storefront.md +72 -0
- package/skills/app-store-review-agent/references/rules/metadata/competitor_terms.md +56 -0
- package/skills/app-store-review-agent/references/rules/metadata/subscription_metadata.md +81 -0
- package/skills/app-store-review-agent/references/rules/privacy/privacy_manifest.md +84 -0
- package/skills/app-store-review-agent/references/rules/privacy/unnecessary_data.md +60 -0
- package/skills/app-store-review-agent/references/rules/subscription/misleading_pricing.md +63 -0
- package/skills/app-store-review-agent/references/rules/subscription/missing_tos_pp.md +54 -0
- package/skills/awf-ponytail/SKILL.md +91 -0
- package/skills/awf-ponytail-review/SKILL.md +67 -0
- package/skills/awf-session-restore/SKILL.md +3 -3
- package/skills/brainstorm-agent/SKILL.md +11 -2
- package/skills/brainstorm-agent/templates/brief-template.md +8 -0
- package/skills/claude-planner/SKILL.md +47 -0
- package/skills/code-review/SKILL.md +87 -0
- package/skills/expo-game-development/SKILL.md +163 -0
- package/skills/flutter/LICENSE.txt +202 -0
- package/skills/flutter/SKILL.md +127 -0
- package/skills/flutter-project-creater/LICENSE.txt +202 -0
- package/skills/flutter-project-creater/SKILL.md +106 -0
- package/skills/game-developer/SKILL.md +163 -0
- package/skills/game-developer/references/ecs-patterns.md +501 -0
- package/skills/game-developer/references/multiplayer-networking.md +475 -0
- package/skills/game-developer/references/performance-optimization.md +422 -0
- package/skills/game-developer/references/unity-patterns.md +271 -0
- package/skills/game-developer/references/unreal-cpp.md +352 -0
- package/skills/generate-gui-assets/SKILL.md +305 -0
- package/skills/generate-gui-assets/agents/openai.yaml +4 -0
- package/skills/generate-gui-assets/references/catalog-schema.md +58 -0
- package/skills/generate-gui-assets/references/extraction-techniques.md +21 -0
- package/skills/generate-gui-assets/references/prompt-patterns.md +58 -0
- package/skills/generate-gui-assets/scripts/__pycache__/clean_chroma_edges.cpython-311.pyc +0 -0
- package/skills/generate-gui-assets/scripts/build_gui_contact_sheet.py +51 -0
- package/skills/generate-gui-assets/scripts/clean_chroma_edges.py +262 -0
- package/skills/generate-gui-assets/scripts/copy_approved_icons.py +64 -0
- package/skills/generate-gui-assets/scripts/prepare_gui_asset_run.py +91 -0
- package/skills/generate-gui-assets/scripts/suggest_grid_options.py +63 -0
- package/skills/generate-gui-assets/scripts/validate_gui_catalog.py +50 -0
- package/skills/godot-game-development/SKILL.md +142 -0
- package/skills/hatch-pet/LICENSE.txt +201 -0
- package/skills/hatch-pet/SKILL.md +420 -0
- package/skills/hatch-pet/agents/openai.yaml +4 -0
- package/skills/hatch-pet/references/animation-rows.md +29 -0
- package/skills/hatch-pet/references/codex-pet-contract.md +35 -0
- package/skills/hatch-pet/references/qa-rubric.md +60 -0
- package/skills/hatch-pet/scripts/__pycache__/clean_chroma_edges.cpython-311.pyc +0 -0
- package/skills/hatch-pet/scripts/clean_chroma_edges.py +262 -0
- package/skills/hatch-pet/scripts/compose_atlas.py +150 -0
- package/skills/hatch-pet/scripts/derive_running_left_from_running_right.py +143 -0
- package/skills/hatch-pet/scripts/extract_strip_frames.py +323 -0
- package/skills/hatch-pet/scripts/finalize_pet_run.py +382 -0
- package/skills/hatch-pet/scripts/generate_pet_images.py +287 -0
- package/skills/hatch-pet/scripts/inspect_frames.py +246 -0
- package/skills/hatch-pet/scripts/make_contact_sheet.py +96 -0
- package/skills/hatch-pet/scripts/package_custom_pet.py +108 -0
- package/skills/hatch-pet/scripts/pet_job_status.py +117 -0
- package/skills/hatch-pet/scripts/prepare_pet_run.py +673 -0
- package/skills/hatch-pet/scripts/queue_pet_repairs.py +172 -0
- package/skills/hatch-pet/scripts/record_imagegen_result.py +250 -0
- package/skills/hatch-pet/scripts/render_animation_videos.py +134 -0
- package/skills/hatch-pet/scripts/render_animation_videos.sh +5 -0
- package/skills/hatch-pet/scripts/validate_atlas.py +139 -0
- package/skills/i18n-orchestrator/SKILL.md +37 -0
- package/skills/ios-simulator-skill/SKILL.md +390 -0
- package/skills/ios-simulator-skill/scripts/accessibility_audit.py +300 -0
- package/skills/ios-simulator-skill/scripts/app_launcher.py +326 -0
- package/skills/ios-simulator-skill/scripts/app_state_capture.py +400 -0
- package/skills/ios-simulator-skill/scripts/appearance.py +385 -0
- package/skills/ios-simulator-skill/scripts/build_and_test.py +348 -0
- package/skills/ios-simulator-skill/scripts/clipboard.py +103 -0
- package/skills/ios-simulator-skill/scripts/common/__init__.py +61 -0
- package/skills/ios-simulator-skill/scripts/common/cache_utils.py +289 -0
- package/skills/ios-simulator-skill/scripts/common/device_utils.py +462 -0
- package/skills/ios-simulator-skill/scripts/common/env_config.py +35 -0
- package/skills/ios-simulator-skill/scripts/common/hang_pipeline.py +862 -0
- package/skills/ios-simulator-skill/scripts/common/hang_sessions.py +490 -0
- package/skills/ios-simulator-skill/scripts/common/idb_utils.py +180 -0
- package/skills/ios-simulator-skill/scripts/common/screenshot_utils.py +338 -0
- package/skills/ios-simulator-skill/scripts/container.py +668 -0
- package/skills/ios-simulator-skill/scripts/gesture.py +394 -0
- package/skills/ios-simulator-skill/scripts/hang_watcher.py +1533 -0
- package/skills/ios-simulator-skill/scripts/keyboard.py +391 -0
- package/skills/ios-simulator-skill/scripts/localization_audit.py +483 -0
- package/skills/ios-simulator-skill/scripts/location.py +467 -0
- package/skills/ios-simulator-skill/scripts/log_monitor.py +493 -0
- package/skills/ios-simulator-skill/scripts/model_inspector.py +645 -0
- package/skills/ios-simulator-skill/scripts/navigator.py +461 -0
- package/skills/ios-simulator-skill/scripts/privacy_manager.py +310 -0
- package/skills/ios-simulator-skill/scripts/push_notification.py +240 -0
- package/skills/ios-simulator-skill/scripts/screen_mapper.py +296 -0
- package/skills/ios-simulator-skill/scripts/sim_health_check.sh +245 -0
- package/skills/ios-simulator-skill/scripts/sim_list.py +299 -0
- package/skills/ios-simulator-skill/scripts/simctl_boot.py +312 -0
- package/skills/ios-simulator-skill/scripts/simctl_create.py +316 -0
- package/skills/ios-simulator-skill/scripts/simctl_delete.py +357 -0
- package/skills/ios-simulator-skill/scripts/simctl_erase.py +351 -0
- package/skills/ios-simulator-skill/scripts/simctl_shutdown.py +290 -0
- package/skills/ios-simulator-skill/scripts/simulator_selector.py +375 -0
- package/skills/ios-simulator-skill/scripts/status_bar.py +250 -0
- package/skills/ios-simulator-skill/scripts/test_recorder.py +323 -0
- package/skills/ios-simulator-skill/scripts/visual_diff.py +235 -0
- package/skills/ios-simulator-skill/scripts/xcode/__init__.py +13 -0
- package/skills/ios-simulator-skill/scripts/xcode/builder.py +397 -0
- package/skills/ios-simulator-skill/scripts/xcode/cache.py +204 -0
- package/skills/ios-simulator-skill/scripts/xcode/config.py +178 -0
- package/skills/ios-simulator-skill/scripts/xcode/reporter.py +343 -0
- package/skills/ios-simulator-skill/scripts/xcode/xcresult.py +451 -0
- package/skills/ios-visual-qa-strategist/SKILL.md +111 -0
- package/skills/ios-visual-qa-strategist/agents/openai.yaml +4 -0
- package/skills/ios-visual-qa-strategist/references/ios-tool-selection.md +61 -0
- package/skills/ios-visual-qa-strategist/references/minimal-capture-policy.md +56 -0
- package/skills/ios-visual-qa-strategist/references/visual-reasoning-heuristics.md +53 -0
- package/skills/orchestrator/SKILL.md +0 -20
- package/skills/persistent-storage/SKILL.md +55 -0
- package/skills/short-maker/SKILL.md +23 -0
- package/skills/short-maker/scripts/effects.js +56 -0
- package/skills/short-maker/scripts/shortmaker-bridge.js +332 -0
- package/skills/short-maker/scripts/videomix.js +601 -0
- package/skills/short-maker/templates/hyperframes/cinematic-character.template.html +172 -0
- package/skills/short-maker/templates/hyperframes/index.template.html +194 -0
- package/skills/smali-to-kotlin/SKILL.md +128 -0
- package/skills/smali-to-kotlin/examples/getting-started/tech-stack.md +58 -0
- package/skills/smali-to-kotlin/examples/pipeline/data-ui-parity.md +118 -0
- package/skills/smali-to-kotlin/examples/pipeline/scanner-and-bootstrap.md +106 -0
- package/skills/smali-to-kotlin/library-patterns.md +189 -0
- package/skills/smali-to-kotlin/phase-0-discovery.md +128 -0
- package/skills/smali-to-kotlin/phase-1-architecture.md +166 -0
- package/skills/smali-to-kotlin/phase-2-blueprint-ui.md +347 -0
- package/skills/smali-to-kotlin/phase-2-blueprint.md +228 -0
- package/skills/smali-to-kotlin/phase-3-build.md +248 -0
- package/skills/smali-to-kotlin/phase-3-logic-build.md +268 -0
- package/skills/smali-to-kotlin/smali-reading-guide.md +310 -0
- package/skills/smali-to-kotlin/templates/app-map.md +101 -0
- package/skills/smali-to-kotlin/templates/architecture.md +142 -0
- package/skills/smali-to-kotlin/templates/blueprint.md +145 -0
- package/skills/spec-gate/SKILL.md +6 -2
- package/skills/symphony-enforcer/SKILL.md +8 -0
- package/skills/symphony-enforcer/examples/mindful-stop.md +2 -0
- package/skills/symphony-enforcer/examples/three-phase.md +16 -0
- package/skills/symphony-enforcer/examples/trigger-points.md +7 -1
- package/skills/unity-game-development/SKILL.md +231 -0
- package/skills/verification-gate/SKILL.md +4 -2
- package/skills/video-edit/SKILL.md +36 -0
- package/skills/video-edit/scripts/video_edit.py +324 -0
- package/templates/setup-mapping.json +48 -0
- package/templates/specs/design-template.md +161 -71
- package/templates/specs/requirements-template.md +65 -133
- package/templates/specs/task-spec-template.xml +3 -0
- package/workflows/_uncategorized/critic.md +40 -0
- package/workflows/_uncategorized/git-rebase-flow.md +81 -0
- package/workflows/_uncategorized/image-gen.md +118 -0
- package/workflows/_uncategorized/multi-model-pipeline.md +60 -0
- package/workflows/_uncategorized/pixel-gen.md +86 -0
- package/workflows/_uncategorized/pixel-setup.md +90 -0
- package/workflows/_uncategorized/ponytail-review.md +59 -0
- package/workflows/_uncategorized/reverse-android-build.md +222 -0
- package/workflows/_uncategorized/reverse-android-design.md +139 -0
- package/workflows/_uncategorized/reverse-android-discover.md +150 -0
- package/workflows/_uncategorized/reverse-android-scan.md +158 -0
- package/workflows/_uncategorized/reverse-android.md +143 -0
- package/workflows/_uncategorized/reverse-ios-build.md +240 -0
- package/workflows/_uncategorized/reverse-ios-design.md +112 -0
- package/workflows/_uncategorized/reverse-ios-discover.md +120 -0
- package/workflows/_uncategorized/reverse-ios-scan.md +155 -0
- package/workflows/_uncategorized/reverse-ios.md +152 -0
- package/workflows/_uncategorized/safety-router.md +34 -0
- package/workflows/_uncategorized/teach.md +89 -0
- package/workflows/_uncategorized/verify-ui.md +53 -0
- package/workflows/_uncategorized/visualize-screenshots.md +34 -0
- package/workflows/ads/ads-analyst.md +201 -0
- package/workflows/ads/ads-audit.md +106 -0
- package/workflows/ads/ads-optimize.md +97 -0
- package/workflows/ads/ads-targeting.md +241 -0
- package/workflows/ads/adsExpert.md +160 -0
- package/workflows/ads/smali-ads-config.md +400 -0
- package/workflows/ads/smali-ads-flow.md +331 -0
- package/workflows/ads/smali-ads-interstitial.md +377 -0
- package/workflows/ads/smali-ads-native.md +382 -0
- package/workflows/context/teach.md +89 -0
- package/workflows/gitnexus.md +8 -8
- package/workflows/lifecycle/brainstorm.md +43 -0
- package/workflows/lifecycle/code.md +5 -0
- package/workflows/lifecycle/init.md +23 -5
- package/workflows/lifecycle/multi-model-pipeline.md +60 -0
- package/workflows/quality/ponytail-review.md +59 -0
- package/workflows/roles/critic.md +40 -0
- package/workflows/roles/safety-router.md +34 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
App sandbox / UserDefaults / Core Data inspector for iOS simulators.
|
|
4
|
+
|
|
5
|
+
Wraps `xcrun simctl get_app_container` to provide semantic access to an
|
|
6
|
+
app's data container — no manual CoreSimulator directory archaeology needed.
|
|
7
|
+
|
|
8
|
+
Key features:
|
|
9
|
+
- List files in the app data container (--ls)
|
|
10
|
+
- Read a file from the container (--cat, with progressive cache for large files)
|
|
11
|
+
- Inspect UserDefaults as JSON or key=value (--userdefaults)
|
|
12
|
+
- Locate Core Data stores (--core-data-path)
|
|
13
|
+
- Export the full data container (--export)
|
|
14
|
+
- JSON output mode (--json) and auto-UDID detection
|
|
15
|
+
|
|
16
|
+
Keychain is explicitly out of scope — requires entitlements and is risky.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import contextlib
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import plistlib
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
from decimal import Decimal
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from common.cache_utils import ProgressiveCache
|
|
31
|
+
from common.device_utils import resolve_device_identifier
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _env_int(name: str, default: int, *, min_value: int = 0) -> int:
|
|
35
|
+
"""Read an integer from the environment with a minimum clamp."""
|
|
36
|
+
raw = os.environ.get(name)
|
|
37
|
+
if raw is None:
|
|
38
|
+
return default
|
|
39
|
+
try:
|
|
40
|
+
return max(min_value, int(raw))
|
|
41
|
+
except ValueError:
|
|
42
|
+
return default
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Threshold in bytes above which --cat output is cached instead of printed inline
|
|
46
|
+
_CAT_CACHE_THRESHOLD_BYTES = _env_int("IOS_SIM_CAT_CACHE_BYTES", 8_192, min_value=512)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ContainerInspector:
|
|
50
|
+
"""Inspect an app's data sandbox on an iOS simulator."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, udid: str | None = None):
|
|
53
|
+
"""Initialize inspector with optional device UDID.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
udid: Simulator UDID (uses booted device if None)
|
|
57
|
+
"""
|
|
58
|
+
self.udid = udid
|
|
59
|
+
self._cache = ProgressiveCache()
|
|
60
|
+
|
|
61
|
+
# === PUBLIC API ===
|
|
62
|
+
|
|
63
|
+
def get_container_path(self, bundle_id: str) -> tuple[bool, str]:
|
|
64
|
+
"""Resolve the data container root for a bundle ID.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
bundle_id: App bundle identifier (e.g. com.example.app)
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
(success, path_or_error_message)
|
|
71
|
+
"""
|
|
72
|
+
device = self.udid or "booted"
|
|
73
|
+
try:
|
|
74
|
+
result = subprocess.run(
|
|
75
|
+
["xcrun", "simctl", "get_app_container", device, bundle_id, "data"],
|
|
76
|
+
check=False,
|
|
77
|
+
capture_output=True,
|
|
78
|
+
text=True,
|
|
79
|
+
timeout=15,
|
|
80
|
+
)
|
|
81
|
+
except subprocess.TimeoutExpired:
|
|
82
|
+
return False, "get_app_container timed out"
|
|
83
|
+
except Exception as e:
|
|
84
|
+
return False, f"get_app_container error: {e}"
|
|
85
|
+
|
|
86
|
+
if result.returncode != 0:
|
|
87
|
+
error = result.stderr.strip() or f"exit code {result.returncode}"
|
|
88
|
+
return False, f"Cannot find container for {bundle_id}: {error}"
|
|
89
|
+
|
|
90
|
+
container_path = result.stdout.strip()
|
|
91
|
+
if not container_path:
|
|
92
|
+
return False, f"No data container found for {bundle_id}"
|
|
93
|
+
|
|
94
|
+
return True, container_path
|
|
95
|
+
|
|
96
|
+
def list_files(
|
|
97
|
+
self,
|
|
98
|
+
bundle_id: str,
|
|
99
|
+
relative_path: str = "",
|
|
100
|
+
max_depth: int = 3,
|
|
101
|
+
) -> tuple[bool, dict]:
|
|
102
|
+
"""List files in the app's data container.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
bundle_id: App bundle identifier
|
|
106
|
+
relative_path: Sub-path within container (default: container root)
|
|
107
|
+
max_depth: Recursive listing depth (default: 3)
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
(success, result_dict) where result_dict contains 'path', 'entries'
|
|
111
|
+
"""
|
|
112
|
+
ok, container = self.get_container_path(bundle_id)
|
|
113
|
+
if not ok:
|
|
114
|
+
return False, {"error": container}
|
|
115
|
+
|
|
116
|
+
container_resolved = Path(container).resolve()
|
|
117
|
+
target = container_resolved
|
|
118
|
+
if relative_path:
|
|
119
|
+
target = (container_resolved / relative_path).resolve()
|
|
120
|
+
if not target.is_relative_to(container_resolved):
|
|
121
|
+
return False, {"error": f"Path escapes container boundary: {relative_path}"}
|
|
122
|
+
|
|
123
|
+
if not target.exists():
|
|
124
|
+
return False, {"error": f"Path does not exist: {target}"}
|
|
125
|
+
|
|
126
|
+
if target.is_file():
|
|
127
|
+
return True, {
|
|
128
|
+
"path": str(target),
|
|
129
|
+
"entries": [_describe_path(target, Path(container))],
|
|
130
|
+
"total_files": 1,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
entries = _walk_directory(target, Path(container), current_depth=0, max_depth=max_depth)
|
|
134
|
+
return True, {
|
|
135
|
+
"bundle_id": bundle_id,
|
|
136
|
+
"container_root": container,
|
|
137
|
+
"listed_path": str(target),
|
|
138
|
+
"max_depth": max_depth,
|
|
139
|
+
"entries": entries,
|
|
140
|
+
"total_entries": len(entries),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def cat_file(self, bundle_id: str, relative_path: str) -> tuple[bool, dict]:
|
|
144
|
+
"""Read a file from the container.
|
|
145
|
+
|
|
146
|
+
Large files (> 8 KB) are stored in ProgressiveCache and a cache_id is
|
|
147
|
+
returned instead of the raw content — matching the pattern used by
|
|
148
|
+
build_and_test.py and log_monitor.py.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
bundle_id: App bundle identifier
|
|
152
|
+
relative_path: Path relative to container root
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
(success, result_dict) with 'content' or 'cache_id' key
|
|
156
|
+
"""
|
|
157
|
+
ok, container = self.get_container_path(bundle_id)
|
|
158
|
+
if not ok:
|
|
159
|
+
return False, {"error": container}
|
|
160
|
+
|
|
161
|
+
container_resolved = Path(container).resolve()
|
|
162
|
+
file_path = (container_resolved / relative_path).resolve()
|
|
163
|
+
if not file_path.is_relative_to(container_resolved):
|
|
164
|
+
return False, {"error": f"Path escapes container boundary: {relative_path}"}
|
|
165
|
+
if not file_path.exists():
|
|
166
|
+
return False, {"error": f"File not found: {file_path}"}
|
|
167
|
+
|
|
168
|
+
if file_path.is_dir():
|
|
169
|
+
return False, {"error": f"Path is a directory — use --ls instead: {relative_path}"}
|
|
170
|
+
|
|
171
|
+
if file_path.is_symlink() and not file_path.resolve().exists():
|
|
172
|
+
return False, {"error": f"Broken symlink: {relative_path}"}
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
file_bytes = file_path.read_bytes()
|
|
176
|
+
except OSError as exc:
|
|
177
|
+
return False, {"error": f"Cannot read file: {exc}"}
|
|
178
|
+
|
|
179
|
+
size_bytes = len(file_bytes)
|
|
180
|
+
|
|
181
|
+
# Attempt plist decode first (handles both binary and XML plists)
|
|
182
|
+
plist_data, is_plist = _try_parse_plist(file_bytes)
|
|
183
|
+
|
|
184
|
+
if is_plist:
|
|
185
|
+
content = plist_data
|
|
186
|
+
content_type = "plist"
|
|
187
|
+
else:
|
|
188
|
+
# Try UTF-8 text; fall back to hex summary for binary blobs
|
|
189
|
+
try:
|
|
190
|
+
content = file_bytes.decode("utf-8")
|
|
191
|
+
content_type = "text"
|
|
192
|
+
except UnicodeDecodeError:
|
|
193
|
+
content = f"<binary file: {size_bytes} bytes — use --export to retrieve>"
|
|
194
|
+
content_type = "binary"
|
|
195
|
+
|
|
196
|
+
base_result = {
|
|
197
|
+
"bundle_id": bundle_id,
|
|
198
|
+
"path": relative_path,
|
|
199
|
+
"size_bytes": size_bytes,
|
|
200
|
+
"content_type": content_type,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Cache large text / plist content
|
|
204
|
+
if size_bytes > _CAT_CACHE_THRESHOLD_BYTES and content_type != "binary":
|
|
205
|
+
cache_data = {**base_result, "content": content}
|
|
206
|
+
cache_id = self._cache.save(cache_data, "container-cat")
|
|
207
|
+
return True, {
|
|
208
|
+
**base_result,
|
|
209
|
+
"cache_id": cache_id,
|
|
210
|
+
"note": (
|
|
211
|
+
f"File is {size_bytes:,} bytes — full content cached. "
|
|
212
|
+
f"Retrieve with cache_id '{cache_id}'."
|
|
213
|
+
),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return True, {**base_result, "content": content}
|
|
217
|
+
|
|
218
|
+
def read_userdefaults(self, bundle_id: str) -> tuple[bool, dict]:
|
|
219
|
+
"""Read UserDefaults plist for the app.
|
|
220
|
+
|
|
221
|
+
Looks in Library/Preferences/<bundle_id>.plist (both binary and XML
|
|
222
|
+
formats are handled via stdlib plistlib).
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
bundle_id: App bundle identifier
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
(success, result_dict) with 'preferences' key containing decoded data
|
|
229
|
+
"""
|
|
230
|
+
ok, container = self.get_container_path(bundle_id)
|
|
231
|
+
if not ok:
|
|
232
|
+
return False, {"error": container}
|
|
233
|
+
|
|
234
|
+
plist_path = Path(container) / "Library" / "Preferences" / f"{bundle_id}.plist"
|
|
235
|
+
|
|
236
|
+
if not plist_path.exists():
|
|
237
|
+
return False, {
|
|
238
|
+
"error": (
|
|
239
|
+
f"UserDefaults plist not found: {plist_path}. "
|
|
240
|
+
"The app may not have written any defaults yet."
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
raw_bytes = plist_path.read_bytes()
|
|
246
|
+
except OSError as exc:
|
|
247
|
+
return False, {"error": f"Cannot read plist: {exc}"}
|
|
248
|
+
|
|
249
|
+
plist_data, is_plist = _try_parse_plist(raw_bytes)
|
|
250
|
+
if not is_plist:
|
|
251
|
+
return False, {"error": f"File is not a valid plist: {plist_path}"}
|
|
252
|
+
|
|
253
|
+
return True, {
|
|
254
|
+
"bundle_id": bundle_id,
|
|
255
|
+
"plist_path": str(plist_path),
|
|
256
|
+
"preferences": plist_data,
|
|
257
|
+
"total_keys": len(plist_data) if isinstance(plist_data, dict) else None,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
def find_core_data_paths(self, bundle_id: str) -> tuple[bool, dict]:
|
|
261
|
+
"""Locate Core Data SQLite stores in the container.
|
|
262
|
+
|
|
263
|
+
Searches Library/Application Support/ and Documents/ for *.sqlite,
|
|
264
|
+
*.sqlite-wal, and *.sqlite-shm files. Paths are reported only — no
|
|
265
|
+
file opening occurs.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
bundle_id: App bundle identifier
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
(success, result_dict) with 'stores' list
|
|
272
|
+
"""
|
|
273
|
+
ok, container = self.get_container_path(bundle_id)
|
|
274
|
+
if not ok:
|
|
275
|
+
return False, {"error": container}
|
|
276
|
+
|
|
277
|
+
container_path = Path(container)
|
|
278
|
+
search_dirs = [
|
|
279
|
+
container_path / "Library" / "Application Support",
|
|
280
|
+
container_path / "Documents",
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
stores: list[dict] = []
|
|
284
|
+
sqlite_extensions = {".sqlite", ".sqlite-wal", ".sqlite-shm"}
|
|
285
|
+
|
|
286
|
+
for search_dir in search_dirs:
|
|
287
|
+
if not search_dir.exists():
|
|
288
|
+
continue
|
|
289
|
+
for file_path in search_dir.rglob("*"):
|
|
290
|
+
if file_path.suffix.lower() in sqlite_extensions and file_path.is_file():
|
|
291
|
+
relative = str(file_path.relative_to(container_path))
|
|
292
|
+
size_bytes = file_path.stat().st_size
|
|
293
|
+
stores.append(
|
|
294
|
+
{
|
|
295
|
+
"path": relative,
|
|
296
|
+
"absolute_path": str(file_path),
|
|
297
|
+
"size_bytes": size_bytes,
|
|
298
|
+
"type": _classify_sqlite_file(file_path.name),
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return True, {
|
|
303
|
+
"bundle_id": bundle_id,
|
|
304
|
+
"container_root": container,
|
|
305
|
+
"stores": stores,
|
|
306
|
+
"total_stores": len(stores),
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
def export_container(self, bundle_id: str, dest_dir: str) -> tuple[bool, dict]:
|
|
310
|
+
"""Copy the full data container to dest_dir.
|
|
311
|
+
|
|
312
|
+
Uses shutil.copytree with symlinks=True to preserve symlink structure.
|
|
313
|
+
Keychain is not included — it is not part of the data container path.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
bundle_id: App bundle identifier
|
|
317
|
+
dest_dir: Destination directory (will be created if absent)
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
(success, result_dict) with 'destination' and 'size_bytes'
|
|
321
|
+
"""
|
|
322
|
+
ok, container = self.get_container_path(bundle_id)
|
|
323
|
+
if not ok:
|
|
324
|
+
return False, {"error": container}
|
|
325
|
+
|
|
326
|
+
dest = Path(dest_dir)
|
|
327
|
+
target = dest / bundle_id
|
|
328
|
+
|
|
329
|
+
if target.exists():
|
|
330
|
+
return False, {
|
|
331
|
+
"error": (
|
|
332
|
+
f"Destination already exists: {target}. "
|
|
333
|
+
"Remove it or choose a different path."
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
shutil.copytree(
|
|
341
|
+
container,
|
|
342
|
+
str(target),
|
|
343
|
+
symlinks=True,
|
|
344
|
+
ignore_dangling_symlinks=True,
|
|
345
|
+
)
|
|
346
|
+
except shutil.Error as exc:
|
|
347
|
+
return False, {"error": f"Export failed: {exc}"}
|
|
348
|
+
except OSError as exc:
|
|
349
|
+
return False, {"error": f"Export OS error: {exc}"}
|
|
350
|
+
|
|
351
|
+
total_size = _directory_size_bytes(target)
|
|
352
|
+
|
|
353
|
+
return True, {
|
|
354
|
+
"bundle_id": bundle_id,
|
|
355
|
+
"source": container,
|
|
356
|
+
"destination": str(target),
|
|
357
|
+
"size_bytes": total_size,
|
|
358
|
+
"size_human": _human_bytes(total_size),
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# === PRIVATE HELPERS ===
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _try_parse_plist(data: bytes) -> tuple[dict | list | None, bool]:
|
|
366
|
+
"""Attempt to parse bytes as a plist (binary or XML).
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
data: Raw file bytes
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
(parsed_data, is_plist) — parsed_data is None if not a valid plist
|
|
373
|
+
"""
|
|
374
|
+
try:
|
|
375
|
+
parsed = plistlib.loads(data)
|
|
376
|
+
return _make_json_serializable(parsed), True
|
|
377
|
+
except Exception:
|
|
378
|
+
return None, False
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _make_json_serializable(obj):
|
|
382
|
+
"""Recursively convert plist types to JSON-serializable equivalents.
|
|
383
|
+
|
|
384
|
+
Handles bytes (→ hex string), sets (→ list), and datetime (→ ISO string).
|
|
385
|
+
"""
|
|
386
|
+
if isinstance(obj, dict):
|
|
387
|
+
return {k: _make_json_serializable(v) for k, v in obj.items()}
|
|
388
|
+
if isinstance(obj, list):
|
|
389
|
+
return [_make_json_serializable(i) for i in obj]
|
|
390
|
+
if isinstance(obj, bytes):
|
|
391
|
+
return obj.hex()
|
|
392
|
+
if isinstance(obj, set):
|
|
393
|
+
return sorted(_make_json_serializable(i) for i in obj)
|
|
394
|
+
if isinstance(obj, Decimal):
|
|
395
|
+
return float(obj)
|
|
396
|
+
# datetime — plistlib returns datetime.datetime for plist dates
|
|
397
|
+
if hasattr(obj, "isoformat"):
|
|
398
|
+
return obj.isoformat()
|
|
399
|
+
return obj
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _describe_path(path: Path, container_root: Path) -> dict:
|
|
403
|
+
"""Build a metadata dict for a single filesystem path."""
|
|
404
|
+
try:
|
|
405
|
+
stat = path.stat()
|
|
406
|
+
size_bytes = stat.st_size if path.is_file() else None
|
|
407
|
+
except OSError:
|
|
408
|
+
size_bytes = None
|
|
409
|
+
|
|
410
|
+
relative = str(path.relative_to(container_root))
|
|
411
|
+
kind = "symlink" if path.is_symlink() else ("dir" if path.is_dir() else "file")
|
|
412
|
+
|
|
413
|
+
entry: dict = {"path": relative, "kind": kind}
|
|
414
|
+
if size_bytes is not None:
|
|
415
|
+
entry["size_bytes"] = size_bytes
|
|
416
|
+
if path.is_symlink():
|
|
417
|
+
try:
|
|
418
|
+
entry["symlink_target"] = str(path.readlink())
|
|
419
|
+
except OSError:
|
|
420
|
+
entry["symlink_target"] = "<unreadable>"
|
|
421
|
+
|
|
422
|
+
return entry
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _walk_directory(
|
|
426
|
+
directory: Path,
|
|
427
|
+
container_root: Path,
|
|
428
|
+
current_depth: int,
|
|
429
|
+
max_depth: int,
|
|
430
|
+
) -> list[dict]:
|
|
431
|
+
"""Recursively walk a directory up to max_depth, returning metadata entries."""
|
|
432
|
+
entries: list[dict] = []
|
|
433
|
+
try:
|
|
434
|
+
children = sorted(directory.iterdir())
|
|
435
|
+
except PermissionError:
|
|
436
|
+
return entries
|
|
437
|
+
|
|
438
|
+
for child in children:
|
|
439
|
+
entries.append(_describe_path(child, container_root))
|
|
440
|
+
if child.is_dir() and not child.is_symlink() and current_depth < max_depth:
|
|
441
|
+
entries.extend(_walk_directory(child, container_root, current_depth + 1, max_depth))
|
|
442
|
+
|
|
443
|
+
return entries
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _classify_sqlite_file(filename: str) -> str:
|
|
447
|
+
"""Return a human-readable type label for a SQLite-related file."""
|
|
448
|
+
lower = filename.lower()
|
|
449
|
+
if lower.endswith(".sqlite-wal"):
|
|
450
|
+
return "write-ahead-log"
|
|
451
|
+
if lower.endswith(".sqlite-shm"):
|
|
452
|
+
return "shared-memory"
|
|
453
|
+
return "database"
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _directory_size_bytes(path: Path) -> int:
|
|
457
|
+
"""Return the total size of all files under a directory."""
|
|
458
|
+
total = 0
|
|
459
|
+
for dirpath, _, filenames in os.walk(path, followlinks=False):
|
|
460
|
+
for fname in filenames:
|
|
461
|
+
fpath = Path(dirpath) / fname
|
|
462
|
+
with contextlib.suppress(OSError):
|
|
463
|
+
total += fpath.stat().st_size
|
|
464
|
+
return total
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _human_bytes(num: int) -> str:
|
|
468
|
+
"""Format byte count as a human-readable string (KB / MB / GB)."""
|
|
469
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
470
|
+
if num < 1024:
|
|
471
|
+
return f"{num:.1f} {unit}"
|
|
472
|
+
num /= 1024
|
|
473
|
+
return f"{num:.1f} TB"
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _format_userdefaults(preferences: dict, bundle_id: str, verbose: bool) -> str:
|
|
477
|
+
"""Format UserDefaults output as key=value lines."""
|
|
478
|
+
lines = [f"UserDefaults: {bundle_id}"]
|
|
479
|
+
if not preferences:
|
|
480
|
+
lines.append(" (no keys found)")
|
|
481
|
+
return "\n".join(lines)
|
|
482
|
+
|
|
483
|
+
for key, value in sorted(preferences.items()):
|
|
484
|
+
if isinstance(value, dict):
|
|
485
|
+
lines.append(f" {key} = <dict with {len(value)} keys>")
|
|
486
|
+
if verbose:
|
|
487
|
+
for sub_key, sub_value in sorted(value.items()):
|
|
488
|
+
lines.append(f" {sub_key} = {sub_value!r}")
|
|
489
|
+
elif isinstance(value, list):
|
|
490
|
+
lines.append(f" {key} = <list with {len(value)} items>")
|
|
491
|
+
if verbose:
|
|
492
|
+
for item in value:
|
|
493
|
+
lines.append(f" - {item!r}")
|
|
494
|
+
else:
|
|
495
|
+
lines.append(f" {key} = {value!r}")
|
|
496
|
+
|
|
497
|
+
return "\n".join(lines)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# === CLI ===
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def main():
|
|
504
|
+
"""Main entry point."""
|
|
505
|
+
parser = argparse.ArgumentParser(
|
|
506
|
+
description="App sandbox / UserDefaults / Core Data inspector for iOS simulators",
|
|
507
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
508
|
+
epilog="""
|
|
509
|
+
Examples:
|
|
510
|
+
python container.py --ls com.example.app
|
|
511
|
+
python container.py --ls com.example.app Library/Preferences
|
|
512
|
+
python container.py --cat com.example.app Library/Preferences/com.example.app.plist
|
|
513
|
+
python container.py --userdefaults com.example.app
|
|
514
|
+
python container.py --core-data-path com.example.app
|
|
515
|
+
python container.py --export com.example.app ./snapshot/
|
|
516
|
+
python container.py --ls com.example.app --udid <DEVICE_UDID> --json
|
|
517
|
+
""",
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
# Operation flags — mutually exclusive
|
|
521
|
+
ops = parser.add_mutually_exclusive_group(required=True)
|
|
522
|
+
ops.add_argument(
|
|
523
|
+
"--ls",
|
|
524
|
+
metavar="BUNDLE_ID",
|
|
525
|
+
help="List files in the data container. Optionally pass a sub-path as second argument.",
|
|
526
|
+
)
|
|
527
|
+
ops.add_argument(
|
|
528
|
+
"--cat",
|
|
529
|
+
nargs=2,
|
|
530
|
+
metavar=("BUNDLE_ID", "PATH"),
|
|
531
|
+
help="Print a file from the container. Large files are cached.",
|
|
532
|
+
)
|
|
533
|
+
ops.add_argument(
|
|
534
|
+
"--userdefaults",
|
|
535
|
+
metavar="BUNDLE_ID",
|
|
536
|
+
help="Read and display UserDefaults (Library/Preferences/<bundle>.plist).",
|
|
537
|
+
)
|
|
538
|
+
ops.add_argument(
|
|
539
|
+
"--core-data-path",
|
|
540
|
+
metavar="BUNDLE_ID",
|
|
541
|
+
help="List Core Data SQLite store paths inside the container.",
|
|
542
|
+
)
|
|
543
|
+
ops.add_argument(
|
|
544
|
+
"--export",
|
|
545
|
+
nargs=2,
|
|
546
|
+
metavar=("BUNDLE_ID", "DEST_DIR"),
|
|
547
|
+
help="Copy the full data container to DEST_DIR/BUNDLE_ID.",
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# Sub-path for --ls
|
|
551
|
+
parser.add_argument(
|
|
552
|
+
"path",
|
|
553
|
+
nargs="?",
|
|
554
|
+
default="",
|
|
555
|
+
help="Sub-path for --ls (optional)",
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
# Common flags
|
|
559
|
+
parser.add_argument("--udid", help="Simulator UDID (uses booted device if omitted)")
|
|
560
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
561
|
+
parser.add_argument("--verbose", action="store_true", help="Show extended detail")
|
|
562
|
+
parser.add_argument(
|
|
563
|
+
"--depth",
|
|
564
|
+
type=int,
|
|
565
|
+
default=3,
|
|
566
|
+
help="Recursive depth for --ls (default: 3)",
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
args = parser.parse_args()
|
|
570
|
+
|
|
571
|
+
# Resolve device
|
|
572
|
+
try:
|
|
573
|
+
udid = resolve_device_identifier(args.udid)
|
|
574
|
+
except RuntimeError as exc:
|
|
575
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
576
|
+
sys.exit(1)
|
|
577
|
+
|
|
578
|
+
inspector = ContainerInspector(udid=udid)
|
|
579
|
+
|
|
580
|
+
# === --ls ===
|
|
581
|
+
if args.ls:
|
|
582
|
+
success, result = inspector.list_files(
|
|
583
|
+
args.ls, relative_path=args.path, max_depth=args.depth
|
|
584
|
+
)
|
|
585
|
+
if args.json:
|
|
586
|
+
print(json.dumps({"action": "ls", "success": success, **result}, indent=2))
|
|
587
|
+
elif not success:
|
|
588
|
+
print(f"Error: {result.get('error', result)}", file=sys.stderr)
|
|
589
|
+
else:
|
|
590
|
+
print(f"Container: {result['container_root']}")
|
|
591
|
+
print(f"Listed: {result['listed_path']} ({result['total_entries']} entries)")
|
|
592
|
+
listing_root = args.path or ""
|
|
593
|
+
root_depth = listing_root.count("/") + 1 if listing_root else 0
|
|
594
|
+
for entry in result["entries"]:
|
|
595
|
+
relative_depth = entry["path"].count("/") - root_depth
|
|
596
|
+
prefix = " " * max(0, relative_depth)
|
|
597
|
+
size = f" [{_human_bytes(entry['size_bytes'])}]" if entry.get("size_bytes") else ""
|
|
598
|
+
print(f" {prefix}{entry['path']} ({entry['kind']}){size}")
|
|
599
|
+
sys.exit(0 if success else 1)
|
|
600
|
+
|
|
601
|
+
# === --cat ===
|
|
602
|
+
if args.cat:
|
|
603
|
+
bundle_id, rel_path = args.cat
|
|
604
|
+
success, result = inspector.cat_file(bundle_id, rel_path)
|
|
605
|
+
if args.json:
|
|
606
|
+
print(json.dumps({"action": "cat", "success": success, **result}, indent=2))
|
|
607
|
+
elif not success:
|
|
608
|
+
print(f"Error: {result.get('error', result)}", file=sys.stderr)
|
|
609
|
+
elif "cache_id" in result:
|
|
610
|
+
print(f"[{result['path']}] {_human_bytes(result['size_bytes'])} (cached)")
|
|
611
|
+
print(result["note"])
|
|
612
|
+
else:
|
|
613
|
+
content = result.get("content", "")
|
|
614
|
+
if isinstance(content, dict | list):
|
|
615
|
+
print(json.dumps(content, indent=2))
|
|
616
|
+
else:
|
|
617
|
+
print(content)
|
|
618
|
+
sys.exit(0 if success else 1)
|
|
619
|
+
|
|
620
|
+
# === --userdefaults ===
|
|
621
|
+
if args.userdefaults:
|
|
622
|
+
success, result = inspector.read_userdefaults(args.userdefaults)
|
|
623
|
+
if args.json:
|
|
624
|
+
print(json.dumps({"action": "userdefaults", "success": success, **result}, indent=2))
|
|
625
|
+
elif not success:
|
|
626
|
+
print(f"Error: {result.get('error', result)}", file=sys.stderr)
|
|
627
|
+
else:
|
|
628
|
+
prefs = result.get("preferences", {})
|
|
629
|
+
print(_format_userdefaults(prefs, args.userdefaults, verbose=args.verbose))
|
|
630
|
+
print(f"\n({result['total_keys']} keys • {result['plist_path']})")
|
|
631
|
+
sys.exit(0 if success else 1)
|
|
632
|
+
|
|
633
|
+
# === --core-data-path ===
|
|
634
|
+
if args.core_data_path:
|
|
635
|
+
success, result = inspector.find_core_data_paths(args.core_data_path)
|
|
636
|
+
if args.json:
|
|
637
|
+
print(json.dumps({"action": "core-data-path", "success": success, **result}, indent=2))
|
|
638
|
+
elif not success:
|
|
639
|
+
print(f"Error: {result.get('error', result)}", file=sys.stderr)
|
|
640
|
+
else:
|
|
641
|
+
stores = result.get("stores", [])
|
|
642
|
+
if not stores:
|
|
643
|
+
print(f"No Core Data stores found for {args.core_data_path}")
|
|
644
|
+
else:
|
|
645
|
+
print(f"Core Data stores for {args.core_data_path}:")
|
|
646
|
+
for store in stores:
|
|
647
|
+
size = _human_bytes(store["size_bytes"])
|
|
648
|
+
print(f" {store['path']} [{size}] ({store['type']})")
|
|
649
|
+
sys.exit(0 if success else 1)
|
|
650
|
+
|
|
651
|
+
# === --export ===
|
|
652
|
+
if args.export:
|
|
653
|
+
bundle_id, dest_dir = args.export
|
|
654
|
+
success, result = inspector.export_container(bundle_id, dest_dir)
|
|
655
|
+
if args.json:
|
|
656
|
+
print(json.dumps({"action": "export", "success": success, **result}, indent=2))
|
|
657
|
+
elif not success:
|
|
658
|
+
print(f"Error: {result.get('error', result)}", file=sys.stderr)
|
|
659
|
+
else:
|
|
660
|
+
print(
|
|
661
|
+
f"Exported: {result['bundle_id']} → {result['destination']} "
|
|
662
|
+
f"[{result['size_human']}]"
|
|
663
|
+
)
|
|
664
|
+
sys.exit(0 if success else 1)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
if __name__ == "__main__":
|
|
668
|
+
main()
|