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:
Taeyoung96
2026-03-30 20:29:41 +09:00
parent fc5794ddba
commit 17a1b42edf
8 changed files with 75 additions and 32 deletions

View File

@@ -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.
---

View File

@@ -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:

View File

@@ -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,

View File

@@ -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 ───────────────────────────────────────────────────────

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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",