vibe-coding-master 0.4.0 → 0.4.2
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.
- package/README.md +3 -3
- package/dist/backend/cli/install-vcm-harness.js +1 -1
- package/dist/backend/server.js +8 -8
- package/dist/backend/services/claude-hook-service.js +5 -0
- package/dist/backend/services/harness-service.js +119 -174
- package/dist/backend/services/message-service.js +0 -32
- package/dist/backend/templates/harness/architect-agent.js +1 -1
- package/dist/backend/templates/harness/claude-root.js +1 -1
- package/dist/backend/templates/harness/coder-agent.js +1 -1
- package/dist/backend/templates/harness/vcm-harness-bootstrap-skill.js +1 -1
- package/dist-frontend/assets/{index-CMuJhDdX.js → index-uYhgIwNK.js} +22 -22
- package/dist-frontend/index.html +1 -1
- package/docs/full-harness-baseline.md +5 -5
- package/docs/product-design.md +1 -1
- package/docs/vcm-cc-best-practices.md +12 -18
- package/package.json +1 -1
- package/scripts/harness-tools/__pycache__/generate-module-indexcpython-314.pyc +0 -0
- package/scripts/harness-tools/__pycache__/generate-public-surfacecpython-314.pyc +0 -0
- package/scripts/harness-tools/generate-module-index +225 -5
- package/scripts/harness-tools/generate-public-surface +388 -3
|
@@ -23,6 +23,20 @@ BLOCK_ITEM_KEYWORDS = {
|
|
|
23
23
|
"union",
|
|
24
24
|
"macro_rules",
|
|
25
25
|
}
|
|
26
|
+
TS_SOURCE_EXTENSIONS = (".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs")
|
|
27
|
+
TS_EXPORT_DECLARATION = re.compile(
|
|
28
|
+
r"\bexport\s+(?:default\s+)?(?:declare\s+)?(?:(?:async\s+)?function\s+(?P<function>[A-Za-z_$][A-Za-z0-9_$]*)|class\s+(?P<class>[A-Za-z_$][A-Za-z0-9_$]*)|interface\s+(?P<interface>[A-Za-z_$][A-Za-z0-9_$]*)|type\s+(?P<type>[A-Za-z_$][A-Za-z0-9_$]*)|enum\s+(?P<enum>[A-Za-z_$][A-Za-z0-9_$]*)|(?:const|let|var)\s+(?P<variable>[A-Za-z_$][A-Za-z0-9_$]*))"
|
|
29
|
+
)
|
|
30
|
+
TS_REEXPORT = re.compile(
|
|
31
|
+
r"\bexport\s+(?:type\s+)?(?P<form>\*|\{[^}]*\})\s+from\s+(?P<quote>[\"'`])(?P<specifier>[^\"'`]+)(?P=quote)"
|
|
32
|
+
)
|
|
33
|
+
TS_ROUTE_CALL = re.compile(
|
|
34
|
+
r"\.\s*(?P<method>get|post|put|patch|delete|options|head)\s*\(\s*(?P<quote>[\"'`])(?P<path>[^\"'`]+)(?P=quote)",
|
|
35
|
+
re.IGNORECASE,
|
|
36
|
+
)
|
|
37
|
+
TS_ROUTE_OBJECT = re.compile(r"\.route\s*\(\s*\{(?P<body>.*?)\}\s*\)", re.IGNORECASE | re.DOTALL)
|
|
38
|
+
TS_ROUTE_METHOD = re.compile(r"\bmethod\s*:\s*(?P<quote>[\"'`])(?P<method>[A-Za-z]+)(?P=quote)", re.IGNORECASE)
|
|
39
|
+
TS_ROUTE_URL = re.compile(r"\b(?:url|path)\s*:\s*(?P<quote>[\"'`])(?P<path>[^\"'`]+)(?P=quote)", re.IGNORECASE)
|
|
26
40
|
|
|
27
41
|
|
|
28
42
|
@dataclass(frozen=True)
|
|
@@ -56,6 +70,24 @@ def load_module_index(root: Path) -> dict:
|
|
|
56
70
|
return json.loads(path.read_text())
|
|
57
71
|
|
|
58
72
|
|
|
73
|
+
def load_optional_json(path: Path) -> dict:
|
|
74
|
+
if not path.is_file():
|
|
75
|
+
return {}
|
|
76
|
+
return json.loads(path.read_text())
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def relative_path(path: Path, root: Path) -> str:
|
|
80
|
+
try:
|
|
81
|
+
rel = path.absolute().relative_to(root.absolute())
|
|
82
|
+
except ValueError:
|
|
83
|
+
rel = path.resolve().relative_to(root.resolve())
|
|
84
|
+
return "." if str(rel) == "." else rel.as_posix()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def line_number_from_offset(source: str, offset: int) -> int:
|
|
88
|
+
return source.count("\n", 0, offset) + 1
|
|
89
|
+
|
|
90
|
+
|
|
59
91
|
def raw_string_end(source: str, index: int) -> int | None:
|
|
60
92
|
for prefix in ("br", "rb", "r"):
|
|
61
93
|
if not source.startswith(prefix, index):
|
|
@@ -633,13 +665,366 @@ def extract_public_items(root: Path, module: dict) -> list[dict]:
|
|
|
633
665
|
return unique_items
|
|
634
666
|
|
|
635
667
|
|
|
668
|
+
def remove_ts_comments_preserve_literals(source: str) -> str:
|
|
669
|
+
output: list[str] = []
|
|
670
|
+
cursor = 0
|
|
671
|
+
quote: str | None = None
|
|
672
|
+
escaped = False
|
|
673
|
+
|
|
674
|
+
while cursor < len(source):
|
|
675
|
+
char = source[cursor]
|
|
676
|
+
|
|
677
|
+
if quote is not None:
|
|
678
|
+
output.append(char)
|
|
679
|
+
if escaped:
|
|
680
|
+
escaped = False
|
|
681
|
+
elif char == "\\":
|
|
682
|
+
escaped = True
|
|
683
|
+
elif char == quote:
|
|
684
|
+
quote = None
|
|
685
|
+
cursor += 1
|
|
686
|
+
continue
|
|
687
|
+
|
|
688
|
+
if source.startswith("//", cursor):
|
|
689
|
+
end = source.find("\n", cursor + 2)
|
|
690
|
+
if end == -1:
|
|
691
|
+
output.append(" " * (len(source) - cursor))
|
|
692
|
+
break
|
|
693
|
+
output.append(" " * (end - cursor))
|
|
694
|
+
output.append("\n")
|
|
695
|
+
cursor = end + 1
|
|
696
|
+
continue
|
|
697
|
+
|
|
698
|
+
if source.startswith("/*", cursor):
|
|
699
|
+
end = source.find("*/", cursor + 2)
|
|
700
|
+
end = len(source) if end == -1 else end + 2
|
|
701
|
+
chunk = source[cursor:end]
|
|
702
|
+
output.append("".join("\n" if item == "\n" else " " for item in chunk))
|
|
703
|
+
cursor = end
|
|
704
|
+
continue
|
|
705
|
+
|
|
706
|
+
if char in {"'", '"', "`"}:
|
|
707
|
+
quote = char
|
|
708
|
+
escaped = False
|
|
709
|
+
output.append(char)
|
|
710
|
+
cursor += 1
|
|
711
|
+
continue
|
|
712
|
+
|
|
713
|
+
output.append(char)
|
|
714
|
+
cursor += 1
|
|
715
|
+
|
|
716
|
+
return "".join(output)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def ts_signature_from_source(source: str, start: int) -> str:
|
|
720
|
+
cursor = start
|
|
721
|
+
depth_angle = 0
|
|
722
|
+
depth_paren = 0
|
|
723
|
+
depth_bracket = 0
|
|
724
|
+
|
|
725
|
+
while cursor < len(source):
|
|
726
|
+
char = source[cursor]
|
|
727
|
+
if char in {"'", '"', "`"}:
|
|
728
|
+
cursor = quoted_literal_end(source, cursor) if char != "`" else quoted_literal_end(source.replace("`", '"'), cursor)
|
|
729
|
+
continue
|
|
730
|
+
|
|
731
|
+
if char == "<":
|
|
732
|
+
depth_angle += 1
|
|
733
|
+
elif char == ">" and depth_angle:
|
|
734
|
+
depth_angle -= 1
|
|
735
|
+
elif char == "(":
|
|
736
|
+
depth_paren += 1
|
|
737
|
+
elif char == ")" and depth_paren:
|
|
738
|
+
depth_paren -= 1
|
|
739
|
+
elif char == "[":
|
|
740
|
+
depth_bracket += 1
|
|
741
|
+
elif char == "]" and depth_bracket:
|
|
742
|
+
depth_bracket -= 1
|
|
743
|
+
|
|
744
|
+
at_top_level = depth_angle == 0 and depth_paren == 0 and depth_bracket == 0
|
|
745
|
+
if at_top_level and char in "{;":
|
|
746
|
+
return " ".join(source[start:cursor].split())
|
|
747
|
+
cursor += 1
|
|
748
|
+
|
|
749
|
+
return " ".join(source[start:].split())
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def ts_item_kind(match: re.Match) -> tuple[str, str] | None:
|
|
753
|
+
for kind in ("function", "class", "interface", "type", "enum", "variable"):
|
|
754
|
+
name = match.group(kind)
|
|
755
|
+
if name:
|
|
756
|
+
return ("const" if kind == "variable" else kind, name)
|
|
757
|
+
return None
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def extract_ts_exported_declarations(source: str, source_file: str) -> list[dict]:
|
|
761
|
+
sanitized = remove_ts_comments_preserve_literals(source)
|
|
762
|
+
items: list[dict] = []
|
|
763
|
+
|
|
764
|
+
for match in TS_EXPORT_DECLARATION.finditer(sanitized):
|
|
765
|
+
kind_and_name = ts_item_kind(match)
|
|
766
|
+
if kind_and_name is None:
|
|
767
|
+
continue
|
|
768
|
+
kind, name = kind_and_name
|
|
769
|
+
items.append(
|
|
770
|
+
{
|
|
771
|
+
"path": name,
|
|
772
|
+
"kind": kind,
|
|
773
|
+
"name": name,
|
|
774
|
+
"source": {
|
|
775
|
+
"path": source_file,
|
|
776
|
+
"line": line_number_from_offset(sanitized, match.start()),
|
|
777
|
+
},
|
|
778
|
+
"signature": ts_signature_from_source(source, match.start()),
|
|
779
|
+
}
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
return items
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def exported_names_from_group(group: str) -> list[tuple[str, str]]:
|
|
786
|
+
names: list[tuple[str, str]] = []
|
|
787
|
+
body = group.strip()[1:-1]
|
|
788
|
+
for raw_part in body.split(","):
|
|
789
|
+
part = raw_part.strip()
|
|
790
|
+
if not part:
|
|
791
|
+
continue
|
|
792
|
+
if part.startswith("type "):
|
|
793
|
+
part = part.removeprefix("type ").strip()
|
|
794
|
+
pieces = [piece.strip() for piece in re.split(r"\s+as\s+", part, maxsplit=1)]
|
|
795
|
+
source_name = pieces[0]
|
|
796
|
+
public_name = pieces[1] if len(pieces) == 2 else source_name
|
|
797
|
+
if source_name and source_name != "default":
|
|
798
|
+
names.append((source_name, public_name))
|
|
799
|
+
return names
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def resolve_ts_specifier(current_file: Path, specifier: str, indexed_sources: dict[Path, str]) -> str | None:
|
|
803
|
+
if not specifier.startswith("."):
|
|
804
|
+
return None
|
|
805
|
+
|
|
806
|
+
base = (current_file.parent / specifier).resolve()
|
|
807
|
+
candidates: list[Path] = []
|
|
808
|
+
|
|
809
|
+
if base.suffix:
|
|
810
|
+
candidates.append(base)
|
|
811
|
+
for extension in TS_SOURCE_EXTENSIONS:
|
|
812
|
+
candidates.append(base.with_suffix(extension))
|
|
813
|
+
else:
|
|
814
|
+
for extension in TS_SOURCE_EXTENSIONS:
|
|
815
|
+
candidates.append(base.with_suffix(extension))
|
|
816
|
+
for extension in TS_SOURCE_EXTENSIONS:
|
|
817
|
+
candidates.append(base / f"index{extension}")
|
|
818
|
+
|
|
819
|
+
for candidate in candidates:
|
|
820
|
+
source_file = indexed_sources.get(candidate.resolve())
|
|
821
|
+
if source_file is not None:
|
|
822
|
+
return source_file
|
|
823
|
+
|
|
824
|
+
return None
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def package_export_targets(value) -> list[str]:
|
|
828
|
+
if isinstance(value, str):
|
|
829
|
+
return [value]
|
|
830
|
+
if isinstance(value, list):
|
|
831
|
+
targets: list[str] = []
|
|
832
|
+
for item in value:
|
|
833
|
+
targets.extend(package_export_targets(item))
|
|
834
|
+
return targets
|
|
835
|
+
if isinstance(value, dict):
|
|
836
|
+
targets: list[str] = []
|
|
837
|
+
for key, item in value.items():
|
|
838
|
+
if key in {"types", "import", "require", "default", "module"} or key.startswith("."):
|
|
839
|
+
targets.extend(package_export_targets(item))
|
|
840
|
+
return targets
|
|
841
|
+
return []
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def ts_entrypoint_files(root: Path, module: dict) -> list[str]:
|
|
845
|
+
indexed_sources = set(module.get("files", {}).get("source", []))
|
|
846
|
+
module_path = module.get("path", ".")
|
|
847
|
+
module_root = root if module_path == "." else root / module_path
|
|
848
|
+
manifest = load_optional_json(root / module.get("manifest", ""))
|
|
849
|
+
targets: list[str] = []
|
|
850
|
+
|
|
851
|
+
for key in ("exports", "main", "module", "types"):
|
|
852
|
+
targets.extend(package_export_targets(manifest.get(key)))
|
|
853
|
+
|
|
854
|
+
candidates: list[str] = []
|
|
855
|
+
for target in targets:
|
|
856
|
+
if not isinstance(target, str):
|
|
857
|
+
continue
|
|
858
|
+
target_path = (module_root / target).resolve()
|
|
859
|
+
for candidate in [target_path, *[target_path.with_suffix(ext) for ext in TS_SOURCE_EXTENSIONS]]:
|
|
860
|
+
rel = relative_path(candidate, root)
|
|
861
|
+
if rel in indexed_sources:
|
|
862
|
+
candidates.append(rel)
|
|
863
|
+
|
|
864
|
+
if not candidates:
|
|
865
|
+
for name in ("index", "main", "app"):
|
|
866
|
+
for extension in TS_SOURCE_EXTENSIONS:
|
|
867
|
+
rel = relative_path(module_root / "src" / f"{name}{extension}", root)
|
|
868
|
+
if rel in indexed_sources:
|
|
869
|
+
candidates.append(rel)
|
|
870
|
+
|
|
871
|
+
if not candidates and module_path.startswith("apps/"):
|
|
872
|
+
candidates.extend(module.get("files", {}).get("source", []))
|
|
873
|
+
|
|
874
|
+
return sorted(dict.fromkeys(candidates))
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def collect_ts_public_items(
|
|
878
|
+
root: Path,
|
|
879
|
+
source_file: str,
|
|
880
|
+
module: dict,
|
|
881
|
+
direct_definitions: dict[str, dict[str, dict]],
|
|
882
|
+
indexed_sources: dict[Path, str],
|
|
883
|
+
visited: set[str],
|
|
884
|
+
) -> list[dict]:
|
|
885
|
+
if source_file in visited:
|
|
886
|
+
return []
|
|
887
|
+
visited.add(source_file)
|
|
888
|
+
|
|
889
|
+
path = root / source_file
|
|
890
|
+
source = path.read_text()
|
|
891
|
+
sanitized = remove_ts_comments_preserve_literals(source)
|
|
892
|
+
items = list(direct_definitions.get(source_file, {}).values())
|
|
893
|
+
|
|
894
|
+
for match in TS_REEXPORT.finditer(sanitized):
|
|
895
|
+
target_file = resolve_ts_specifier(path, match.group("specifier"), indexed_sources)
|
|
896
|
+
if target_file is None:
|
|
897
|
+
continue
|
|
898
|
+
|
|
899
|
+
form = match.group("form")
|
|
900
|
+
if form == "*":
|
|
901
|
+
items.extend(collect_ts_public_items(root, target_file, module, direct_definitions, indexed_sources, visited))
|
|
902
|
+
continue
|
|
903
|
+
|
|
904
|
+
target_definitions = direct_definitions.get(target_file, {})
|
|
905
|
+
for source_name, public_name in exported_names_from_group(form):
|
|
906
|
+
definition = target_definitions.get(source_name)
|
|
907
|
+
if definition is None:
|
|
908
|
+
items.append(
|
|
909
|
+
{
|
|
910
|
+
"path": public_name,
|
|
911
|
+
"kind": "re-export",
|
|
912
|
+
"name": public_name,
|
|
913
|
+
"source": {
|
|
914
|
+
"path": source_file,
|
|
915
|
+
"line": line_number_from_offset(sanitized, match.start()),
|
|
916
|
+
},
|
|
917
|
+
}
|
|
918
|
+
)
|
|
919
|
+
continue
|
|
920
|
+
items.append({**definition, "path": public_name, "name": public_name})
|
|
921
|
+
|
|
922
|
+
return items
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def extract_ts_routes(source: str, source_file: str) -> list[dict]:
|
|
926
|
+
sanitized = remove_ts_comments_preserve_literals(source)
|
|
927
|
+
items: list[dict] = []
|
|
928
|
+
|
|
929
|
+
for match in TS_ROUTE_CALL.finditer(sanitized):
|
|
930
|
+
method = match.group("method").upper()
|
|
931
|
+
route_path = match.group("path")
|
|
932
|
+
name = f"{method} {route_path}"
|
|
933
|
+
items.append(
|
|
934
|
+
{
|
|
935
|
+
"path": name,
|
|
936
|
+
"kind": "route",
|
|
937
|
+
"name": name,
|
|
938
|
+
"source": {
|
|
939
|
+
"path": source_file,
|
|
940
|
+
"line": line_number_from_offset(sanitized, match.start()),
|
|
941
|
+
},
|
|
942
|
+
}
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
for match in TS_ROUTE_OBJECT.finditer(sanitized):
|
|
946
|
+
body = match.group("body")
|
|
947
|
+
method_match = TS_ROUTE_METHOD.search(body)
|
|
948
|
+
path_match = TS_ROUTE_URL.search(body)
|
|
949
|
+
if method_match is None or path_match is None:
|
|
950
|
+
continue
|
|
951
|
+
name = f"{method_match.group('method').upper()} {path_match.group('path')}"
|
|
952
|
+
items.append(
|
|
953
|
+
{
|
|
954
|
+
"path": name,
|
|
955
|
+
"kind": "route",
|
|
956
|
+
"name": name,
|
|
957
|
+
"source": {
|
|
958
|
+
"path": source_file,
|
|
959
|
+
"line": line_number_from_offset(sanitized, match.start()),
|
|
960
|
+
},
|
|
961
|
+
}
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
return items
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def extract_typescript_public_items(root: Path, module: dict) -> list[dict]:
|
|
968
|
+
indexed_sources = {
|
|
969
|
+
(root / source_file).resolve(): source_file
|
|
970
|
+
for source_file in module.get("files", {}).get("source", [])
|
|
971
|
+
}
|
|
972
|
+
direct_definitions: dict[str, dict[str, dict]] = {}
|
|
973
|
+
|
|
974
|
+
for source_file in module.get("files", {}).get("source", []):
|
|
975
|
+
path = root / source_file
|
|
976
|
+
if not path.is_file():
|
|
977
|
+
raise SystemExit(f"Missing source file from module index: {source_file}")
|
|
978
|
+
direct_definitions[source_file] = {
|
|
979
|
+
item["name"]: item
|
|
980
|
+
for item in extract_ts_exported_declarations(path.read_text(), source_file)
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
entrypoints = ts_entrypoint_files(root, module)
|
|
984
|
+
if not entrypoints:
|
|
985
|
+
entrypoints = list(module.get("files", {}).get("source", []))
|
|
986
|
+
|
|
987
|
+
items: list[dict] = []
|
|
988
|
+
for entrypoint in entrypoints:
|
|
989
|
+
items.extend(collect_ts_public_items(root, entrypoint, module, direct_definitions, indexed_sources, set()))
|
|
990
|
+
|
|
991
|
+
if module.get("path", "").startswith("apps/"):
|
|
992
|
+
for source_file in module.get("files", {}).get("source", []):
|
|
993
|
+
items.extend(extract_ts_routes((root / source_file).read_text(), source_file))
|
|
994
|
+
|
|
995
|
+
seen: set[tuple[str, str, str, int]] = set()
|
|
996
|
+
unique_items: list[dict] = []
|
|
997
|
+
for item in items:
|
|
998
|
+
key = (item["path"], item["kind"], item["source"]["path"], item["source"]["line"])
|
|
999
|
+
if key in seen:
|
|
1000
|
+
continue
|
|
1001
|
+
seen.add(key)
|
|
1002
|
+
unique_items.append(item)
|
|
1003
|
+
|
|
1004
|
+
return unique_items
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def module_index_is_typescript(module_index: dict) -> bool:
|
|
1008
|
+
workspace = module_index.get("workspace", {})
|
|
1009
|
+
workspace_type = workspace.get("type")
|
|
1010
|
+
if workspace_type in {"npm-workspaces", "npm-package"}:
|
|
1011
|
+
return True
|
|
1012
|
+
manifest = workspace.get("manifest")
|
|
1013
|
+
return isinstance(manifest, str) and manifest.endswith("package.json")
|
|
1014
|
+
|
|
1015
|
+
|
|
636
1016
|
def build_surface(root: Path) -> dict:
|
|
637
1017
|
module_index = load_module_index(root)
|
|
1018
|
+
is_typescript = module_index_is_typescript(module_index)
|
|
638
1019
|
modules = []
|
|
639
1020
|
|
|
640
1021
|
for layer in module_index.get("layers", []):
|
|
641
1022
|
for module in layer.get("modules", []):
|
|
642
|
-
public_items =
|
|
1023
|
+
public_items = (
|
|
1024
|
+
extract_typescript_public_items(root, module)
|
|
1025
|
+
if is_typescript or module.get("language") == "typescript"
|
|
1026
|
+
else extract_public_items(root, module)
|
|
1027
|
+
)
|
|
643
1028
|
modules.append(
|
|
644
1029
|
{
|
|
645
1030
|
"name": module["name"],
|
|
@@ -651,13 +1036,13 @@ def build_surface(root: Path) -> dict:
|
|
|
651
1036
|
"schemaVersion": 1,
|
|
652
1037
|
"kind": "public-surface",
|
|
653
1038
|
"generatedBy": ".ai/tools/generate-public-surface",
|
|
654
|
-
"visibility": "crate-external",
|
|
1039
|
+
"visibility": "project-public" if is_typescript else "crate-external",
|
|
655
1040
|
"modules": modules,
|
|
656
1041
|
}
|
|
657
1042
|
|
|
658
1043
|
|
|
659
1044
|
def main() -> int:
|
|
660
|
-
parser = argparse.ArgumentParser(description="Generate .ai/generated/public-surface.json from Rust source files.")
|
|
1045
|
+
parser = argparse.ArgumentParser(description="Generate .ai/generated/public-surface.json from Rust or TypeScript source files.")
|
|
661
1046
|
parser.add_argument("--check", action="store_true", help="Fail if the generated public surface differs from the current file.")
|
|
662
1047
|
parser.add_argument("--print", action="store_true", help="Print generated JSON instead of writing it.")
|
|
663
1048
|
parser.add_argument("--output", default=".ai/generated/public-surface.json", help="Output path relative to the project root.")
|