arcgispro-cli 0.1.2__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.
@@ -0,0 +1,21 @@
1
+ # Visual Studio
2
+ .vs/
3
+ bin/
4
+ obj/
5
+ *.user
6
+ *.suo
7
+
8
+ # Python
9
+ __pycache__/
10
+ *.pyc
11
+ *.egg-info/
12
+ dist/
13
+ .venv/
14
+ venv/
15
+
16
+ # ArcGIS Pro
17
+ # *.esriAddinX # Commented out - we bundle the addin in the CLI package
18
+
19
+ # OS
20
+ .DS_Store
21
+ Thumbs.db
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: arcgispro-cli
3
+ Version: 0.1.2
4
+ Summary: CLI tool for inspecting ArcGIS Pro session exports
5
+ Author: mcveydb
6
+ License: MIT
7
+ Keywords: arcgis,cli,esri,gis
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: GIS
19
+ Requires-Python: >=3.9
20
+ Requires-Dist: click>=8.0
21
+ Requires-Dist: rich>=13.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
24
+ Requires-Dist: pytest>=7.0; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # ArcGIS Pro CLI
28
+
29
+ A command-line tool for inspecting and managing ArcGIS Pro session exports.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ cd cli
35
+ pip install -e .
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```bash
41
+ # View help
42
+ arcgispro --help
43
+
44
+ # Inspect exported context
45
+ arcgispro inspect
46
+
47
+ # Validate JSON exports
48
+ arcgispro dump
49
+
50
+ # Validate images
51
+ arcgispro images
52
+
53
+ # Assemble snapshot
54
+ arcgispro snapshot
55
+
56
+ # Clean up exports
57
+ arcgispro clean --all
58
+
59
+ # Select active project
60
+ arcgispro open
61
+ ```
62
+
63
+ ## Workflow
64
+
65
+ 1. **In ArcGIS Pro:** Click "Snapshot" button in the ArcGIS Pro CLI ribbon
66
+ 2. **In terminal:** Run `arcgispro inspect` to see what was exported
67
+ 3. **Use context:** Read JSON files or markdown summary for AI analysis
68
+
69
+ ## Folder Structure
70
+
71
+ The CLI reads from `.arcgispro/` folder created by the add-in:
72
+
73
+ ```
74
+ .arcgispro/
75
+ ├── meta.json # Export metadata
76
+ ├── active_project.txt # Path to active .aprx
77
+ ├── context/
78
+ │ ├── project.json
79
+ │ ├── maps.json
80
+ │ ├── layers.json
81
+ │ ├── tables.json
82
+ │ ├── connections.json
83
+ │ └── layouts.json
84
+ ├── snapshot/
85
+ │ ├── context.md
86
+ │ ├── CONTEXT_SKILL.md
87
+ │ └── AGENT_TOOL_SKILL.md
88
+ └── images/
89
+ ├── map_*.png
90
+ └── layout_*.png
91
+ ```
92
+
93
+ ## Requirements
94
+
95
+ - Python 3.9+
96
+ - click
97
+ - rich
@@ -0,0 +1,71 @@
1
+ # ArcGIS Pro CLI
2
+
3
+ A command-line tool for inspecting and managing ArcGIS Pro session exports.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ cd cli
9
+ pip install -e .
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```bash
15
+ # View help
16
+ arcgispro --help
17
+
18
+ # Inspect exported context
19
+ arcgispro inspect
20
+
21
+ # Validate JSON exports
22
+ arcgispro dump
23
+
24
+ # Validate images
25
+ arcgispro images
26
+
27
+ # Assemble snapshot
28
+ arcgispro snapshot
29
+
30
+ # Clean up exports
31
+ arcgispro clean --all
32
+
33
+ # Select active project
34
+ arcgispro open
35
+ ```
36
+
37
+ ## Workflow
38
+
39
+ 1. **In ArcGIS Pro:** Click "Snapshot" button in the ArcGIS Pro CLI ribbon
40
+ 2. **In terminal:** Run `arcgispro inspect` to see what was exported
41
+ 3. **Use context:** Read JSON files or markdown summary for AI analysis
42
+
43
+ ## Folder Structure
44
+
45
+ The CLI reads from `.arcgispro/` folder created by the add-in:
46
+
47
+ ```
48
+ .arcgispro/
49
+ ├── meta.json # Export metadata
50
+ ├── active_project.txt # Path to active .aprx
51
+ ├── context/
52
+ │ ├── project.json
53
+ │ ├── maps.json
54
+ │ ├── layers.json
55
+ │ ├── tables.json
56
+ │ ├── connections.json
57
+ │ └── layouts.json
58
+ ├── snapshot/
59
+ │ ├── context.md
60
+ │ ├── CONTEXT_SKILL.md
61
+ │ └── AGENT_TOOL_SKILL.md
62
+ └── images/
63
+ ├── map_*.png
64
+ └── layout_*.png
65
+ ```
66
+
67
+ ## Requirements
68
+
69
+ - Python 3.9+
70
+ - click
71
+ - rich
@@ -0,0 +1,3 @@
1
+ """ArcGIS Pro CLI - Inspect and manage ArcGIS Pro session exports."""
2
+
3
+ __version__ = "0.1.2"
@@ -0,0 +1,56 @@
1
+ """
2
+ ArcGIS Pro CLI - Main entry point
3
+
4
+ Commands:
5
+ arcgispro install - Install the ProExporter add-in
6
+ arcgispro uninstall - Show uninstall instructions
7
+ arcgispro inspect - Print human-readable summary of exports
8
+ arcgispro dump - Validate context JSON files
9
+ arcgispro images - Validate exported images
10
+ arcgispro snapshot - Assemble full snapshot
11
+ arcgispro clean - Remove generated files
12
+ arcgispro open - Select active project
13
+ """
14
+
15
+ import click
16
+ from rich.console import Console
17
+
18
+ from . import __version__
19
+ from .commands import inspect, dump, images, snapshot, clean, open_project, install
20
+
21
+ console = Console()
22
+
23
+
24
+ @click.group()
25
+ @click.version_option(version=__version__, prog_name="arcgispro")
26
+ @click.pass_context
27
+ def main(ctx):
28
+ """ArcGIS Pro CLI - Inspect and manage session exports.
29
+
30
+ This tool reads exports from the .arcgispro/ folder created by the
31
+ ProExporter add-in. Use it to validate exports, view summaries,
32
+ and assemble snapshots for AI agents.
33
+
34
+ \b
35
+ Quick start:
36
+ pip install arcgispro-cli
37
+ arcgispro install # Install add-in (one time)
38
+ # In ArcGIS Pro: Click "Snapshot" button
39
+ arcgispro inspect # View exported context
40
+ """
41
+ ctx.ensure_object(dict)
42
+
43
+
44
+ # Register commands
45
+ main.add_command(install.install_cmd, name="install")
46
+ main.add_command(install.uninstall_cmd, name="uninstall")
47
+ main.add_command(inspect.inspect_cmd, name="inspect")
48
+ main.add_command(dump.dump_cmd, name="dump")
49
+ main.add_command(images.images_cmd, name="images")
50
+ main.add_command(snapshot.snapshot_cmd, name="snapshot")
51
+ main.add_command(clean.clean_cmd, name="clean")
52
+ main.add_command(open_project.open_cmd, name="open")
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()
@@ -0,0 +1 @@
1
+ """Command implementations for arcgispro CLI."""
@@ -0,0 +1,119 @@
1
+ """clean command - Remove generated files."""
2
+
3
+ import click
4
+ import shutil
5
+ from rich.console import Console
6
+ from pathlib import Path
7
+
8
+ from ..paths import (
9
+ find_arcgispro_folder,
10
+ get_context_folder,
11
+ get_images_folder,
12
+ get_snapshot_folder,
13
+ )
14
+
15
+ console = Console()
16
+
17
+
18
+ @click.command("clean")
19
+ @click.option("--path", "-p", type=click.Path(exists=True), help="Path to search for .arcgispro folder")
20
+ @click.option("--images", "clean_images", is_flag=True, help="Remove images/ folder")
21
+ @click.option("--context", "clean_context", is_flag=True, help="Remove context/ folder")
22
+ @click.option("--snapshot", "clean_snapshot", is_flag=True, help="Remove snapshot/ folder")
23
+ @click.option("--all", "clean_all", is_flag=True, help="Remove everything in .arcgispro/")
24
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
25
+ def clean_cmd(path, clean_images, clean_context, clean_snapshot, clean_all, yes):
26
+ """Remove generated files from .arcgispro/ folder.
27
+
28
+ Use flags to specify what to remove:
29
+
30
+ \b
31
+ --images Remove images/ folder
32
+ --context Remove context/ folder
33
+ --snapshot Remove snapshot/ folder
34
+ --all Remove everything
35
+
36
+ By default, asks for confirmation before deleting.
37
+ """
38
+ start_path = Path(path) if path else None
39
+ arcgispro_path = find_arcgispro_folder(start_path)
40
+
41
+ if not arcgispro_path:
42
+ console.print("[red]✗[/red] No .arcgispro folder found")
43
+ raise SystemExit(1)
44
+
45
+ # If no flags specified, show help
46
+ if not any([clean_images, clean_context, clean_snapshot, clean_all]):
47
+ console.print("[yellow]No cleanup option specified.[/yellow]")
48
+ console.print()
49
+ console.print("Use one of:")
50
+ console.print(" --images Remove images/ folder")
51
+ console.print(" --context Remove context/ folder")
52
+ console.print(" --snapshot Remove snapshot/ folder")
53
+ console.print(" --all Remove everything in .arcgispro/")
54
+ raise SystemExit(1)
55
+
56
+ to_remove = []
57
+
58
+ if clean_all:
59
+ # Remove everything
60
+ for item in arcgispro_path.iterdir():
61
+ to_remove.append(item)
62
+ else:
63
+ if clean_images:
64
+ images_folder = get_images_folder(arcgispro_path)
65
+ if images_folder.exists():
66
+ to_remove.append(images_folder)
67
+
68
+ if clean_context:
69
+ context_folder = get_context_folder(arcgispro_path)
70
+ if context_folder.exists():
71
+ to_remove.append(context_folder)
72
+ # Also remove meta.json and active_project.txt
73
+ meta_file = arcgispro_path / "meta.json"
74
+ if meta_file.exists():
75
+ to_remove.append(meta_file)
76
+ active_file = arcgispro_path / "active_project.txt"
77
+ if active_file.exists():
78
+ to_remove.append(active_file)
79
+
80
+ if clean_snapshot:
81
+ snapshot_folder = get_snapshot_folder(arcgispro_path)
82
+ if snapshot_folder.exists():
83
+ to_remove.append(snapshot_folder)
84
+
85
+ if not to_remove:
86
+ console.print("[dim]Nothing to remove.[/dim]")
87
+ return
88
+
89
+ # Show what will be removed
90
+ console.print("[bold]Will remove:[/bold]")
91
+ for item in to_remove:
92
+ if item.is_dir():
93
+ count = sum(1 for _ in item.rglob("*") if _.is_file())
94
+ console.print(f" 📁 {item.name}/ ({count} files)")
95
+ else:
96
+ console.print(f" 📄 {item.name}")
97
+
98
+ # Confirm
99
+ if not yes:
100
+ console.print()
101
+ if not click.confirm("Proceed with deletion?"):
102
+ console.print("[dim]Cancelled.[/dim]")
103
+ return
104
+
105
+ # Delete
106
+ removed = 0
107
+ for item in to_remove:
108
+ try:
109
+ if item.is_dir():
110
+ shutil.rmtree(item)
111
+ else:
112
+ item.unlink()
113
+ removed += 1
114
+ console.print(f"[green]✓[/green] Removed {item.name}")
115
+ except Exception as e:
116
+ console.print(f"[red]✗[/red] Failed to remove {item.name}: {e}")
117
+
118
+ console.print()
119
+ console.print(f"[bold]Removed {removed} item(s)[/bold]")
@@ -0,0 +1,87 @@
1
+ """dump command - Validate context JSON files."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from pathlib import Path
6
+
7
+ from ..paths import find_arcgispro_folder, get_context_folder, load_json_file
8
+
9
+ console = Console()
10
+
11
+ EXPECTED_FILES = [
12
+ ("meta.json", False), # (filename, is_in_context_folder)
13
+ ("project.json", True),
14
+ ("maps.json", True),
15
+ ("layers.json", True),
16
+ ("tables.json", True),
17
+ ("connections.json", True),
18
+ ("layouts.json", True),
19
+ ]
20
+
21
+
22
+ @click.command("dump")
23
+ @click.option("--path", "-p", type=click.Path(exists=True), help="Path to search for .arcgispro folder")
24
+ @click.option("--verbose", "-v", is_flag=True, help="Show file contents")
25
+ def dump_cmd(path, verbose):
26
+ """Validate that context JSON files exist and are valid.
27
+
28
+ Checks for expected JSON files in the .arcgispro/context/ folder
29
+ and validates they contain valid JSON.
30
+
31
+ Exit code 0 if all files valid, 1 if any issues found.
32
+ """
33
+ start_path = Path(path) if path else None
34
+ arcgispro_path = find_arcgispro_folder(start_path)
35
+
36
+ if not arcgispro_path:
37
+ console.print("[red]✗[/red] No .arcgispro folder found")
38
+ raise SystemExit(1)
39
+
40
+ context_folder = get_context_folder(arcgispro_path)
41
+
42
+ console.print(f"[bold]Validating context files in:[/bold] {arcgispro_path}")
43
+ console.print()
44
+
45
+ all_valid = True
46
+ valid_count = 0
47
+
48
+ for filename, in_context in EXPECTED_FILES:
49
+ if in_context:
50
+ file_path = context_folder / filename
51
+ else:
52
+ file_path = arcgispro_path / filename
53
+
54
+ if not file_path.exists():
55
+ console.print(f"[yellow]⚠[/yellow] {filename}: [yellow]missing[/yellow]")
56
+ all_valid = False
57
+ continue
58
+
59
+ data = load_json_file(file_path)
60
+ if data is None:
61
+ console.print(f"[red]✗[/red] {filename}: [red]invalid JSON[/red]")
62
+ all_valid = False
63
+ continue
64
+
65
+ # Get some stats about the data
66
+ if isinstance(data, list):
67
+ info = f"{len(data)} items"
68
+ elif isinstance(data, dict):
69
+ info = f"{len(data)} keys"
70
+ else:
71
+ info = "valid"
72
+
73
+ console.print(f"[green]✓[/green] {filename}: {info}")
74
+ valid_count += 1
75
+
76
+ if verbose and isinstance(data, dict):
77
+ for key in list(data.keys())[:5]:
78
+ console.print(f" {key}: {type(data[key]).__name__}")
79
+
80
+ console.print()
81
+ console.print(f"[bold]Result:[/bold] {valid_count}/{len(EXPECTED_FILES)} files valid")
82
+
83
+ if not all_valid:
84
+ console.print("[yellow]Some files are missing or invalid. Re-run export from ArcGIS Pro.[/yellow]")
85
+ raise SystemExit(1)
86
+
87
+ console.print("[green]All context files valid![/green]")
@@ -0,0 +1,76 @@
1
+ """images command - Validate exported images."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from pathlib import Path
6
+
7
+ from ..paths import find_arcgispro_folder, list_image_files, get_images_folder
8
+
9
+ console = Console()
10
+
11
+
12
+ @click.command("images")
13
+ @click.option("--path", "-p", type=click.Path(exists=True), help="Path to search for .arcgispro folder")
14
+ def images_cmd(path):
15
+ """Validate that exported images exist.
16
+
17
+ Checks for PNG files in the .arcgispro/images/ folder.
18
+
19
+ Exit code 0 if images exist, 1 if none found.
20
+ """
21
+ start_path = Path(path) if path else None
22
+ arcgispro_path = find_arcgispro_folder(start_path)
23
+
24
+ if not arcgispro_path:
25
+ console.print("[red]✗[/red] No .arcgispro folder found")
26
+ raise SystemExit(1)
27
+
28
+ images_folder = get_images_folder(arcgispro_path)
29
+
30
+ console.print(f"[bold]Checking images in:[/bold] {images_folder}")
31
+ console.print()
32
+
33
+ if not images_folder.exists():
34
+ console.print("[yellow]⚠[/yellow] images/ folder does not exist")
35
+ console.print(" Run 'Export Images' or 'Snapshot' from ArcGIS Pro.")
36
+ raise SystemExit(1)
37
+
38
+ images = list_image_files(arcgispro_path)
39
+
40
+ if not images:
41
+ console.print("[yellow]⚠[/yellow] No PNG images found")
42
+ console.print(" Make sure a map view is active when exporting.")
43
+ raise SystemExit(1)
44
+
45
+ # Categorize images
46
+ map_images = [img for img in images if img.name.startswith("map_")]
47
+ layout_images = [img for img in images if img.name.startswith("layout_")]
48
+ other_images = [img for img in images if not img.name.startswith(("map_", "layout_"))]
49
+
50
+ console.print("[bold]Map images:[/bold]")
51
+ if map_images:
52
+ for img in map_images:
53
+ size_kb = img.stat().st_size / 1024
54
+ console.print(f" [green]✓[/green] {img.name} ({size_kb:.1f} KB)")
55
+ else:
56
+ console.print(" [dim]None[/dim]")
57
+
58
+ console.print()
59
+ console.print("[bold]Layout images:[/bold]")
60
+ if layout_images:
61
+ for img in layout_images:
62
+ size_kb = img.stat().st_size / 1024
63
+ console.print(f" [green]✓[/green] {img.name} ({size_kb:.1f} KB)")
64
+ else:
65
+ console.print(" [dim]None[/dim]")
66
+
67
+ if other_images:
68
+ console.print()
69
+ console.print("[bold]Other images:[/bold]")
70
+ for img in other_images:
71
+ size_kb = img.stat().st_size / 1024
72
+ console.print(f" [green]✓[/green] {img.name} ({size_kb:.1f} KB)")
73
+
74
+ console.print()
75
+ console.print(f"[bold]Total:[/bold] {len(images)} images found")
76
+ console.print("[green]Image validation passed![/green]")
@@ -0,0 +1,126 @@
1
+ """inspect command - Print human-readable summary of exports."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from rich.panel import Panel
7
+ from rich import box
8
+ from datetime import datetime
9
+
10
+ from ..paths import find_arcgispro_folder, load_context_files, list_image_files
11
+
12
+ console = Console()
13
+
14
+
15
+ @click.command("inspect")
16
+ @click.option("--path", "-p", type=click.Path(exists=True), help="Path to search for .arcgispro folder")
17
+ def inspect_cmd(path):
18
+ """Print a human-readable summary of the exported context.
19
+
20
+ Shows project info, maps, layers, and export metadata in a
21
+ formatted display.
22
+ """
23
+ from pathlib import Path
24
+
25
+ start_path = Path(path) if path else None
26
+ arcgispro_path = find_arcgispro_folder(start_path)
27
+
28
+ if not arcgispro_path:
29
+ console.print("[red]✗[/red] No .arcgispro folder found")
30
+ console.print(" Run the Snapshot export from ArcGIS Pro first.")
31
+ raise SystemExit(1)
32
+
33
+ context = load_context_files(arcgispro_path)
34
+ images = list_image_files(arcgispro_path)
35
+
36
+ # Header
37
+ console.print()
38
+ console.print(Panel.fit(
39
+ "[bold blue]ArcGIS Pro Session Context[/bold blue]",
40
+ border_style="blue"
41
+ ))
42
+
43
+ # Meta info
44
+ meta = context.get("meta")
45
+ if meta:
46
+ exported_at = meta.get("exportedAt", "Unknown")
47
+ if isinstance(exported_at, str) and "T" in exported_at:
48
+ try:
49
+ dt = datetime.fromisoformat(exported_at.replace("Z", "+00:00"))
50
+ exported_at = dt.strftime("%Y-%m-%d %H:%M:%S UTC")
51
+ except ValueError:
52
+ pass
53
+ console.print(f"[dim]Exported: {exported_at}[/dim]")
54
+ console.print(f"[dim]Location: {arcgispro_path}[/dim]")
55
+ console.print()
56
+
57
+ # Project info
58
+ project = context.get("project")
59
+ if project:
60
+ console.print("[bold]📁 Project[/bold]")
61
+ console.print(f" Name: [cyan]{project.get('name', 'Unknown')}[/cyan]")
62
+ if project.get("path"):
63
+ console.print(f" Path: {project.get('path')}")
64
+ map_count = len(project.get("mapNames", []))
65
+ layout_count = len(project.get("layoutNames", []))
66
+ console.print(f" Maps: {map_count} | Layouts: {layout_count}")
67
+ console.print()
68
+ else:
69
+ console.print("[yellow]⚠ No project info found[/yellow]")
70
+ console.print()
71
+
72
+ # Maps
73
+ maps = context.get("maps") or []
74
+ if maps:
75
+ console.print("[bold]🗺️ Maps[/bold]")
76
+ for m in maps:
77
+ active = " [green]★ Active[/green]" if m.get("isActiveMap") else ""
78
+ console.print(f" • {m.get('name', 'Unknown')}{active}")
79
+ console.print(f" Type: {m.get('mapType', '-')} | Layers: {m.get('layerCount', 0)} | Tables: {m.get('standaloneTableCount', 0)}")
80
+ if m.get("scale"):
81
+ console.print(f" Scale: 1:{m.get('scale'):,.0f}")
82
+ console.print()
83
+
84
+ # Layers summary
85
+ layers = context.get("layers") or []
86
+ if layers:
87
+ console.print("[bold]📊 Layers[/bold]")
88
+
89
+ table = Table(box=box.SIMPLE, show_header=True, header_style="bold")
90
+ table.add_column("Layer", style="cyan")
91
+ table.add_column("Type")
92
+ table.add_column("Geometry")
93
+ table.add_column("Features", justify="right")
94
+ table.add_column("Visible")
95
+
96
+ for layer in layers[:15]: # Limit to first 15
97
+ visible = "✓" if layer.get("isVisible") else "✗"
98
+ broken = " [red]⚠[/red]" if layer.get("isBroken") else ""
99
+ features = f"{layer.get('featureCount', '-'):,}" if layer.get('featureCount') else "-"
100
+
101
+ table.add_row(
102
+ f"{layer.get('name', 'Unknown')}{broken}",
103
+ layer.get("layerType", "-"),
104
+ layer.get("geometryType", "-"),
105
+ features,
106
+ visible
107
+ )
108
+
109
+ console.print(table)
110
+
111
+ if len(layers) > 15:
112
+ console.print(f" [dim]...and {len(layers) - 15} more layers[/dim]")
113
+ console.print()
114
+
115
+ # Images
116
+ if images:
117
+ console.print("[bold]🖼️ Images[/bold]")
118
+ for img in images:
119
+ console.print(f" • {img.name}")
120
+ console.print()
121
+
122
+ # Summary
123
+ console.print("[bold]Summary[/bold]")
124
+ console.print(f" Context files: {sum(1 for v in context.values() if v is not None)}/7")
125
+ console.print(f" Images: {len(images)}")
126
+ console.print()
@@ -0,0 +1,76 @@
1
+ """install command - Install the ArcGIS Pro add-in."""
2
+
3
+ import os
4
+ import shutil
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from rich.console import Console
10
+
11
+ console = Console()
12
+
13
+
14
+ def get_addin_path() -> Path:
15
+ """Get the path to the bundled .addin file."""
16
+ return Path(__file__).parent.parent / "addin" / "ProExporter.addin"
17
+
18
+
19
+ @click.command("install")
20
+ def install_cmd():
21
+ """Install the ProExporter add-in for ArcGIS Pro.
22
+
23
+ Extracts the bundled add-in and launches the installer.
24
+ You'll see the ArcGIS Pro Add-In Installation Utility dialog.
25
+ Click "Install Add-In" to complete installation.
26
+ """
27
+ addin_source = get_addin_path()
28
+
29
+ if not addin_source.exists():
30
+ console.print("[red]✗[/red] Add-in file not found in package.")
31
+ console.print(f" Expected: {addin_source}")
32
+ raise SystemExit(1)
33
+
34
+ # Copy to temp folder (some systems have issues launching from site-packages)
35
+ temp_dir = Path(tempfile.gettempdir()) / "arcgispro_cli"
36
+ temp_dir.mkdir(exist_ok=True)
37
+
38
+ addin_dest = temp_dir / "ProExporter.esriAddinX"
39
+ shutil.copy2(addin_source, addin_dest)
40
+
41
+ console.print("[bold]Installing ProExporter add-in...[/bold]")
42
+ console.print()
43
+ console.print(f" Add-in: {addin_dest}")
44
+ console.print()
45
+
46
+ # Launch the installer (Windows shell "open" action)
47
+ try:
48
+ os.startfile(str(addin_dest))
49
+ console.print("[green]✓[/green] Add-in installer launched!")
50
+ console.print()
51
+ console.print(" [dim]Click 'Install Add-In' in the dialog that appeared.[/dim]")
52
+ console.print(" [dim]Then restart ArcGIS Pro to use ProExporter.[/dim]")
53
+ except OSError as e:
54
+ console.print(f"[red]✗[/red] Failed to launch installer: {e}")
55
+ console.print()
56
+ console.print(" Try double-clicking the file manually:")
57
+ console.print(f" {addin_dest}")
58
+ raise SystemExit(1)
59
+
60
+
61
+ @click.command("uninstall")
62
+ def uninstall_cmd():
63
+ """Show instructions for uninstalling the add-in.
64
+
65
+ ArcGIS Pro add-ins must be removed through ArcGIS Pro settings.
66
+ """
67
+ console.print("[bold]Uninstalling ProExporter add-in[/bold]")
68
+ console.print()
69
+ console.print(" Add-ins are managed in ArcGIS Pro:")
70
+ console.print()
71
+ console.print(" 1. Open ArcGIS Pro")
72
+ console.print(" 2. Go to [cyan]Project → Add-In Manager[/cyan]")
73
+ console.print(" 3. Find [cyan]ProExporter[/cyan] in the list")
74
+ console.print(" 4. Click [cyan]Delete this Add-In[/cyan]")
75
+ console.print(" 5. Restart ArcGIS Pro")
76
+ console.print()
@@ -0,0 +1,99 @@
1
+ """open command - Select active project."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from pathlib import Path
6
+
7
+ from ..paths import find_arcgispro_folder, find_aprx_files
8
+
9
+ console = Console()
10
+
11
+
12
+ @click.command("open")
13
+ @click.option("--path", "-p", type=click.Path(exists=True), help="Path to search for .aprx files")
14
+ @click.argument("project", required=False)
15
+ def open_cmd(path, project):
16
+ """Select the active ArcGIS Pro project.
17
+
18
+ If PROJECT is not specified, searches for .aprx files in the
19
+ current directory and lets you choose.
20
+
21
+ The selected project path is written to .arcgispro/active_project.txt
22
+
23
+ \b
24
+ Examples:
25
+ arcgispro open # Search and select
26
+ arcgispro open MyProject.aprx # Select specific project
27
+ """
28
+ search_path = Path(path) if path else Path.cwd()
29
+
30
+ # If project specified directly, use it
31
+ if project:
32
+ project_path = Path(project)
33
+ if not project_path.exists():
34
+ # Try relative to search path
35
+ project_path = search_path / project
36
+
37
+ if not project_path.exists():
38
+ console.print(f"[red]✗[/red] Project not found: {project}")
39
+ raise SystemExit(1)
40
+
41
+ if not project_path.suffix.lower() == ".aprx":
42
+ console.print(f"[red]✗[/red] Not an ArcGIS Pro project file: {project}")
43
+ raise SystemExit(1)
44
+
45
+ _save_active_project(search_path, project_path)
46
+ return
47
+
48
+ # Search for .aprx files
49
+ console.print(f"[bold]Searching for .aprx files in:[/bold] {search_path}")
50
+ console.print()
51
+
52
+ aprx_files = find_aprx_files(search_path)
53
+
54
+ if not aprx_files:
55
+ console.print("[yellow]No .aprx files found[/yellow]")
56
+ console.print(" Navigate to a folder containing an ArcGIS Pro project.")
57
+ raise SystemExit(1)
58
+
59
+ # Show found projects
60
+ console.print(f"[bold]Found {len(aprx_files)} project(s):[/bold]")
61
+ console.print()
62
+
63
+ for i, aprx in enumerate(aprx_files, 1):
64
+ relative = aprx.relative_to(search_path) if aprx.is_relative_to(search_path) else aprx
65
+ console.print(f" {i}. {relative}")
66
+
67
+ console.print()
68
+
69
+ # Let user choose
70
+ if len(aprx_files) == 1:
71
+ choice = 1
72
+ console.print(f"[dim]Auto-selecting the only project found.[/dim]")
73
+ else:
74
+ choice = click.prompt(
75
+ "Select project",
76
+ type=click.IntRange(1, len(aprx_files)),
77
+ default=1
78
+ )
79
+
80
+ selected = aprx_files[choice - 1]
81
+ _save_active_project(search_path, selected)
82
+
83
+
84
+ def _save_active_project(base_path: Path, project_path: Path):
85
+ """Save the selected project to .arcgispro/active_project.txt"""
86
+
87
+ # Create .arcgispro folder if needed
88
+ arcgispro_folder = base_path / ".arcgispro"
89
+ arcgispro_folder.mkdir(exist_ok=True)
90
+
91
+ # Write active project
92
+ active_file = arcgispro_folder / "active_project.txt"
93
+ active_file.write_text(str(project_path.resolve()), encoding="utf-8")
94
+
95
+ console.print()
96
+ console.print(f"[green]✓[/green] Active project set to: [cyan]{project_path.name}[/cyan]")
97
+ console.print(f" Path: {project_path.resolve()}")
98
+ console.print()
99
+ console.print("[dim]Open this project in ArcGIS Pro and run Snapshot to export context.[/dim]")
@@ -0,0 +1,124 @@
1
+ """snapshot command - Assemble full snapshot."""
2
+
3
+ import click
4
+ import shutil
5
+ from rich.console import Console
6
+ from pathlib import Path
7
+
8
+ from ..paths import (
9
+ find_arcgispro_folder,
10
+ load_context_files,
11
+ list_image_files,
12
+ get_snapshot_folder,
13
+ get_images_folder,
14
+ )
15
+
16
+ console = Console()
17
+
18
+
19
+ @click.command("snapshot")
20
+ @click.option("--path", "-p", type=click.Path(exists=True), help="Path to search for .arcgispro folder")
21
+ @click.option("--force", "-f", is_flag=True, help="Overwrite existing snapshot")
22
+ def snapshot_cmd(path, force):
23
+ """Assemble a complete snapshot from context and images.
24
+
25
+ Verifies that context JSON and images exist, then assembles
26
+ everything into the snapshot/ folder for AI consumption.
27
+
28
+ The snapshot includes:
29
+ - context.md: Human-readable summary
30
+ - CONTEXT_SKILL.md: How to use the exports
31
+ - AGENT_TOOL_SKILL.md: CLI usage guide
32
+ - images/: Copy of exported images
33
+ """
34
+ start_path = Path(path) if path else None
35
+ arcgispro_path = find_arcgispro_folder(start_path)
36
+
37
+ if not arcgispro_path:
38
+ console.print("[red]✗[/red] No .arcgispro folder found")
39
+ console.print(" Run the Snapshot export from ArcGIS Pro first.")
40
+ raise SystemExit(1)
41
+
42
+ console.print(f"[bold]Assembling snapshot from:[/bold] {arcgispro_path}")
43
+ console.print()
44
+
45
+ # Verify context exists
46
+ context = load_context_files(arcgispro_path)
47
+ missing_context = [k for k, v in context.items() if v is None]
48
+
49
+ if missing_context:
50
+ console.print(f"[yellow]⚠[/yellow] Missing context files: {', '.join(missing_context)}")
51
+ console.print(" Run 'Dump Context' or 'Snapshot' from ArcGIS Pro.")
52
+
53
+ # Check for images
54
+ images = list_image_files(arcgispro_path)
55
+ if not images:
56
+ console.print("[yellow]⚠[/yellow] No images found")
57
+ console.print(" Run 'Export Images' or 'Snapshot' from ArcGIS Pro.")
58
+
59
+ # Check if snapshot already exists
60
+ snapshot_folder = get_snapshot_folder(arcgispro_path)
61
+
62
+ if snapshot_folder.exists():
63
+ existing_files = list(snapshot_folder.iterdir())
64
+ if existing_files and not force:
65
+ console.print(f"[yellow]⚠[/yellow] Snapshot folder already contains {len(existing_files)} items")
66
+ console.print(" Use --force to overwrite, or delete snapshot/ manually.")
67
+ raise SystemExit(1)
68
+
69
+ # Check if the add-in already created the snapshot files
70
+ context_md = snapshot_folder / "context.md"
71
+ context_skill = snapshot_folder / "CONTEXT_SKILL.md"
72
+ agent_skill = snapshot_folder / "AGENT_TOOL_SKILL.md"
73
+
74
+ files_created = 0
75
+
76
+ if context_md.exists():
77
+ console.print(f"[green]✓[/green] context.md exists")
78
+ files_created += 1
79
+ else:
80
+ console.print(f"[yellow]⚠[/yellow] context.md missing (should be created by add-in)")
81
+
82
+ if context_skill.exists():
83
+ console.print(f"[green]✓[/green] CONTEXT_SKILL.md exists")
84
+ files_created += 1
85
+ else:
86
+ console.print(f"[yellow]⚠[/yellow] CONTEXT_SKILL.md missing")
87
+
88
+ if agent_skill.exists():
89
+ console.print(f"[green]✓[/green] AGENT_TOOL_SKILL.md exists")
90
+ files_created += 1
91
+ else:
92
+ console.print(f"[yellow]⚠[/yellow] AGENT_TOOL_SKILL.md missing")
93
+
94
+ # Copy images to snapshot folder
95
+ snapshot_images_folder = snapshot_folder / "images"
96
+
97
+ if images:
98
+ snapshot_images_folder.mkdir(parents=True, exist_ok=True)
99
+
100
+ copied = 0
101
+ for img in images:
102
+ dest = snapshot_images_folder / img.name
103
+ if not dest.exists() or force:
104
+ shutil.copy2(img, dest)
105
+ copied += 1
106
+
107
+ console.print(f"[green]✓[/green] Copied {copied} images to snapshot/images/")
108
+
109
+ console.print()
110
+
111
+ # Summary
112
+ if files_created >= 2 and images:
113
+ console.print("[green]✓ Snapshot is ready![/green]")
114
+ console.print()
115
+ console.print("[bold]Contents:[/bold]")
116
+ console.print(f" {snapshot_folder}/")
117
+ console.print(f" context.md - Human-readable summary")
118
+ console.print(f" CONTEXT_SKILL.md - How to use exports")
119
+ console.print(f" AGENT_TOOL_SKILL.md - CLI usage")
120
+ console.print(f" images/ - {len(images)} PNG files")
121
+ else:
122
+ console.print("[yellow]⚠ Snapshot incomplete[/yellow]")
123
+ console.print(" Run 'Snapshot' from ArcGIS Pro to generate all files.")
124
+ raise SystemExit(1)
@@ -0,0 +1,161 @@
1
+ """Utility functions for finding and validating .arcgispro folders."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Optional, Dict, Any, List
6
+
7
+
8
+ def find_arcgispro_folder(start_path: Optional[Path] = None) -> Optional[Path]:
9
+ """
10
+ Find the .arcgispro folder by searching current directory and ancestors.
11
+
12
+ Args:
13
+ start_path: Starting directory to search from. Defaults to cwd.
14
+
15
+ Returns:
16
+ Path to .arcgispro folder, or None if not found.
17
+ """
18
+ if start_path is None:
19
+ start_path = Path.cwd()
20
+
21
+ current = start_path.resolve()
22
+
23
+ # Search current directory and ancestors
24
+ while current != current.parent:
25
+ candidate = current / ".arcgispro"
26
+ if candidate.is_dir():
27
+ return candidate
28
+ current = current.parent
29
+
30
+ # Check root as well
31
+ candidate = current / ".arcgispro"
32
+ if candidate.is_dir():
33
+ return candidate
34
+
35
+ return None
36
+
37
+
38
+ def get_context_folder(arcgispro_path: Path) -> Path:
39
+ """Get the context subfolder path."""
40
+ return arcgispro_path / "context"
41
+
42
+
43
+ def get_images_folder(arcgispro_path: Path) -> Path:
44
+ """Get the images subfolder path."""
45
+ return arcgispro_path / "images"
46
+
47
+
48
+ def get_snapshot_folder(arcgispro_path: Path) -> Path:
49
+ """Get the snapshot subfolder path."""
50
+ return arcgispro_path / "snapshot"
51
+
52
+
53
+ def load_json_file(path: Path) -> Optional[Dict[str, Any]]:
54
+ """
55
+ Load and parse a JSON file.
56
+
57
+ Args:
58
+ path: Path to JSON file
59
+
60
+ Returns:
61
+ Parsed JSON as dict, or None if file doesn't exist or is invalid.
62
+ """
63
+ if not path.exists():
64
+ return None
65
+
66
+ try:
67
+ with open(path, "r", encoding="utf-8-sig") as f:
68
+ return json.load(f)
69
+ except (json.JSONDecodeError, IOError):
70
+ return None
71
+
72
+
73
+ def load_context_files(arcgispro_path: Path) -> Dict[str, Any]:
74
+ """
75
+ Load all context JSON files.
76
+
77
+ Args:
78
+ arcgispro_path: Path to .arcgispro folder
79
+
80
+ Returns:
81
+ Dict with keys: meta, project, maps, layers, tables, connections, layouts
82
+ Values are the parsed JSON or None if missing/invalid.
83
+ """
84
+ context_dir = get_context_folder(arcgispro_path)
85
+
86
+ return {
87
+ "meta": load_json_file(arcgispro_path / "meta.json"),
88
+ "project": load_json_file(context_dir / "project.json"),
89
+ "maps": load_json_file(context_dir / "maps.json"),
90
+ "layers": load_json_file(context_dir / "layers.json"),
91
+ "tables": load_json_file(context_dir / "tables.json"),
92
+ "connections": load_json_file(context_dir / "connections.json"),
93
+ "layouts": load_json_file(context_dir / "layouts.json"),
94
+ }
95
+
96
+
97
+ def list_image_files(arcgispro_path: Path) -> List[Path]:
98
+ """
99
+ List all PNG files in the images folder.
100
+
101
+ Args:
102
+ arcgispro_path: Path to .arcgispro folder
103
+
104
+ Returns:
105
+ List of paths to PNG files.
106
+ """
107
+ images_dir = get_images_folder(arcgispro_path)
108
+ if not images_dir.exists():
109
+ return []
110
+
111
+ return list(images_dir.glob("*.png"))
112
+
113
+
114
+ def get_active_project(arcgispro_path: Path) -> Optional[str]:
115
+ """
116
+ Read the active project path from active_project.txt.
117
+
118
+ Args:
119
+ arcgispro_path: Path to .arcgispro folder
120
+
121
+ Returns:
122
+ Active project path string, or None if not set.
123
+ """
124
+ active_file = arcgispro_path / "active_project.txt"
125
+ if not active_file.exists():
126
+ return None
127
+
128
+ try:
129
+ return active_file.read_text(encoding="utf-8").strip()
130
+ except IOError:
131
+ return None
132
+
133
+
134
+ def find_aprx_files(directory: Path, max_depth: int = 2) -> List[Path]:
135
+ """
136
+ Find .aprx files in directory and subdirectories.
137
+
138
+ Args:
139
+ directory: Starting directory
140
+ max_depth: Maximum depth to search
141
+
142
+ Returns:
143
+ List of paths to .aprx files.
144
+ """
145
+ aprx_files = []
146
+
147
+ def search(path: Path, depth: int):
148
+ if depth > max_depth:
149
+ return
150
+
151
+ try:
152
+ for item in path.iterdir():
153
+ if item.is_file() and item.suffix.lower() == ".aprx":
154
+ aprx_files.append(item)
155
+ elif item.is_dir() and not item.name.startswith("."):
156
+ search(item, depth + 1)
157
+ except PermissionError:
158
+ pass
159
+
160
+ search(directory, 0)
161
+ return aprx_files
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "arcgispro-cli"
3
+ version = "0.1.2"
4
+ description = "CLI tool for inspecting ArcGIS Pro session exports"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = {text = "MIT"}
8
+ authors = [
9
+ {name = "mcveydb"}
10
+ ]
11
+ keywords = ["arcgis", "gis", "cli", "esri"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: Microsoft :: Windows",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Scientific/Engineering :: GIS",
24
+ ]
25
+
26
+ dependencies = [
27
+ "click>=8.0",
28
+ "rich>=13.0",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest>=7.0",
34
+ "pytest-cov>=4.0",
35
+ ]
36
+
37
+ [project.scripts]
38
+ arcgispro = "arcgispro_cli.cli:main"
39
+
40
+ [build-system]
41
+ requires = ["hatchling"]
42
+ build-backend = "hatchling.build"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["arcgispro_cli"]
46
+
47
+ [tool.hatch.build.targets.sdist]
48
+ include = [
49
+ "arcgispro_cli/**/*",
50
+ ]