"""c_project_create (03-C §3.0): register/scaffold a target project. U-002 scope: the directory-scaffolding + DB-persistence half of c_project_create -- no HTTP endpoint yet (that's U-007); this is the plain function U-007 will wire up. Target-project directory convention is architecture doc §13.2, NOT §7.3 (see development-plan.md's 2026-07-31 correction: §7.3 describes graphlemon's own platform repo, not a target project). c_project_create only ever creates `.graphlemon/` inside the target folder; a target project's own `src/` and everything else in it is left untouched. """ import os import subprocess import uuid _GIT_IDENTITY = [ "-c", "user.name=graphlemon", "-c", "user.email=graphlemon@localhost", ] class PathNotFoundError(Exception): """PATH_NOT_FOUND -- 03-C §3.0: target_folder_path does not exist.""" class PathNotWritableError(Exception): """PATH_NOT_WRITABLE -- 03-C §3.0: no write permission on target_folder_path.""" class GitInitDeclinedError(Exception): """GIT_INIT_DECLINED -- 03-C §3.0: not a git repo and caller didn't ask to init one. 02-V §2.1: the UI must get explicit user confirmation before setting init_git_if_missing=True; this function never silently runs `git init`. """ def _run_git(args, cwd): subprocess.run(["git"] + args, cwd=cwd, check=True, capture_output=True, text=True) def create_project(conn, target_folder_path, name=None, init_git_if_missing=False): """Implements c_project_create (03-C §3.0). Returns {"project_id", "has_git_repo", "graphlemon_subdir_initialized"}. Idempotent: a second call with the same target_folder_path returns the existing row untouched (01-M 1.0 幂等约束) -- no re-validation, no repeat side effects, matching REQ-11's failure-case wording. """ existing = conn.execute( "SELECT project_id, has_git_repo, graphlemon_subdir_initialized " "FROM project WHERE target_folder_path = ?", (target_folder_path,), ).fetchone() if existing is not None: return { "project_id": existing[0], "has_git_repo": bool(existing[1]), "graphlemon_subdir_initialized": bool(existing[2]), } if not os.path.isdir(target_folder_path): raise PathNotFoundError(target_folder_path) if not os.access(target_folder_path, os.W_OK): raise PathNotWritableError(target_folder_path) has_git_repo = os.path.isdir(os.path.join(target_folder_path, ".git")) if not has_git_repo: if not init_git_if_missing: raise GitInitDeclinedError(target_folder_path) _run_git(["init"], cwd=target_folder_path) has_git_repo = True graphlemon_subdir = os.path.join(target_folder_path, ".graphlemon") os.makedirs(graphlemon_subdir, exist_ok=True) # git doesn't track empty directories; this is only so the init commit has # something under .graphlemon/ to record. Real content (runs//...) # arrives once tasks actually execute (01-M 1.5 artifact.file_path). open(os.path.join(graphlemon_subdir, ".gitkeep"), "w").close() _run_git(["add", ".graphlemon"], cwd=target_folder_path) _run_git( _GIT_IDENTITY + ["commit", "-m", "graphlemon: initialize .graphlemon/ workspace"], cwd=target_folder_path, ) project_id = str(uuid.uuid4()) project_name = name or os.path.basename(os.path.normpath(target_folder_path)) conn.execute( "INSERT INTO project " "(project_id, name, target_folder_path, has_git_repo, graphlemon_subdir_initialized, status) " "VALUES (?, ?, ?, ?, 1, 'active')", (project_id, project_name, target_folder_path, int(has_git_repo)), ) conn.commit() return { "project_id": project_id, "has_git_repo": has_git_repo, "graphlemon_subdir_initialized": True, }