wup 0.1.8__tar.gz → 0.1.10__tar.gz
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.
- {wup-0.1.8/wup.egg-info → wup-0.1.10}/PKG-INFO +5 -5
- {wup-0.1.8 → wup-0.1.10}/README.md +4 -4
- {wup-0.1.8 → wup-0.1.10}/pyproject.toml +1 -1
- {wup-0.1.8 → wup-0.1.10}/wup/__init__.py +1 -1
- {wup-0.1.8 → wup-0.1.10}/wup/core.py +31 -32
- wup-0.1.10/wup/testql_watcher.py +247 -0
- {wup-0.1.8 → wup-0.1.10/wup.egg-info}/PKG-INFO +5 -5
- {wup-0.1.8 → wup-0.1.10}/wup.egg-info/SOURCES.txt +1 -0
- {wup-0.1.8 → wup-0.1.10}/LICENSE +0 -0
- {wup-0.1.8 → wup-0.1.10}/setup.cfg +0 -0
- {wup-0.1.8 → wup-0.1.10}/tests/test_wup.py +0 -0
- {wup-0.1.8 → wup-0.1.10}/wup/cli.py +0 -0
- {wup-0.1.8 → wup-0.1.10}/wup/dependency_mapper.py +0 -0
- {wup-0.1.8 → wup-0.1.10}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.1.8 → wup-0.1.10}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.1.8 → wup-0.1.10}/wup.egg-info/requires.txt +0 -0
- {wup-0.1.8 → wup-0.1.10}/wup.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wup
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.10
|
|
4
4
|
Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -28,17 +28,17 @@ Dynamic: license-file
|
|
|
28
28
|
|
|
29
29
|
## AI Cost Tracking
|
|
30
30
|
|
|
31
|
-
    
|
|
32
|
+
  
|
|
33
33
|
|
|
34
|
-
- 🤖 **LLM usage:** $0.
|
|
34
|
+
- 🤖 **LLM usage:** $0.6000 (4 commits)
|
|
35
35
|
- 👤 **Human dev:** ~$200 (2.0h @ $100/h, 30min dedup)
|
|
36
36
|
|
|
37
37
|
Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
-
    
