Skip to content

Commit 14ce8dd

Browse files
authored
fix: cli improvements (#230)
* fix: feature support add-update * feat: feature name validation
1 parent 3f053b9 commit 14ce8dd

File tree

5 files changed

+197
-59
lines changed

5 files changed

+197
-59
lines changed

packages/browseros/build/cli/dev.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -403,29 +403,40 @@ def feature_show(
403403
raise typer.Exit(1)
404404

405405

406-
@feature_app.command(name="add")
407-
def feature_add(
408-
feature_name: str = Argument(..., help="Feature name to add"),
409-
commit: str = Argument(..., help="Git commit reference"),
410-
description: Optional[str] = Option(
411-
None, "--description", "-d", help="Feature description"
406+
@feature_app.command(name="add-update")
407+
def feature_add_update(
408+
name: str = Option(
409+
..., "--name", "-n", help="Feature name (lowercase kebab-case, e.g., 'llm-chat')"
410+
),
411+
commit: str = Option(..., "--commit", "-c", help="Git commit reference"),
412+
description: str = Option(
413+
...,
414+
"--description",
415+
"-d",
416+
help="Feature description with prefix (feat:, fix:, build:, chore:, series:)",
412417
),
413418
):
414-
"""Add a new feature from a commit"""
419+
"""Add or update a feature with files from a commit.
420+
421+
If the feature exists, merges new files into it.
422+
If the feature is new, creates it.
423+
424+
Examples:
425+
browseros dev feature add-update --name llm-chat --commit HEAD -d "feat: LLM chat panel"
426+
browseros dev feature add-update -n api -c HEAD~3 -d "feat: browseros API updates"
427+
"""
415428
ctx = create_build_context(state.chromium_src)
416429
if not ctx:
417430
raise typer.Exit(1)
418431

419-
from ..modules.feature import AddFeatureModule
432+
from ..modules.feature import AddUpdateFeatureModule
420433

421-
module = AddFeatureModule()
434+
module = AddUpdateFeatureModule()
422435
try:
423436
module.validate(ctx)
424-
module.execute(
425-
ctx, feature_name=feature_name, commit=commit, description=description
426-
)
437+
module.execute(ctx, name=name, commit=commit, description=description)
427438
except Exception as e:
428-
log_error(f"Failed to add feature: {e}")
439+
log_error(f"Failed to add/update feature: {e}")
429440
raise typer.Exit(1)
430441

431442

packages/browseros/build/features.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,8 @@ features:
366366
- chrome/browser/profiles/profile_shortcut_manager_win.cc
367367
- chrome/browser/ui/views/frame/browser_desktop_window_tree_host_win.cc
368368
- chrome/browser/ui/views/frame/browser_window_property_manager_win.cc
369-
"feat: chromium urls for browseros":
370-
description: "feat: feat: chromium urls for browseros"
369+
chromium-urls:
370+
description: "feat: chromium urls for browseros"
371371
files:
372372
- chrome/browser/chrome_content_browser_client.cc
373373
- chrome/browser/extensions/browseros_extension_constants.h

packages/browseros/build/modules/feature/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,26 @@
22
Feature module - Manage feature-to-file mappings.
33
44
Provides commands for managing features:
5-
- add_feature: Add files from a commit to a feature
5+
- add_or_update_feature: Add or update a feature with files from a commit
66
- list_features: List all defined features
77
- show_feature: Show details of a specific feature
88
- prompt_feature_selection: Interactive feature selection for extract commands
99
- add_files_to_feature: Add files to a feature (with duplicate handling)
1010
- classify_files: Classify unclassified patch files into features
11+
- validate_description: Validate description has required prefix
12+
- validate_feature_name: Validate feature name format
1113
"""
1214

15+
from .validation import (
16+
validate_description,
17+
validate_feature_name,
18+
VALID_PREFIXES,
19+
)
1320
from .feature import (
1421
add_feature,
22+
add_or_update_feature,
1523
AddFeatureModule,
24+
AddUpdateFeatureModule,
1625
list_features,
1726
ListFeaturesModule,
1827
show_feature,
@@ -28,7 +37,12 @@
2837

2938
__all__ = [
3039
"add_feature",
40+
"add_or_update_feature",
41+
"validate_description",
42+
"validate_feature_name",
43+
"VALID_PREFIXES",
3144
"AddFeatureModule",
45+
"AddUpdateFeatureModule",
3246
"list_features",
3347
"ListFeaturesModule",
3448
"show_feature",

packages/browseros/build/modules/feature/feature.py

Lines changed: 108 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,49 +5,122 @@
55
"""
66

77
import yaml
8-
from typing import Dict, Optional
8+
from typing import Dict, List, Optional, Tuple
99
from ...common.context import Context
1010
from ...common.module import CommandModule, ValidationError
1111
from ..extract.utils import get_commit_changed_files
1212
from ...common.utils import log_info, log_error, log_success, log_warning
13+
from .validation import validate_description, validate_feature_name, VALID_PREFIXES
1314

1415

15-
def add_feature(ctx: Context, feature_name: str, commit: str, description: Optional[str] = None) -> bool:
16-
"""Add files from a commit to a feature
16+
def add_or_update_feature(
17+
ctx: Context,
18+
feature_name: str,
19+
commit: str,
20+
description: str,
21+
) -> Tuple[bool, str]:
22+
"""Add or update a feature with files from a commit.
23+
24+
If feature exists, merges files (appends new ones).
25+
If feature is new, creates it.
26+
27+
Args:
28+
ctx: Build context
29+
feature_name: Feature key name (e.g., 'llm-chat')
30+
commit: Git commit reference
31+
description: Feature description with prefix (e.g., 'feat: LLM chat')
1732
18-
Examples:
19-
dev feature add my-feature HEAD
20-
dev feature add llm-chat HEAD~3 --description "LLM chat integration"
33+
Returns:
34+
Tuple of (success, error_message)
2135
"""
36+
# Validate inputs
37+
valid, error = validate_feature_name(feature_name)
38+
if not valid:
39+
return False, error
40+
41+
valid, error = validate_description(description)
42+
if not valid:
43+
return False, error
44+
2245
features_file = ctx.get_features_yaml_path()
2346

2447
# Get changed files from commit
2548
changed_files = get_commit_changed_files(commit, ctx.chromium_src)
2649
if not changed_files:
27-
log_error(f"No changed files found in commit {commit}")
28-
return False
50+
return False, f"No changed files found in commit {commit}"
2951

3052
# Load existing features
31-
features: Dict = {"features": {}}
53+
features: Dict = {"version": "1.0", "features": {}}
3254
if features_file.exists():
3355
with open(features_file, "r") as f:
3456
content = yaml.safe_load(f)
35-
if content and "features" in content:
57+
if content:
3658
features = content
59+
if "features" not in features:
60+
features["features"] = {}
61+
62+
existing_feature = features["features"].get(feature_name)
63+
64+
if existing_feature:
65+
# Update existing feature - merge files
66+
existing_files = set(existing_feature.get("files", []))
67+
new_files = set(changed_files)
68+
69+
added_files = new_files - existing_files
70+
already_present = new_files & existing_files
71+
merged_files = existing_files | new_files
72+
73+
log_info(f"Updating existing feature '{feature_name}'")
74+
log_info(f" Current files: {len(existing_files)}")
75+
log_info(f" Files from commit: {len(new_files)}")
76+
77+
if added_files:
78+
log_success(f" Adding {len(added_files)} new file(s):")
79+
for f in sorted(added_files)[:10]:
80+
log_info(f" + {f}")
81+
if len(added_files) > 10:
82+
log_info(f" ... and {len(added_files) - 10} more")
3783

38-
# Add or update feature
39-
features["features"][feature_name] = {
40-
"description": description or f"Feature: {feature_name}",
41-
"files": sorted(changed_files),
42-
"commit": commit,
43-
}
84+
if already_present:
85+
log_warning(f" Skipping {len(already_present)} file(s) already in feature")
86+
87+
features["features"][feature_name]["files"] = sorted(merged_files)
88+
# Update description if provided (allows updating description)
89+
features["features"][feature_name]["description"] = description
90+
91+
else:
92+
# Create new feature
93+
log_info(f"Creating new feature '{feature_name}'")
94+
log_info(f" Files from commit: {len(changed_files)}")
95+
96+
features["features"][feature_name] = {
97+
"description": description,
98+
"files": sorted(changed_files),
99+
}
44100

45101
# Save to file
46102
with open(features_file, "w") as f:
47103
yaml.safe_dump(features, f, sort_keys=False, default_flow_style=False)
48104

49-
log_success(f"✓ Added feature '{feature_name}' with {len(changed_files)} files")
50-
return True
105+
total_files = len(features["features"][feature_name]["files"])
106+
if existing_feature:
107+
log_success(f"✓ Updated feature '{feature_name}' - now has {total_files} files")
108+
else:
109+
log_success(f"✓ Created feature '{feature_name}' with {total_files} files")
110+
111+
return True, ""
112+
113+
114+
# Keep old function name for backwards compatibility but mark deprecated
115+
def add_feature(ctx: Context, feature_name: str, commit: str, description: Optional[str] = None) -> bool:
116+
"""Deprecated: Use add_or_update_feature instead."""
117+
if description is None:
118+
log_error("Description is required and must have a valid prefix")
119+
return False
120+
success, error = add_or_update_feature(ctx, feature_name, commit, description)
121+
if not success:
122+
log_error(error)
123+
return success
51124

52125

53126
def list_features(ctx: Context):
@@ -134,11 +207,11 @@ def execute(self, ctx: Context, feature_name: str, **kwargs) -> None:
134207
show_feature(ctx, feature_name)
135208

136209

137-
class AddFeatureModule(CommandModule):
138-
"""Add files from a commit to a feature"""
210+
class AddUpdateFeatureModule(CommandModule):
211+
"""Add or update a feature with files from a commit"""
139212
produces = []
140213
requires = []
141-
description = "Add files from a commit to a feature"
214+
description = "Add or update a feature with files from a commit"
142215

143216
def validate(self, ctx: Context) -> None:
144217
"""Validate git is available"""
@@ -148,10 +221,21 @@ def validate(self, ctx: Context) -> None:
148221
if not ctx.chromium_src.exists():
149222
raise ValidationError(f"Chromium source not found: {ctx.chromium_src}")
150223

151-
def execute(self, ctx: Context, feature_name: str, commit: str, description: Optional[str] = None, **kwargs) -> None:
152-
success = add_feature(ctx, feature_name, commit, description)
224+
def execute(
225+
self,
226+
ctx: Context,
227+
name: str,
228+
commit: str,
229+
description: str,
230+
**kwargs,
231+
) -> None:
232+
success, error = add_or_update_feature(ctx, name, commit, description)
153233
if not success:
154-
raise RuntimeError(f"Failed to add feature '{feature_name}'")
234+
raise RuntimeError(error)
235+
236+
237+
# Backwards compatibility alias
238+
AddFeatureModule = AddUpdateFeatureModule
155239

156240

157241
class ClassifyFeaturesModule(CommandModule):

packages/browseros/build/modules/feature/select.py

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from ...common.context import Context
1313
from ...common.utils import log_info, log_success, log_warning, log_error
14+
from .validation import validate_feature_name, validate_description, VALID_PREFIXES
1415

1516

1617
def load_features_yaml(features_file: Path) -> Dict:
@@ -120,29 +121,57 @@ def prompt_new_feature(default_description: Optional[str] = None) -> Optional[Tu
120121
log_info("")
121122
log_info("Creating new feature:")
122123
log_info("-" * 40)
124+
log_info(f" Valid prefixes: {', '.join(VALID_PREFIXES)}")
125+
log_info("")
123126

124127
try:
125-
# Get feature name
126-
feature_name = input("Feature name: ").strip()
127-
if not feature_name:
128-
log_warning("Cancelled - no feature name provided")
129-
return None
130-
131-
# Sanitize feature name (lowercase, hyphens instead of spaces)
132-
feature_name = feature_name.lower().replace(" ", "-")
133-
134-
# Get description
135-
if default_description:
136-
desc_prompt = f"Description [{default_description}]: "
137-
else:
138-
desc_prompt = "Description: "
128+
# Get and validate feature name
129+
while True:
130+
feature_name = input("Feature name (kebab-case): ").strip()
131+
if not feature_name:
132+
log_warning("Cancelled - no feature name provided")
133+
return None
139134

140-
description = input(desc_prompt).strip()
141-
if not description and default_description:
142-
description = default_description
135+
# Sanitize feature name (lowercase, hyphens instead of spaces)
136+
feature_name = feature_name.lower().replace(" ", "-")
137+
138+
# Validate
139+
valid, error = validate_feature_name(feature_name)
140+
if valid:
141+
break
142+
log_warning(f"Invalid name: {error}")
143+
144+
# Get and validate description
145+
while True:
146+
if default_description:
147+
# Check if default already has valid prefix
148+
valid, _ = validate_description(default_description)
149+
if valid:
150+
desc_prompt = f"Description [{default_description}]: "
151+
else:
152+
desc_prompt = f"Description (e.g., feat: {default_description}): "
153+
else:
154+
desc_prompt = "Description (e.g., feat: Add feature): "
155+
156+
description = input(desc_prompt).strip()
157+
if not description and default_description:
158+
# Check if default is valid
159+
valid, _ = validate_description(default_description)
160+
if valid:
161+
description = default_description
162+
else:
163+
log_warning(f"Default description needs prefix. Valid: {', '.join(VALID_PREFIXES)}")
164+
continue
165+
166+
if not description:
167+
log_warning(f"Description required. Must start with: {', '.join(VALID_PREFIXES)}")
168+
continue
143169

144-
if not description:
145-
description = f"Feature: {feature_name}"
170+
# Validate
171+
valid, error = validate_description(description)
172+
if valid:
173+
break
174+
log_warning(f"Invalid description: {error}")
146175

147176
return (feature_name, description)
148177

0 commit comments

Comments
 (0)