scc-cli 1.5.3__py3-none-any.whl
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.
Potentially problematic release.
This version of scc-cli might be problematic. Click here for more details.
- scc_cli/__init__.py +15 -0
- scc_cli/audit/__init__.py +37 -0
- scc_cli/audit/parser.py +191 -0
- scc_cli/audit/reader.py +180 -0
- scc_cli/auth.py +145 -0
- scc_cli/claude_adapter.py +485 -0
- scc_cli/cli.py +311 -0
- scc_cli/cli_common.py +190 -0
- scc_cli/cli_helpers.py +244 -0
- scc_cli/commands/__init__.py +20 -0
- scc_cli/commands/admin.py +708 -0
- scc_cli/commands/audit.py +246 -0
- scc_cli/commands/config.py +528 -0
- scc_cli/commands/exceptions.py +696 -0
- scc_cli/commands/init.py +272 -0
- scc_cli/commands/launch/__init__.py +73 -0
- scc_cli/commands/launch/app.py +1247 -0
- scc_cli/commands/launch/render.py +309 -0
- scc_cli/commands/launch/sandbox.py +135 -0
- scc_cli/commands/launch/workspace.py +339 -0
- scc_cli/commands/org/__init__.py +49 -0
- scc_cli/commands/org/_builders.py +264 -0
- scc_cli/commands/org/app.py +41 -0
- scc_cli/commands/org/import_cmd.py +267 -0
- scc_cli/commands/org/init_cmd.py +269 -0
- scc_cli/commands/org/schema_cmd.py +76 -0
- scc_cli/commands/org/status_cmd.py +157 -0
- scc_cli/commands/org/update_cmd.py +330 -0
- scc_cli/commands/org/validate_cmd.py +138 -0
- scc_cli/commands/support.py +323 -0
- scc_cli/commands/team.py +910 -0
- scc_cli/commands/worktree/__init__.py +72 -0
- scc_cli/commands/worktree/_helpers.py +57 -0
- scc_cli/commands/worktree/app.py +170 -0
- scc_cli/commands/worktree/container_commands.py +385 -0
- scc_cli/commands/worktree/context_commands.py +61 -0
- scc_cli/commands/worktree/session_commands.py +128 -0
- scc_cli/commands/worktree/worktree_commands.py +734 -0
- scc_cli/config.py +647 -0
- scc_cli/confirm.py +20 -0
- scc_cli/console.py +562 -0
- scc_cli/contexts.py +394 -0
- scc_cli/core/__init__.py +68 -0
- scc_cli/core/constants.py +101 -0
- scc_cli/core/errors.py +297 -0
- scc_cli/core/exit_codes.py +91 -0
- scc_cli/core/workspace.py +57 -0
- scc_cli/deprecation.py +54 -0
- scc_cli/deps.py +189 -0
- scc_cli/docker/__init__.py +127 -0
- scc_cli/docker/core.py +467 -0
- scc_cli/docker/credentials.py +726 -0
- scc_cli/docker/launch.py +595 -0
- scc_cli/doctor/__init__.py +105 -0
- scc_cli/doctor/checks/__init__.py +166 -0
- scc_cli/doctor/checks/cache.py +314 -0
- scc_cli/doctor/checks/config.py +107 -0
- scc_cli/doctor/checks/environment.py +182 -0
- scc_cli/doctor/checks/json_helpers.py +157 -0
- scc_cli/doctor/checks/organization.py +264 -0
- scc_cli/doctor/checks/worktree.py +278 -0
- scc_cli/doctor/render.py +365 -0
- scc_cli/doctor/types.py +66 -0
- scc_cli/evaluation/__init__.py +27 -0
- scc_cli/evaluation/apply_exceptions.py +207 -0
- scc_cli/evaluation/evaluate.py +97 -0
- scc_cli/evaluation/models.py +80 -0
- scc_cli/git.py +84 -0
- scc_cli/json_command.py +166 -0
- scc_cli/json_output.py +159 -0
- scc_cli/kinds.py +65 -0
- scc_cli/marketplace/__init__.py +123 -0
- scc_cli/marketplace/adapter.py +74 -0
- scc_cli/marketplace/compute.py +377 -0
- scc_cli/marketplace/constants.py +87 -0
- scc_cli/marketplace/managed.py +135 -0
- scc_cli/marketplace/materialize.py +846 -0
- scc_cli/marketplace/normalize.py +548 -0
- scc_cli/marketplace/render.py +281 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +506 -0
- scc_cli/marketplace/sync.py +279 -0
- scc_cli/marketplace/team_cache.py +195 -0
- scc_cli/marketplace/team_fetch.py +689 -0
- scc_cli/marketplace/trust.py +244 -0
- scc_cli/models/__init__.py +41 -0
- scc_cli/models/exceptions.py +273 -0
- scc_cli/models/plugin_audit.py +434 -0
- scc_cli/org_templates.py +269 -0
- scc_cli/output_mode.py +167 -0
- scc_cli/panels.py +113 -0
- scc_cli/platform.py +350 -0
- scc_cli/profiles.py +960 -0
- scc_cli/remote.py +443 -0
- scc_cli/schemas/__init__.py +1 -0
- scc_cli/schemas/org-v1.schema.json +456 -0
- scc_cli/schemas/team-config.v1.schema.json +163 -0
- scc_cli/services/__init__.py +1 -0
- scc_cli/services/git/__init__.py +79 -0
- scc_cli/services/git/branch.py +151 -0
- scc_cli/services/git/core.py +216 -0
- scc_cli/services/git/hooks.py +108 -0
- scc_cli/services/git/worktree.py +444 -0
- scc_cli/services/workspace/__init__.py +36 -0
- scc_cli/services/workspace/resolver.py +223 -0
- scc_cli/services/workspace/suspicious.py +200 -0
- scc_cli/sessions.py +425 -0
- scc_cli/setup.py +589 -0
- scc_cli/source_resolver.py +470 -0
- scc_cli/stats.py +378 -0
- scc_cli/stores/__init__.py +13 -0
- scc_cli/stores/exception_store.py +251 -0
- scc_cli/subprocess_utils.py +88 -0
- scc_cli/teams.py +383 -0
- scc_cli/templates/__init__.py +2 -0
- scc_cli/templates/org/__init__.py +0 -0
- scc_cli/templates/org/minimal.json +19 -0
- scc_cli/templates/org/reference.json +74 -0
- scc_cli/templates/org/strict.json +38 -0
- scc_cli/templates/org/teams.json +42 -0
- scc_cli/templates/statusline.sh +75 -0
- scc_cli/theme.py +348 -0
- scc_cli/ui/__init__.py +154 -0
- scc_cli/ui/branding.py +68 -0
- scc_cli/ui/chrome.py +401 -0
- scc_cli/ui/dashboard/__init__.py +62 -0
- scc_cli/ui/dashboard/_dashboard.py +794 -0
- scc_cli/ui/dashboard/loaders.py +452 -0
- scc_cli/ui/dashboard/models.py +185 -0
- scc_cli/ui/dashboard/orchestrator.py +735 -0
- scc_cli/ui/formatters.py +444 -0
- scc_cli/ui/gate.py +350 -0
- scc_cli/ui/git_interactive.py +869 -0
- scc_cli/ui/git_render.py +176 -0
- scc_cli/ui/help.py +157 -0
- scc_cli/ui/keys.py +615 -0
- scc_cli/ui/list_screen.py +437 -0
- scc_cli/ui/picker.py +763 -0
- scc_cli/ui/prompts.py +201 -0
- scc_cli/ui/quick_resume.py +116 -0
- scc_cli/ui/wizard.py +576 -0
- scc_cli/update.py +680 -0
- scc_cli/utils/__init__.py +39 -0
- scc_cli/utils/fixit.py +264 -0
- scc_cli/utils/fuzzy.py +124 -0
- scc_cli/utils/locks.py +114 -0
- scc_cli/utils/ttl.py +376 -0
- scc_cli/validate.py +455 -0
- scc_cli-1.5.3.dist-info/METADATA +401 -0
- scc_cli-1.5.3.dist-info/RECORD +153 -0
- scc_cli-1.5.3.dist-info/WHEEL +4 -0
- scc_cli-1.5.3.dist-info/entry_points.txt +2 -0
- scc_cli-1.5.3.dist-info/licenses/LICENSE +21 -0
scc_cli/ui/prompts.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Simple Rich-based prompts for CLI interactions.
|
|
2
|
+
|
|
3
|
+
This module provides straightforward prompt utilities for user input that don't
|
|
4
|
+
require full TUI screens. For more complex interactive pickers with keyboard
|
|
5
|
+
navigation, see picker.py and wizard.py.
|
|
6
|
+
|
|
7
|
+
Functions:
|
|
8
|
+
render_error: Display an SCCError with user-friendly formatting
|
|
9
|
+
select_session: Interactive session selection from a list
|
|
10
|
+
select_team: Interactive team selection menu
|
|
11
|
+
prompt_custom_workspace: Prompt for custom workspace path
|
|
12
|
+
prompt_repo_url: Prompt for Git repository URL
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from rich import box
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich.prompt import IntPrompt, Prompt
|
|
22
|
+
from rich.table import Table
|
|
23
|
+
|
|
24
|
+
from scc_cli.confirm import Confirm
|
|
25
|
+
from scc_cli.theme import Borders, Colors
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from scc_cli.core.errors import SCCError
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def render_error(console: Console, error: "SCCError", debug: bool = False) -> None:
|
|
32
|
+
"""Render an error with user-friendly formatting.
|
|
33
|
+
|
|
34
|
+
Philosophy: "One message, one action"
|
|
35
|
+
- Display what went wrong (user_message)
|
|
36
|
+
- Display what to do next (suggested_action)
|
|
37
|
+
- Display debug info only if --debug flag is used
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
console: Rich console for output.
|
|
41
|
+
error: The SCCError to render.
|
|
42
|
+
debug: Whether to show debug context.
|
|
43
|
+
"""
|
|
44
|
+
lines = []
|
|
45
|
+
|
|
46
|
+
# Main error message
|
|
47
|
+
lines.append(f"[bold]{error.user_message}[/bold]")
|
|
48
|
+
|
|
49
|
+
# Suggested action (if available)
|
|
50
|
+
if error.suggested_action:
|
|
51
|
+
lines.append("")
|
|
52
|
+
lines.append(f"[{Colors.SECONDARY}]->[/{Colors.SECONDARY}] {error.suggested_action}")
|
|
53
|
+
|
|
54
|
+
# Debug context (only with --debug)
|
|
55
|
+
if debug and error.debug_context:
|
|
56
|
+
lines.append("")
|
|
57
|
+
lines.append(f"[{Colors.SECONDARY}]--- Debug Info ---[/{Colors.SECONDARY}]")
|
|
58
|
+
lines.append(f"[{Colors.SECONDARY}]{error.debug_context}[/{Colors.SECONDARY}]")
|
|
59
|
+
elif error.debug_context and not debug:
|
|
60
|
+
lines.append("")
|
|
61
|
+
lines.append(
|
|
62
|
+
f"[{Colors.SECONDARY}]Run with --debug for technical details[/{Colors.SECONDARY}]"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Create panel with error styling
|
|
66
|
+
panel = Panel(
|
|
67
|
+
"\n".join(lines),
|
|
68
|
+
title=f"[{Colors.ERROR_BOLD}]Error[/{Colors.ERROR_BOLD}]",
|
|
69
|
+
border_style=Borders.PANEL_ERROR,
|
|
70
|
+
padding=(0, 1),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
console.print()
|
|
74
|
+
console.print(panel)
|
|
75
|
+
console.print()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def select_session(console: Console, sessions_list: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
79
|
+
"""Display an interactive session selection menu.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
console: Rich console for output.
|
|
83
|
+
sessions_list: List of session dicts with 'name', 'workspace', 'last_used', etc.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Selected session dict or None if cancelled.
|
|
87
|
+
"""
|
|
88
|
+
if not sessions_list:
|
|
89
|
+
console.print(f"[{Colors.WARNING}]No sessions available.[/{Colors.WARNING}]")
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
console.print(f"\n[{Colors.BRAND_BOLD}]Select a session:[/{Colors.BRAND_BOLD}]\n")
|
|
93
|
+
|
|
94
|
+
table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
|
|
95
|
+
table.add_column("Option", style=Colors.WARNING, width=4)
|
|
96
|
+
table.add_column("Name", style=Colors.BRAND)
|
|
97
|
+
table.add_column("Workspace", style=Colors.PRIMARY)
|
|
98
|
+
table.add_column("Last Used", style=Colors.SECONDARY)
|
|
99
|
+
|
|
100
|
+
for i, session in enumerate(sessions_list, 1):
|
|
101
|
+
name = session.get("name", "-")
|
|
102
|
+
workspace = session.get("workspace", "-")
|
|
103
|
+
last_used = session.get("last_used", "-")
|
|
104
|
+
table.add_row(f"[{i}]", name, workspace, last_used)
|
|
105
|
+
|
|
106
|
+
table.add_row("[0]", "<- Cancel", "", "")
|
|
107
|
+
|
|
108
|
+
console.print(table)
|
|
109
|
+
|
|
110
|
+
valid_choices = [str(i) for i in range(0, len(sessions_list) + 1)]
|
|
111
|
+
choice = IntPrompt.ask(
|
|
112
|
+
f"\n[{Colors.BRAND}]Select session[/{Colors.BRAND}]",
|
|
113
|
+
default=1,
|
|
114
|
+
choices=valid_choices,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if choice == 0:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
return sessions_list[choice - 1]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def select_team(console: Console, cfg: dict[str, Any]) -> str | None:
|
|
124
|
+
"""Display an interactive team selection menu and return the chosen team.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
console: Rich console for output.
|
|
128
|
+
cfg: Configuration dict containing 'profiles' key with team definitions.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Selected team name or None if no teams available.
|
|
132
|
+
"""
|
|
133
|
+
teams: dict[str, Any] = cfg.get("profiles", {})
|
|
134
|
+
team_list: list[str] = list(teams.keys())
|
|
135
|
+
|
|
136
|
+
if not team_list:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
console.print(f"\n[{Colors.BRAND_BOLD}]Select your team:[/{Colors.BRAND_BOLD}]\n")
|
|
140
|
+
|
|
141
|
+
table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
|
|
142
|
+
table.add_column("Option", style=Colors.WARNING, width=4)
|
|
143
|
+
table.add_column("Team", style=Colors.BRAND)
|
|
144
|
+
table.add_column("Description", style=Colors.PRIMARY)
|
|
145
|
+
|
|
146
|
+
for i, team_name in enumerate(team_list, 1):
|
|
147
|
+
team_info = teams[team_name]
|
|
148
|
+
desc = team_info.get("description", "")
|
|
149
|
+
table.add_row(f"[{i}]", team_name, desc)
|
|
150
|
+
|
|
151
|
+
console.print(table)
|
|
152
|
+
|
|
153
|
+
choice = IntPrompt.ask(
|
|
154
|
+
f"\n[{Colors.BRAND}]Select team[/{Colors.BRAND}]",
|
|
155
|
+
default=1,
|
|
156
|
+
choices=[str(i) for i in range(1, len(team_list) + 1)],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
selected = team_list[choice - 1]
|
|
160
|
+
console.print(f"\n[{Colors.SUCCESS}]Selected: {selected}[/{Colors.SUCCESS}]")
|
|
161
|
+
|
|
162
|
+
return selected
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def prompt_custom_workspace(console: Console) -> str | None:
|
|
166
|
+
"""Prompt the user to enter a custom workspace path.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
console: Rich console for output.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Resolved absolute path string, or None if cancelled or path invalid.
|
|
173
|
+
"""
|
|
174
|
+
path = Prompt.ask(f"\n[{Colors.BRAND}]Enter workspace path[/{Colors.BRAND}]")
|
|
175
|
+
|
|
176
|
+
if not path:
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
expanded = Path(path).expanduser().resolve()
|
|
180
|
+
|
|
181
|
+
if not expanded.exists():
|
|
182
|
+
console.print(f"[{Colors.ERROR}]Path does not exist: {expanded}[/{Colors.ERROR}]")
|
|
183
|
+
if Confirm.ask(f"[{Colors.BRAND}]Create this directory?[/{Colors.BRAND}]", default=False):
|
|
184
|
+
expanded.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
return str(expanded)
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
return str(expanded)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def prompt_repo_url(console: Console) -> str:
|
|
192
|
+
"""Prompt the user to enter a Git repository URL.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
console: Rich console for output.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
The entered URL string (may be empty if user pressed Enter).
|
|
199
|
+
"""
|
|
200
|
+
url = Prompt.ask(f"\n[{Colors.BRAND}]Repository URL (HTTPS or SSH)[/{Colors.BRAND}]")
|
|
201
|
+
return url
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Quick Resume gating and filtering.
|
|
2
|
+
|
|
3
|
+
Centralizes Quick Resume logic to prevent drift between entry points.
|
|
4
|
+
|
|
5
|
+
Policy (explicit):
|
|
6
|
+
- Show QR only if:
|
|
7
|
+
- Interactive allowed (TTY, not --json, not --non-interactive)
|
|
8
|
+
- None of --resume, --select, --fresh set
|
|
9
|
+
- If --interactive is set => wizard only, NO QR (force wizard bypasses QR)
|
|
10
|
+
- Sessions exist for WR
|
|
11
|
+
|
|
12
|
+
- QR selection list filtering:
|
|
13
|
+
- Filter by workspace_root == WR (only sessions for this workspace)
|
|
14
|
+
- AND team scoping: --team X shows only team X; standalone shows only team=None
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from scc_cli.contexts import WorkContext
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def should_show_quick_resume(
|
|
27
|
+
*,
|
|
28
|
+
json_mode: bool = False,
|
|
29
|
+
non_interactive: bool = False,
|
|
30
|
+
resume: bool = False,
|
|
31
|
+
select: bool = False,
|
|
32
|
+
fresh: bool = False,
|
|
33
|
+
interactive_flag: bool = False,
|
|
34
|
+
) -> bool:
|
|
35
|
+
"""Determine if Quick Resume picker should be shown.
|
|
36
|
+
|
|
37
|
+
Returns False if any bypass condition is met.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
json_mode: --json flag set
|
|
41
|
+
non_interactive: --non-interactive flag set
|
|
42
|
+
resume: --resume flag set
|
|
43
|
+
select: --select flag set
|
|
44
|
+
fresh: --fresh flag set
|
|
45
|
+
interactive_flag: --interactive flag set (forces wizard, bypasses QR)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
True if QR should be shown, False otherwise
|
|
49
|
+
"""
|
|
50
|
+
# Non-interactive modes never show QR
|
|
51
|
+
if json_mode or non_interactive:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
# Explicit flags bypass QR
|
|
55
|
+
if resume or select or fresh:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
# --interactive forces wizard, bypasses QR
|
|
59
|
+
if interactive_flag:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def load_contexts_for_workspace_and_team(
|
|
66
|
+
workspace_root: Path,
|
|
67
|
+
team: str | None,
|
|
68
|
+
limit: int = 10,
|
|
69
|
+
) -> list[WorkContext]:
|
|
70
|
+
"""Load contexts filtered by workspace and team.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
workspace_root: Only return contexts matching this workspace
|
|
74
|
+
team: Team filter:
|
|
75
|
+
- None: return only standalone contexts (team=None)
|
|
76
|
+
- str: return only contexts matching this team
|
|
77
|
+
limit: Maximum number of contexts to return
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
List of WorkContext objects matching filters
|
|
81
|
+
"""
|
|
82
|
+
from scc_cli.contexts import load_recent_contexts
|
|
83
|
+
|
|
84
|
+
# Load all recent contexts
|
|
85
|
+
all_contexts = load_recent_contexts(limit=limit * 3) # Load extra for filtering
|
|
86
|
+
|
|
87
|
+
filtered = []
|
|
88
|
+
for ctx in all_contexts:
|
|
89
|
+
# Filter by workspace
|
|
90
|
+
ctx_workspace = Path(ctx.repo_root) if ctx.repo_root else None
|
|
91
|
+
if ctx_workspace is None:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Resolve both paths for comparison
|
|
95
|
+
try:
|
|
96
|
+
if ctx_workspace.resolve() != workspace_root.resolve():
|
|
97
|
+
continue
|
|
98
|
+
except OSError:
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
# Filter by team
|
|
102
|
+
if team is None:
|
|
103
|
+
# Standalone mode: only show contexts with no team
|
|
104
|
+
if ctx.team is not None:
|
|
105
|
+
continue
|
|
106
|
+
else:
|
|
107
|
+
# Team mode: only show contexts matching this team
|
|
108
|
+
if ctx.team != team:
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
filtered.append(ctx)
|
|
112
|
+
|
|
113
|
+
if len(filtered) >= limit:
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
return filtered
|