|
85 | 85 | # Config |
86 | 86 | CONFIG_DIR = ".c3" |
87 | 87 | CONFIG_FILE = ".c3/config.json" |
88 | | -__version__ = "2.43.0" |
| 88 | +__version__ = "2.44.0" |
89 | 89 |
|
90 | 90 |
|
91 | 91 | def _command_deps() -> CommandDeps: |
@@ -1051,6 +1051,10 @@ def cmd_init(args): |
1051 | 1051 | # ── Non-interactive (--clear) ────────────────────────────── |
1052 | 1052 | if getattr(args, "clear", False): |
1053 | 1053 | print("\n[--clear] Wiping C3 files...") |
| 1054 | + parent_link = (load_config(project_path) or {}).get("parent") or {} |
| 1055 | + if parent_link.get("path"): |
| 1056 | + print(f" [!] This project is a sub-project of {parent_link['path']}") |
| 1057 | + print(" The parent still lists it -- run 'c3 sub check --fix' there.") |
1054 | 1058 | _uninstall_mcp_all(project_path) |
1055 | 1059 | if c3_dir.exists(): |
1056 | 1060 | shutil.rmtree(c3_dir) |
@@ -5733,6 +5737,137 @@ def cmd_projects(args): |
5733 | 5737 | print(f" Port {s['port']:>5} {s.get('project_name', '?'):<25} {s.get('project_path', '')}") |
5734 | 5738 |
|
5735 | 5739 |
|
| 5740 | +def cmd_sub(args): |
| 5741 | + """Manage sub-projects: designated sub-folders with linked .c3 branches.""" |
| 5742 | + parent = str(Path(getattr(args, "parent", ".") or ".").resolve()) |
| 5743 | + if not (Path(parent) / CONFIG_DIR).is_dir(): |
| 5744 | + print(f"No .c3 found in {parent}. Run 'c3 init' there first.") |
| 5745 | + return |
| 5746 | + # Import after the cheap guard — the registry module needs a resolvable home. |
| 5747 | + from services.subprojects import VALID_CASCADE_OPS, SubprojectManager |
| 5748 | + |
| 5749 | + sm = SubprojectManager(parent) |
| 5750 | + sub = getattr(args, "sub_cmd", "list") or "list" |
| 5751 | + target = getattr(args, "target", None) |
| 5752 | + as_json = getattr(args, "json", False) |
| 5753 | + |
| 5754 | + if sub == "add": |
| 5755 | + if not target: |
| 5756 | + print("Usage: c3 sub add <folder> [--parent PATH] [--name NAME]") |
| 5757 | + return |
| 5758 | + result = sm.add( |
| 5759 | + target, |
| 5760 | + name=getattr(args, "name", None), |
| 5761 | + ide=getattr(args, "ide", None), |
| 5762 | + run_init=not getattr(args, "no_init", False), |
| 5763 | + reindex_parent=not getattr(args, "no_reindex_parent", False), |
| 5764 | + ) |
| 5765 | + if as_json: |
| 5766 | + print(json.dumps(result, indent=2)) |
| 5767 | + return |
| 5768 | + if not result.get("added"): |
| 5769 | + print(f"Failed: {result.get('error')}") |
| 5770 | + return |
| 5771 | + verb = "Adopted (existing .c3 kept)" if result.get("adopted") else "Initialized" |
| 5772 | + print(f"\n[OK] {verb}: {result['name']} ({result['path']})") |
| 5773 | + code = (result.get("parent_reindex") or {}).get("code") |
| 5774 | + if code: |
| 5775 | + print(f" Parent reindexed: {code.get('files_indexed', '?')} files, " |
| 5776 | + f"{code.get('chunks_created', '?')} chunks (sub-project now excluded)") |
| 5777 | + |
| 5778 | + elif sub == "list": |
| 5779 | + report = sm.reconcile(fix=False) # report-only consistency pass |
| 5780 | + children = sm.list() |
| 5781 | + if as_json: |
| 5782 | + print(json.dumps({"children": children, "orphans": report.get("orphans", [])}, indent=2)) |
| 5783 | + return |
| 5784 | + if not children: |
| 5785 | + print("No sub-projects designated. Use `c3 sub add <folder>`.") |
| 5786 | + return |
| 5787 | + fmt = "{:<22} {:<16} {:>6} {:>7} {}" |
| 5788 | + print(fmt.format("NAME", "STATUS", "FACTS", "ALERTS", "REL PATH")) |
| 5789 | + print("-" * 76) |
| 5790 | + for c in children: |
| 5791 | + print(fmt.format( |
| 5792 | + (c.get("name") or "?")[:21], |
| 5793 | + c.get("status", "?"), |
| 5794 | + c.get("facts_count", 0), |
| 5795 | + c.get("notification_count", 0), |
| 5796 | + c.get("rel_path", ""), |
| 5797 | + )) |
| 5798 | + issues = sum(1 for c in children if c["status"] != "ok") |
| 5799 | + line = f"\n{len(children)} sub-project(s)" |
| 5800 | + if issues: |
| 5801 | + line += f" -- {issues} with issues (run `c3 sub check --fix`)" |
| 5802 | + if report.get("orphans"): |
| 5803 | + line += f" -- {len(report['orphans'])} registry orphan(s)" |
| 5804 | + print(line) |
| 5805 | + |
| 5806 | + elif sub == "remove": |
| 5807 | + if not target: |
| 5808 | + print("Usage: c3 sub remove <name|path> [--clear] [--yes]") |
| 5809 | + return |
| 5810 | + mode = "clear" if getattr(args, "clear", False) else "unlink" |
| 5811 | + if mode == "clear" and not getattr(args, "yes", False): |
| 5812 | + print("This will DELETE the sub-project's .c3 directory and instruction docs.") |
| 5813 | + confirm = input("Type 'clear' to confirm: ").strip().lower() |
| 5814 | + if confirm != "clear": |
| 5815 | + print("Aborted.") |
| 5816 | + return |
| 5817 | + result = sm.remove(target, mode=mode, |
| 5818 | + reindex_parent=not getattr(args, "no_reindex_parent", False)) |
| 5819 | + if as_json: |
| 5820 | + print(json.dumps(result, indent=2)) |
| 5821 | + return |
| 5822 | + if not result.get("removed"): |
| 5823 | + print(f"Failed: {result.get('error')}") |
| 5824 | + return |
| 5825 | + print(f"\n[OK] {'Cleared' if mode == 'clear' else 'Unlinked'}: " |
| 5826 | + f"{result.get('name')} ({result.get('path')})") |
| 5827 | + for w in result.get("warnings", []): |
| 5828 | + print(f" warning: {w}") |
| 5829 | + |
| 5830 | + elif sub == "run": |
| 5831 | + if target not in VALID_CASCADE_OPS: |
| 5832 | + print(f"Usage: c3 sub run {{{'|'.join(VALID_CASCADE_OPS)}}} [--include-parent] [--json]") |
| 5833 | + return |
| 5834 | + result = sm.cascade(target, |
| 5835 | + include_parent=getattr(args, "include_parent", False), |
| 5836 | + mcp=getattr(args, "mcp", False)) |
| 5837 | + if as_json: |
| 5838 | + print(json.dumps(result, indent=2)) |
| 5839 | + return |
| 5840 | + for row in result["results"]: |
| 5841 | + mark = "OK " if row["ok"] else "FAIL" |
| 5842 | + extra = f" -- {row.get('error')}" if row.get("error") else "" |
| 5843 | + print(f" [{mark}] {row['name']:<22} {row['elapsed_ms']:>6}ms{extra}") |
| 5844 | + s = result["summary"] |
| 5845 | + print(f"\n{target}: {s['ok']}/{s['total']} ok, {s['failed']} failed") |
| 5846 | + |
| 5847 | + elif sub == "check": |
| 5848 | + result = sm.reconcile(fix=getattr(args, "fix", False), |
| 5849 | + prune=getattr(args, "prune", False)) |
| 5850 | + if as_json: |
| 5851 | + print(json.dumps(result, indent=2)) |
| 5852 | + return |
| 5853 | + if not result["children"] and not result["orphans"] and not result["pruned"]: |
| 5854 | + print("No sub-projects designated.") |
| 5855 | + return |
| 5856 | + for c in result["children"]: |
| 5857 | + print(f" [{c['status']:<16}] {c.get('name') or '?':<22} {c.get('rel_path', '')}") |
| 5858 | + for o in result["orphans"]: |
| 5859 | + print(f" [orphan_registry ] {o}") |
| 5860 | + for f in result.get("fixed", []): |
| 5861 | + print(f" fixed: {f.get('action')} -> {f.get('path') or f.get('rel_path')}") |
| 5862 | + for p in result.get("pruned", []): |
| 5863 | + print(f" pruned: {p.get('rel_path')}") |
| 5864 | + if result["ok"]: |
| 5865 | + print("\nAll links consistent.") |
| 5866 | + else: |
| 5867 | + hint = "" if getattr(args, "fix", False) else " Run `c3 sub check --fix` to repair." |
| 5868 | + print(f"\nIssues found.{hint}") |
| 5869 | + |
| 5870 | + |
5736 | 5871 | def cmd_session_benchmark(args): |
5737 | 5872 | """Run real-world session workflow benchmark.""" |
5738 | 5873 | if getattr(args, "command", "") == "session-benchmark": |
@@ -6577,6 +6712,7 @@ def main(): |
6577 | 6712 | "terse": cmd_terse, |
6578 | 6713 | "ui": cmd_ui, |
6579 | 6714 | "projects": cmd_projects, |
| 6715 | + "sub": cmd_sub, |
6580 | 6716 | "hub": cmd_hub, |
6581 | 6717 | "bitbucket": cmd_bitbucket, |
6582 | 6718 | "oracle": cmd_oracle, |
|
0 commit comments