|
|
42
42
|
|
|
43
43
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
44
44
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $0.
|
|
9
|
+
- 🤖 **LLM usage:** $0.6000 (4 commits)
|
|
10
10
|
- 👤 **Human dev:** ~$200 (2.0h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
    
|
|
17
17
|
|
|
18
18
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
19
19
|
|
|
@@ -7,7 +7,7 @@ WUP monitors file changes and runs intelligent regression tests using a 3-layer
|
|
|
7
7
|
3. Detail Layer: Full tests with blame reports (only on failure)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
__version__ = "0.1.
|
|
10
|
+
__version__ = "0.1.10"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .core import WupWatcher
|
|
@@ -67,6 +67,13 @@ class WupWatcher:
|
|
|
67
67
|
self.console.print(f"[yellow]Building dependency map...[/yellow]")
|
|
68
68
|
self.dependency_mapper.build_from_codebase()
|
|
69
69
|
self.dependency_mapper.save(deps_file)
|
|
70
|
+
|
|
71
|
+
def _to_relative_path(self, file_path: str) -> Path:
|
|
72
|
+
file_path_obj = Path(file_path)
|
|
73
|
+
try:
|
|
74
|
+
return file_path_obj.relative_to(self.project_root)
|
|
75
|
+
except ValueError:
|
|
76
|
+
return file_path_obj
|
|
70
77
|
|
|
71
78
|
def infer_service(self, file_path: str) -> Optional[str]:
|
|
72
79
|
"""
|
|
@@ -76,7 +83,7 @@ class WupWatcher:
|
|
|
76
83
|
app/users/routes.py → "app/users"
|
|
77
84
|
src/components/auth.ts → "src/components"
|
|
78
85
|
"""
|
|
79
|
-
rel_path =
|
|
86
|
+
rel_path = self._to_relative_path(file_path)
|
|
80
87
|
parts = rel_path.parts
|
|
81
88
|
|
|
82
89
|
# Use dependency mapper if available
|
|
@@ -112,10 +119,8 @@ class WupWatcher:
|
|
|
112
119
|
service: Service name to test
|
|
113
120
|
"""
|
|
114
121
|
endpoints = self.dependency_mapper.get_endpoints_for_service(service)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
self.test_queue.append(("quick", service, endpoints[:3]))
|
|
118
|
-
self.last_test_times[service] = time.time()
|
|
122
|
+
self.test_queue.append(("quick", service, endpoints[:3]))
|
|
123
|
+
self.last_test_times[service] = time.time()
|
|
119
124
|
|
|
120
125
|
def schedule_detail_test(self, service: str):
|
|
121
126
|
"""
|
|
@@ -125,8 +130,23 @@ class WupWatcher:
|
|
|
125
130
|
service: Service name to test
|
|
126
131
|
"""
|
|
127
132
|
endpoints = self.dependency_mapper.get_endpoints_for_service(service)
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
self.test_queue.appendleft(("detail", service, endpoints))
|
|
134
|
+
|
|
135
|
+
async def process_test_queue_once(self):
|
|
136
|
+
if not self.test_queue or not await self.cpu_ok():
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
test_type, service, endpoints = self.test_queue.popleft()
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
if test_type == "quick":
|
|
143
|
+
passed = await self.run_quick_test(service, endpoints)
|
|
144
|
+
if not passed:
|
|
145
|
+
self.schedule_detail_test(service)
|
|
146
|
+
elif test_type == "detail":
|
|
147
|
+
await self.run_detail_test(service, endpoints)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
self.console.print(f"[red]Error testing {service}: {e}[/red]")
|
|
130
150
|
|
|
131
151
|
async def cpu_ok(self) -> bool:
|
|
132
152
|
"""
|
|
@@ -212,20 +232,7 @@ class WupWatcher:
|
|
|
212
232
|
async def test_loop(self):
|
|
213
233
|
"""Main test execution loop."""
|
|
214
234
|
while True:
|
|
215
|
-
|
|
216
|
-
test_type, service, endpoints = self.test_queue.popleft()
|
|
217
|
-
|
|
218
|
-
try:
|
|
219
|
-
if test_type == "quick":
|
|
220
|
-
passed = await self.run_quick_test(service, endpoints)
|
|
221
|
-
if not passed:
|
|
222
|
-
# Escalate to detail test
|
|
223
|
-
self.schedule_detail_test(service)
|
|
224
|
-
elif test_type == "detail":
|
|
225
|
-
await self.run_detail_test(service, endpoints)
|
|
226
|
-
except Exception as e:
|
|
227
|
-
self.console.print(f"[red]Error testing {service}: {e}[/red]")
|
|
228
|
-
|
|
235
|
+
await self.process_test_queue_once()
|
|
229
236
|
await asyncio.sleep(self.debounce_seconds)
|
|
230
237
|
|
|
231
238
|
def on_file_change(self, file_path: str):
|
|
@@ -236,7 +243,7 @@ class WupWatcher:
|
|
|
236
243
|
file_path: Path to the changed file
|
|
237
244
|
"""
|
|
238
245
|
# Only watch relevant directories
|
|
239
|
-
rel_path =
|
|
246
|
+
rel_path = self._to_relative_path(file_path)
|
|
240
247
|
parts = rel_path.parts
|
|
241
248
|
|
|
242
249
|
# Skip certain directories
|
|
@@ -284,6 +291,7 @@ class WupWatcher:
|
|
|
284
291
|
|
|
285
292
|
try:
|
|
286
293
|
while True:
|
|
294
|
+
asyncio.run(self.process_test_queue_once())
|
|
287
295
|
time.sleep(1)
|
|
288
296
|
except KeyboardInterrupt:
|
|
289
297
|
observer.stop()
|
|
@@ -343,16 +351,7 @@ class WupWatcher:
|
|
|
343
351
|
with Live(self.create_status_table(), refresh_per_second=1) as live:
|
|
344
352
|
try:
|
|
345
353
|
while True:
|
|
346
|
-
|
|
347
|
-
if self.test_queue and await self.cpu_ok():
|
|
348
|
-
test_type, service, endpoints = self.test_queue.popleft()
|
|
349
|
-
|
|
350
|
-
if test_type == "quick":
|
|
351
|
-
passed = await self.run_quick_test(service, endpoints)
|
|
352
|
-
if not passed:
|
|
353
|
-
self.schedule_detail_test(service)
|
|
354
|
-
elif test_type == "detail":
|
|
355
|
-
await self.run_detail_test(service, endpoints)
|
|
354
|
+
await self.process_test_queue_once()
|
|
356
355
|
|
|
357
356
|
live.update(self.create_status_table())
|
|
358
357
|
await asyncio.sleep(1)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""TestQL integration for WUP watcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional, Sequence
|
|
12
|
+
from urllib import error, request
|
|
13
|
+
|
|
14
|
+
from .core import WupWatcher
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BrowserNotifier:
|
|
18
|
+
"""Send watcher events to browser-facing service and local file."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, service_url: Optional[str], events_file: Path):
|
|
21
|
+
self.service_url = service_url
|
|
22
|
+
self.events_file = events_file
|
|
23
|
+
self.events_file.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
def notify(self, payload: Dict) -> None:
|
|
26
|
+
payload_with_ts = {"timestamp": int(time.time()), **payload}
|
|
27
|
+
self.events_file.write_text(json.dumps(payload_with_ts, indent=2), encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
if not self.service_url:
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
body = json.dumps(payload_with_ts).encode("utf-8")
|
|
33
|
+
req = request.Request(
|
|
34
|
+
self.service_url,
|
|
35
|
+
data=body,
|
|
36
|
+
headers={"Content-Type": "application/json"},
|
|
37
|
+
method="POST",
|
|
38
|
+
)
|
|
39
|
+
try:
|
|
40
|
+
with request.urlopen(req, timeout=5):
|
|
41
|
+
return
|
|
42
|
+
except (error.URLError, TimeoutError):
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestQLWatcher(WupWatcher):
|
|
47
|
+
"""WUP watcher running selective TestQL scenarios for changed services."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
project_root: str,
|
|
52
|
+
scenarios_dir: str = "testql-scenarios",
|
|
53
|
+
testql_bin: str = "testql",
|
|
54
|
+
track_dir: str = ".wup/tracks",
|
|
55
|
+
browser_service_url: Optional[str] = None,
|
|
56
|
+
quick_limit: int = 3,
|
|
57
|
+
**kwargs,
|
|
58
|
+
):
|
|
59
|
+
super().__init__(project_root=project_root, **kwargs)
|
|
60
|
+
self.scenarios_dir = self.project_root / scenarios_dir
|
|
61
|
+
self.testql_bin = testql_bin
|
|
62
|
+
self.quick_limit = quick_limit
|
|
63
|
+
self.track_dir = self.project_root / track_dir
|
|
64
|
+
self.track_dir.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
self.browser_notifier = BrowserNotifier(
|
|
66
|
+
service_url=browser_service_url,
|
|
67
|
+
events_file=self.project_root / ".wup" / "browser-events" / "latest.json",
|
|
68
|
+
)
|
|
69
|
+
self.last_track_path: Optional[Path] = None
|
|
70
|
+
|
|
71
|
+
def _tokenize_service(self, service: str) -> List[str]:
|
|
72
|
+
raw_tokens = re.split(r"[^a-zA-Z0-9]+", service.lower())
|
|
73
|
+
return [token for token in raw_tokens if len(token) >= 3]
|
|
74
|
+
|
|
75
|
+
def _discover_scenarios(self) -> List[Path]:
|
|
76
|
+
if not self.scenarios_dir.exists():
|
|
77
|
+
return []
|
|
78
|
+
return sorted(self.scenarios_dir.rglob("*.testql.toon.yaml"))
|
|
79
|
+
|
|
80
|
+
def _select_scenarios_for_service(self, service: str) -> List[Path]:
|
|
81
|
+
all_scenarios = self._discover_scenarios()
|
|
82
|
+
if not all_scenarios:
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
tokens = self._tokenize_service(service)
|
|
86
|
+
scored: List[tuple[int, Path]] = []
|
|
87
|
+
|
|
88
|
+
for scenario in all_scenarios:
|
|
89
|
+
name = scenario.name.lower()
|
|
90
|
+
score = 0
|
|
91
|
+
if any(token in name for token in tokens):
|
|
92
|
+
score += 3
|
|
93
|
+
if "api" in name or "endpoint" in name:
|
|
94
|
+
score += 2
|
|
95
|
+
if "infra" in name or "smoke" in name:
|
|
96
|
+
score += 1
|
|
97
|
+
scored.append((score, scenario))
|
|
98
|
+
|
|
99
|
+
scored.sort(key=lambda item: (item[0], item[1].name), reverse=True)
|
|
100
|
+
selected = [scenario for score, scenario in scored if score > 0]
|
|
101
|
+
|
|
102
|
+
if selected:
|
|
103
|
+
return selected
|
|
104
|
+
|
|
105
|
+
return all_scenarios[: self.quick_limit]
|
|
106
|
+
|
|
107
|
+
def _run_testql(self, args: Sequence[str], timeout: int) -> subprocess.CompletedProcess:
|
|
108
|
+
cmd = [self.testql_bin, *args]
|
|
109
|
+
try:
|
|
110
|
+
return subprocess.run(
|
|
111
|
+
cmd,
|
|
112
|
+
cwd=str(self.project_root),
|
|
113
|
+
capture_output=True,
|
|
114
|
+
text=True,
|
|
115
|
+
timeout=timeout,
|
|
116
|
+
)
|
|
117
|
+
except FileNotFoundError:
|
|
118
|
+
fallback_cmd = ["python3", "-m", "testql.cli", *args]
|
|
119
|
+
return subprocess.run(
|
|
120
|
+
fallback_cmd,
|
|
121
|
+
cwd=str(self.project_root),
|
|
122
|
+
capture_output=True,
|
|
123
|
+
text=True,
|
|
124
|
+
timeout=timeout,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def _write_track(self, *, service: str, stage: str, scenario: Optional[Path], result: subprocess.CompletedProcess) -> Path:
|
|
128
|
+
ts = int(time.time())
|
|
129
|
+
safe_service = service.replace("/", "_").replace("\\", "_")
|
|
130
|
+
scenario_name = scenario.name if scenario else "unknown"
|
|
131
|
+
stderr_line = (result.stderr or "").strip().splitlines()[:1]
|
|
132
|
+
stdout_line = (result.stdout or "").strip().splitlines()[:1]
|
|
133
|
+
|
|
134
|
+
payload = {
|
|
135
|
+
"service": service,
|
|
136
|
+
"stage": stage,
|
|
137
|
+
"scenario": str(scenario) if scenario else None,
|
|
138
|
+
"command": result.args,
|
|
139
|
+
"returncode": result.returncode,
|
|
140
|
+
"stderr_head": stderr_line[0] if stderr_line else "",
|
|
141
|
+
"stdout_head": stdout_line[0] if stdout_line else "",
|
|
142
|
+
"track": {
|
|
143
|
+
"file": str(scenario) if scenario else "",
|
|
144
|
+
"line": 1,
|
|
145
|
+
"hint": scenario_name,
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
track_path = self.track_dir / f"{ts}_{safe_service}_{stage}.json"
|
|
150
|
+
track_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
151
|
+
self.last_track_path = track_path
|
|
152
|
+
|
|
153
|
+
self.browser_notifier.notify(
|
|
154
|
+
{
|
|
155
|
+
"type": "wup_testql_error",
|
|
156
|
+
"service": service,
|
|
157
|
+
"stage": stage,
|
|
158
|
+
"track_file": str(track_path),
|
|
159
|
+
"scenario": str(scenario) if scenario else None,
|
|
160
|
+
"message": payload["stderr_head"] or payload["stdout_head"] or "TestQL command failed",
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
return track_path
|
|
164
|
+
|
|
165
|
+
async def run_quick_test(self, service: str, endpoints: List[str]) -> bool:
|
|
166
|
+
scenarios = self._select_scenarios_for_service(service)[: self.quick_limit]
|
|
167
|
+
if not scenarios:
|
|
168
|
+
self.console.print(f"[yellow]⚠ No TestQL scenarios found for {service}[/yellow]")
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
self.console.print(
|
|
172
|
+
f"[cyan]🧪 Quick TestQL for {service} ({len(scenarios)} scenarios / {len(endpoints)} endpoints)[/cyan]"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
for scenario in scenarios:
|
|
176
|
+
result = self._run_testql(["run", str(scenario), "--dry-run"], timeout=60)
|
|
177
|
+
if result.returncode != 0:
|
|
178
|
+
track_path = self._write_track(
|
|
179
|
+
service=service,
|
|
180
|
+
stage="quick",
|
|
181
|
+
scenario=scenario,
|
|
182
|
+
result=result,
|
|
183
|
+
)
|
|
184
|
+
self.console.print(
|
|
185
|
+
f"[red]✗ Quick failed: {scenario.name} | track: {track_path}[/red]"
|
|
186
|
+
)
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
self.console.print(f"[green]✓ Quick TestQL passed for {service}[/green]")
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
async def run_detail_test(self, service: str, endpoints: List[str]) -> Dict:
|
|
193
|
+
scenarios = self._select_scenarios_for_service(service)
|
|
194
|
+
results = {
|
|
195
|
+
"service": service,
|
|
196
|
+
"total_scenarios": len(scenarios),
|
|
197
|
+
"passed": 0,
|
|
198
|
+
"failed": 0,
|
|
199
|
+
"failed_scenarios": [],
|
|
200
|
+
"track_files": [],
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
self.console.print(
|
|
204
|
+
f"[cyan]🔍 Detail TestQL for {service} ({len(scenarios)} scenarios / {len(endpoints)} endpoints)[/cyan]"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
for scenario in scenarios:
|
|
208
|
+
result = self._run_testql(["run", str(scenario), "--output", "json"], timeout=180)
|
|
209
|
+
if result.returncode == 0:
|
|
210
|
+
results["passed"] += 1
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
results["failed"] += 1
|
|
214
|
+
results["failed_scenarios"].append(str(scenario))
|
|
215
|
+
track_path = self._write_track(
|
|
216
|
+
service=service,
|
|
217
|
+
stage="detail",
|
|
218
|
+
scenario=scenario,
|
|
219
|
+
result=result,
|
|
220
|
+
)
|
|
221
|
+
results["track_files"].append(str(track_path))
|
|
222
|
+
self.console.print(
|
|
223
|
+
f"[red]✗ Detail failed: {scenario.name} | track: {track_path}[/red]"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if results["failed"] == 0:
|
|
227
|
+
self.console.print(
|
|
228
|
+
f"[green]✓ Detail TestQL passed for {service} ({results['passed']} scenarios)[/green]"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return results
|
|
232
|
+
|
|
233
|
+
async def process_changed_file_once(self, file_path: str) -> Dict:
|
|
234
|
+
self.on_file_change(file_path)
|
|
235
|
+
|
|
236
|
+
processed = 0
|
|
237
|
+
while self.test_queue and processed < 4:
|
|
238
|
+
await self.process_test_queue_once()
|
|
239
|
+
processed += 1
|
|
240
|
+
await asyncio.sleep(0)
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
"file": file_path,
|
|
244
|
+
"processed_items": processed,
|
|
245
|
+
"remaining_queue": len(self.test_queue),
|
|
246
|
+
"last_track_path": str(self.last_track_path) if self.last_track_path else None,
|
|
247
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wup
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.10
|
|
4
4
|
Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -28,17 +28,17 @@ Dynamic: license-file
|
|
|
28
28
|
|
|
29
29
|
## AI Cost Tracking
|
|
30
30
|
|
|
31
|
-
    
|
|
32
|
+
  
|
|
33
33
|
|
|
34
|
-
- 🤖 **LLM usage:** $0.
|
|
34
|
+
- 🤖 **LLM usage:** $0.6000 (4 commits)
|
|
35
35
|
- 👤 **Human dev:** ~$200 (2.0h @ $100/h, 30min dedup)
|
|
36
36
|
|
|
37
37
|
Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
-
    
|
|
42
42
|
|
|
43
43
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
44
44
|
|
{wup-0.1.8 → wup-0.1.10}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|