mirror of
https://github.com/HKUDS/CLI-Anything.git
synced 2026-05-06 22:26:35 +08:00
fix: address Copilot review comments + update CloudCompare README icon
Code fixes (cc_backend.py): - restrict component glob to input stem to avoid picking up leftover files - move generated components to output_dir via shutil.move - coerce RANDOM subsample parameter to int Code fixes (export.py): - move overwrite check after preset path resolution in export_cloud/export_mesh Code fixes (project.py): - guard fcntl import with try/except for Windows compatibility Code fixes (cloudcompare_cli.py): - remove non-existent 'project open' from REPL help - fix session update after 'project new' and explicit -p/--project usage Docs (SKILL.md): - fix Click root option placement in example - remove non-existent redo reference Docs (README.md): - replace cloud emoji with 🅲🅲 icon - update test totals to 2,005 Config (registry.json, setup.py): - align registry version to 1.0.0 - use Path.read_text(encoding='utf-8') in setup.py 88/88 tests passing (49 unit + 39 E2E)
This commit is contained in:
@@ -758,7 +758,7 @@ Each application received complete, production-ready CLI interfaces — not demo
|
||||
<td align="center">✅ 19</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><strong>☁️ CloudCompare</strong></td>
|
||||
<td align="center"><strong>🅲🅲 CloudCompare</strong></td>
|
||||
<td>3D Point Cloud & Mesh</td>
|
||||
<td><code>cli-anything-cloudcompare</code></td>
|
||||
<td>CloudCompare CLI (headless)</td>
|
||||
@@ -766,11 +766,11 @@ Each application received complete, production-ready CLI interfaces — not demo
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" colspan="4"><strong>Total</strong></td>
|
||||
<td align="center"><strong>✅ 1,946</strong></td>
|
||||
<td align="center"><strong>✅ 2,005</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
> **100% pass rate** across all 1,946 tests — 1,404 unit tests + 523 end-to-end tests + 19 Node.js tests.
|
||||
> **100% pass rate** across all 2,005 tests — 1,453 unit tests + 533 end-to-end tests + 19 Node.js tests.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -173,7 +173,6 @@ def repl(ctx: click.Context, project: Optional[str]) -> None:
|
||||
# Build command reference
|
||||
commands = {
|
||||
"project new -o <file>": "Create a new project",
|
||||
"project open -p <file>": "Open an existing project",
|
||||
"project info": "Show project status",
|
||||
"cloud add <file>": "Add a cloud to the project",
|
||||
"cloud list": "List loaded clouds",
|
||||
@@ -228,10 +227,21 @@ def repl(ctx: click.Context, project: Optional[str]) -> None:
|
||||
standalone_mode=False,
|
||||
obj={"project": session.project_path if session else None, "json": json_mode},
|
||||
)
|
||||
# Update session reference if project was created/opened
|
||||
if session is None and ctx.obj and ctx.obj.get("project"):
|
||||
project_path = ctx.obj["project"]
|
||||
session = Session(project_path)
|
||||
# Update session when project new or explicit -p/--project is used
|
||||
new_path: Optional[str] = None
|
||||
raw = line.split()
|
||||
if len(raw) >= 2 and raw[0] == "project" and raw[1] == "new":
|
||||
for i, tok in enumerate(raw):
|
||||
if tok in ("-o", "--output") and i + 1 < len(raw):
|
||||
new_path = raw[i + 1]
|
||||
break
|
||||
elif "--project" in raw or "-p" in raw:
|
||||
for i, tok in enumerate(raw):
|
||||
if tok in ("--project", "-p") and i + 1 < len(raw):
|
||||
new_path = raw[i + 1]
|
||||
break
|
||||
if new_path:
|
||||
session = Session(new_path)
|
||||
except SystemExit:
|
||||
pass
|
||||
except click.UsageError as e:
|
||||
|
||||
@@ -76,12 +76,8 @@ def export_cloud(
|
||||
if not os.path.exists(input_path):
|
||||
raise FileNotFoundError(f"Input file not found: {input_path}")
|
||||
|
||||
if os.path.exists(output_path) and not overwrite:
|
||||
raise FileExistsError(
|
||||
f"Output file already exists: {output_path}. Use overwrite=True."
|
||||
)
|
||||
|
||||
# Determine format from preset or extension
|
||||
# Determine format from preset or extension (must happen before overwrite check
|
||||
# because a preset can change the output file extension).
|
||||
if preset:
|
||||
preset = preset.lower()
|
||||
if preset not in CLOUD_PRESETS:
|
||||
@@ -97,6 +93,11 @@ def export_cloud(
|
||||
fmt = CLOUD_FORMATS.get(out_ext, "ASC")
|
||||
ext = out_ext
|
||||
|
||||
if os.path.exists(output_path) and not overwrite:
|
||||
raise FileExistsError(
|
||||
f"Output file already exists: {output_path}. Use overwrite=True."
|
||||
)
|
||||
|
||||
result = open_and_save(input_path, output_path, extra_args)
|
||||
|
||||
if result["returncode"] != 0:
|
||||
@@ -143,11 +144,7 @@ def export_mesh(
|
||||
if not os.path.exists(input_path):
|
||||
raise FileNotFoundError(f"Input file not found: {input_path}")
|
||||
|
||||
if os.path.exists(output_path) and not overwrite:
|
||||
raise FileExistsError(
|
||||
f"Output file already exists: {output_path}. Use overwrite=True."
|
||||
)
|
||||
|
||||
# Resolve preset before overwrite check — preset can change the output extension.
|
||||
if preset:
|
||||
preset = preset.lower()
|
||||
if preset not in MESH_PRESETS:
|
||||
@@ -162,6 +159,11 @@ def export_mesh(
|
||||
fmt = MESH_FORMATS.get(out_ext, "OBJ")
|
||||
ext = out_ext
|
||||
|
||||
if os.path.exists(output_path) and not overwrite:
|
||||
raise FileExistsError(
|
||||
f"Output file already exists: {output_path}. Use overwrite=True."
|
||||
)
|
||||
|
||||
args = [
|
||||
"-O", input_path,
|
||||
"-M_EXPORT_FMT", fmt,
|
||||
|
||||
@@ -7,13 +7,18 @@ A 'project' is a JSON file tracking:
|
||||
- Operation history for undo/redo
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
try:
|
||||
import fcntl as _fcntl
|
||||
_HAS_FCNTL = True
|
||||
except ImportError:
|
||||
_HAS_FCNTL = False
|
||||
|
||||
|
||||
# ── JSON locking helper ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -28,10 +33,11 @@ def _locked_save_json(path: str, data: dict, **dump_kwargs) -> None:
|
||||
with f:
|
||||
_locked = False
|
||||
try:
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
||||
_locked = True
|
||||
except (ImportError, OSError):
|
||||
pass # Windows / unsupported FS — proceed unlocked
|
||||
if _HAS_FCNTL:
|
||||
_fcntl.flock(f.fileno(), _fcntl.LOCK_EX)
|
||||
_locked = True
|
||||
except OSError:
|
||||
pass # unsupported FS — proceed unlocked
|
||||
try:
|
||||
f.seek(0)
|
||||
f.truncate()
|
||||
@@ -39,7 +45,7 @@ def _locked_save_json(path: str, data: dict, **dump_kwargs) -> None:
|
||||
f.flush()
|
||||
finally:
|
||||
if _locked:
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||||
_fcntl.flock(f.fileno(), _fcntl.LOCK_UN)
|
||||
|
||||
|
||||
# ── Project data model ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -34,7 +34,7 @@ cli-anything-cloudcompare
|
||||
cli-anything-cloudcompare project new -o project.json
|
||||
|
||||
# Run with JSON output (for agent consumption)
|
||||
cli-anything-cloudcompare --json project info -p project.json
|
||||
cli-anything-cloudcompare --project project.json --json project info
|
||||
```
|
||||
|
||||
## Examples
|
||||
@@ -51,13 +51,13 @@ cli-anything-cloudcompare --json project new -o myproject.json
|
||||
|
||||
### Interactive REPL Session
|
||||
|
||||
Start an interactive session with undo/redo support.
|
||||
Start an interactive session with undo support.
|
||||
|
||||
```bash
|
||||
cli-anything-cloudcompare
|
||||
# Enter commands interactively
|
||||
# Use 'help' to see available commands
|
||||
# Use 'undo' and 'redo' for history navigation
|
||||
# Use 'session undo' to revert the last operation
|
||||
```
|
||||
|
||||
## For AI Agents
|
||||
|
||||
@@ -170,6 +170,13 @@ def subsample(
|
||||
|
||||
if method == "OCTREE":
|
||||
param_str = str(int(parameter))
|
||||
elif method == "RANDOM":
|
||||
count = int(parameter)
|
||||
if count <= 0:
|
||||
raise ValueError(
|
||||
f"RANDOM subsampling parameter must be a positive integer point count, got {parameter!r}"
|
||||
)
|
||||
param_str = str(count)
|
||||
else:
|
||||
param_str = str(parameter)
|
||||
|
||||
@@ -891,7 +898,9 @@ def extract_connected_components(
|
||||
returncode.
|
||||
"""
|
||||
input_path = os.path.abspath(input_path)
|
||||
output_dir = os.path.abspath(output_dir)
|
||||
in_dir = os.path.dirname(input_path)
|
||||
input_stem = os.path.splitext(os.path.basename(input_path))[0]
|
||||
fmt = CLOUD_FORMATS.get(output_fmt.lower(), "ASC")
|
||||
|
||||
args = [
|
||||
@@ -914,10 +923,22 @@ def extract_connected_components(
|
||||
|
||||
result = run_cloudcompare(args, cwd=in_dir)
|
||||
|
||||
# Restrict matching to the current input stem to avoid picking up leftovers.
|
||||
components = sorted(
|
||||
glob.glob(os.path.join(in_dir, f"*_COMPONENT_*.{actual_ext}"))
|
||||
glob.glob(os.path.join(in_dir, f"{input_stem}_COMPONENT_*.{actual_ext}"))
|
||||
)
|
||||
result["output_dir"] = in_dir
|
||||
|
||||
# Move components to output_dir if it differs from in_dir.
|
||||
if os.path.abspath(output_dir) != os.path.abspath(in_dir):
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
moved = []
|
||||
for c in components:
|
||||
dest = os.path.join(output_dir, os.path.basename(c))
|
||||
shutil.move(c, dest)
|
||||
moved.append(dest)
|
||||
components = sorted(moved)
|
||||
|
||||
result["output_dir"] = output_dir
|
||||
result["components"] = components
|
||||
result["component_count"] = len(components)
|
||||
return result
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
from pathlib import Path
|
||||
from setuptools import setup, find_namespace_packages
|
||||
|
||||
_readme = Path("cli_anything/cloudcompare/README.md")
|
||||
_long_description = _readme.read_text(encoding="utf-8") if _readme.is_file() else ""
|
||||
|
||||
setup(
|
||||
name="cli-anything-cloudcompare",
|
||||
version="1.0.0",
|
||||
description="Agent-friendly CLI harness for CloudCompare 3D point cloud software",
|
||||
long_description=open("cli_anything/cloudcompare/README.md").read(),
|
||||
long_description=_long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
author="cli-anything",
|
||||
python_requires=">=3.10",
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
{
|
||||
"name": "cloudcompare",
|
||||
"display_name": "CloudCompare",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"description": "3D point cloud and mesh processing: load/save, color ops, normal estimation, Delaunay meshing, noise filtering, ICP registration, connected component segmentation",
|
||||
"requires": "cloudcompare (CloudCompare application installed, e.g. via Flatpak or package manager)",
|
||||
"homepage": "https://cloudcompare.org",
|
||||
|
||||
Reference in New Issue
Block a user