devcoach 0.1.0__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.
Files changed (43) hide show
  1. devcoach/SKILL.md +288 -0
  2. devcoach/__init__.py +3 -0
  3. devcoach/cli/__init__.py +0 -0
  4. devcoach/cli/commands.py +793 -0
  5. devcoach/core/__init__.py +0 -0
  6. devcoach/core/coach.py +141 -0
  7. devcoach/core/db.py +768 -0
  8. devcoach/core/detect.py +132 -0
  9. devcoach/core/git.py +97 -0
  10. devcoach/core/models.py +104 -0
  11. devcoach/core/prompts.py +52 -0
  12. devcoach/mcp/__init__.py +0 -0
  13. devcoach/mcp/server.py +545 -0
  14. devcoach/web/__init__.py +0 -0
  15. devcoach/web/app.py +319 -0
  16. devcoach/web/static/favicon.svg +3 -0
  17. devcoach/web/static/relative-time.js +24 -0
  18. devcoach/web/static/style.css +163 -0
  19. devcoach/web/static/vendor/alpinejs.min.js +5 -0
  20. devcoach/web/static/vendor/flatpickr-dark.min.css +795 -0
  21. devcoach/web/static/vendor/flatpickr.min.css +13 -0
  22. devcoach/web/static/vendor/flatpickr.min.js +2 -0
  23. devcoach/web/static/vendor/highlight.min.js +1232 -0
  24. devcoach/web/static/vendor/hljs-dark.min.css +1 -0
  25. devcoach/web/static/vendor/hljs-light.min.css +1 -0
  26. devcoach/web/static/vendor/htmx.min.js +1 -0
  27. devcoach/web/static/vendor/icons/bitbucket.svg +1 -0
  28. devcoach/web/static/vendor/icons/github.svg +1 -0
  29. devcoach/web/static/vendor/icons/gitlab.svg +1 -0
  30. devcoach/web/static/vendor/icons/vscode.svg +41 -0
  31. devcoach/web/static/vendor/marked.min.js +6 -0
  32. devcoach/web/static/vendor/tailwind.js +83 -0
  33. devcoach/web/templates/base.html +80 -0
  34. devcoach/web/templates/lesson_detail.html +215 -0
  35. devcoach/web/templates/lessons.html +546 -0
  36. devcoach/web/templates/profile.html +240 -0
  37. devcoach/web/templates/settings.html +144 -0
  38. devcoach-0.1.0.dist-info/METADATA +443 -0
  39. devcoach-0.1.0.dist-info/RECORD +43 -0
  40. devcoach-0.1.0.dist-info/WHEEL +4 -0
  41. devcoach-0.1.0.dist-info/entry_points.txt +2 -0
  42. devcoach-0.1.0.dist-info/licenses/LICENSE +201 -0
  43. devcoach-0.1.0.dist-info/licenses/NOTICE +20 -0
@@ -0,0 +1,80 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{% block title %}devcoach{% endblock %}</title>
7
+ <link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
8
+
9
+ <!-- Apply theme before first paint to avoid flash -->
10
+ <script>
11
+ (function () {
12
+ var stored = localStorage.getItem('theme');
13
+ var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
14
+ var dark = stored ? stored === 'dark' : prefersDark;
15
+ if (dark) document.documentElement.classList.add('dark');
16
+ })();
17
+ </script>
18
+
19
+ <script src="/static/vendor/tailwind.js"></script>
20
+ <!-- Config must come immediately after the CDN so it's set before DOMContentLoaded -->
21
+ <script>
22
+ tailwind.config = { darkMode: 'class' }
23
+ </script>
24
+ <script src="/static/vendor/htmx.min.js"></script>
25
+ <script src="/static/vendor/alpinejs.min.js" defer></script>
26
+ <link rel="stylesheet" href="/static/style.css" />
27
+ {% block head %}{% endblock %}
28
+ </head>
29
+ <body class="bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 min-h-screen font-mono transition-colors duration-200">
30
+
31
+ <nav class="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 px-6 py-3 flex items-center gap-6">
32
+ <span class="text-indigo-600 dark:text-indigo-400 font-bold text-lg">🎓 devcoach</span>
33
+ <a href="/" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition {% if request.url.path == '/' %}text-gray-900 dark:text-white font-semibold{% endif %}">Profile</a>
34
+ <a href="/lessons" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition {% if '/lessons' in request.url.path %}text-gray-900 dark:text-white font-semibold{% endif %}">Lessons</a>
35
+ <a href="/settings" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition {% if request.url.path == '/settings' %}text-gray-900 dark:text-white font-semibold{% endif %}">Settings</a>
36
+ <div class="ml-auto">
37
+ <button id="theme-toggle" onclick="toggleTheme()"
38
+ class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-200 transition text-lg leading-none px-1"
39
+ title="Toggle theme">
40
+ <!-- icon set by JS below -->
41
+ </button>
42
+ </div>
43
+ </nav>
44
+
45
+ <main class="max-w-7xl mx-auto px-4 sm:px-6 py-8">
46
+ {% block content %}{% endblock %}
47
+ </main>
48
+
49
+ <script>
50
+ function isDark() {
51
+ return document.documentElement.classList.contains('dark');
52
+ }
53
+
54
+ function updateThemeIcon() {
55
+ var btn = document.getElementById('theme-toggle');
56
+ if (btn) btn.textContent = isDark() ? '☀️' : '🌙';
57
+ }
58
+
59
+ function updateHljsTheme() {
60
+ var link = document.getElementById('hljs-theme');
61
+ if (!link) return;
62
+ link.href = isDark()
63
+ ? '/static/vendor/hljs-dark.min.css'
64
+ : '/static/vendor/hljs-light.min.css';
65
+ }
66
+
67
+ function toggleTheme() {
68
+ document.documentElement.classList.toggle('dark');
69
+ localStorage.setItem('theme', isDark() ? 'dark' : 'light');
70
+ updateThemeIcon();
71
+ updateHljsTheme();
72
+ }
73
+
74
+ updateThemeIcon();
75
+ updateHljsTheme();
76
+ </script>
77
+
78
+ {% block scripts %}{% endblock %}
79
+ </body>
80
+ </html>
@@ -0,0 +1,215 @@
1
+ {% extends "base.html" %}
2
+ {% block title %}{{ lesson.title }} — devcoach{% endblock %}
3
+
4
+ {% block head %}
5
+ <link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />
6
+ {% endblock %}
7
+
8
+ {% block content %}
9
+ <div class="mb-4">
10
+ <a href="/lessons" class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white text-sm transition">← Back to lessons</a>
11
+ </div>
12
+
13
+ {% if lesson.level == 'junior' %}
14
+ {% set level_class = 'text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700' %}
15
+ {% elif lesson.level == 'mid' %}
16
+ {% set level_class = 'text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700' %}
17
+ {% else %}
18
+ {% set level_class = 'text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700' %}
19
+ {% endif %}
20
+
21
+ <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
22
+
23
+ <!-- Row 1: star + title + level badge -->
24
+ <div class="flex flex-wrap items-center gap-3 mb-2">
25
+ <form method="post" action="/lessons/{{ lesson.id }}/star">
26
+ <input type="hidden" name="next" value="/lessons/{{ lesson.id }}" />
27
+ <button type="submit" title="{% if lesson.starred %}Unstar{% else %}Star{% endif %}"
28
+ class="w-6 text-center text-xl leading-none transition {% if lesson.starred %}text-yellow-400 hover:text-yellow-300{% else %}text-gray-300 dark:text-gray-600 hover:text-yellow-400{% endif %}">
29
+ {% if lesson.starred %}★{% else %}☆{% endif %}
30
+ </button>
31
+ </form>
32
+ <h1 class="text-lg font-bold text-gray-900 dark:text-white flex-1 min-w-0">{{ lesson.title }}</h1>
33
+ <a href="/lessons?level={{ lesson.level }}"
34
+ class="text-xs font-semibold px-2 py-0.5 rounded-full border {{ level_class }} shrink-0 hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">{{ lesson.level }}</a>
35
+ </div>
36
+
37
+ <!-- Row 2: date + topic + tags + feedback result (all inline) -->
38
+ <div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400 mb-5">
39
+ <span class="relative group/date cursor-default">
40
+ 🗓 <span data-ts="{{ lesson.timestamp_iso }}">{{ lesson.timestamp_iso[:10] }}</span>
41
+ <span class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700
42
+ text-white text-xs whitespace-nowrap pointer-events-none
43
+ opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">
44
+ {{ lesson.timestamp_iso[:16].replace('T', ' ') }}
45
+ </span>
46
+ </span>
47
+ <span>🏷 <span class="text-cyan-600 dark:text-cyan-400">{{ lesson.topic_id }}</span></span>
48
+ {% for cat in lesson.categories %}
49
+ <a href="/lessons?category={{ cat }}"
50
+ class="inline-block bg-gray-100 dark:bg-gray-800 hover:bg-indigo-50 dark:hover:bg-indigo-900/50 text-gray-600 dark:text-gray-300 text-xs rounded px-2 py-0.5 transition border border-gray-200 dark:border-gray-700 hover:border-indigo-400 dark:hover:border-indigo-600">
51
+ {{ cat }}
52
+ </a>
53
+ {% endfor %}
54
+ {% if lesson.feedback %}
55
+ {% if lesson.feedback == 'know' %}
56
+ <span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 border border-green-200 dark:border-green-700">✓ I know this</span>
57
+ {% else %}
58
+ <span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 border border-red-200 dark:border-red-700">✗ I don't know this</span>
59
+ {% endif %}
60
+ <form method="post" action="/lessons/{{ lesson.id }}/feedback">
61
+ <input type="hidden" name="feedback" value="clear" />
62
+ <input type="hidden" name="next" value="/lessons/{{ lesson.id }}" />
63
+ <button type="submit" class="text-xs text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-400 transition">Clear</button>
64
+ </form>
65
+ {% endif %}
66
+ </div>
67
+
68
+ <!-- Summary (rendered as markdown) -->
69
+ <div id="summary-content" class="markdown-body"></div>
70
+
71
+ <!-- Task context -->
72
+ {% if lesson.task_context %}
73
+ <div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 text-sm text-gray-500 dark:text-gray-400">
74
+ <span class="text-gray-400 dark:text-gray-500">Context:</span> {{ lesson.task_context }}
75
+ </div>
76
+ {% endif %}
77
+
78
+ <!-- Git / project metadata -->
79
+ {% set has_meta = lesson.project or lesson.repository or lesson.branch or lesson.commit_hash or lesson.folder %}
80
+ {% if has_meta %}
81
+
82
+ {# Compute repo and commit URLs from platform key #}
83
+ {% set _domains = {"github": "github.com", "gitlab": "gitlab.com", "bitbucket": "bitbucket.org"} %}
84
+ {% set _is_local = (lesson.repository_platform == "local") %}
85
+ {% if _is_local and lesson.repository %}
86
+ {% set repo_url = "vscode://file/" ~ lesson.repository %}
87
+ {% elif lesson.repository_platform in _domains and lesson.repository %}
88
+ {% set repo_url = "https://" ~ _domains[lesson.repository_platform] ~ "/" ~ lesson.repository %}
89
+ {% else %}
90
+ {% set repo_url = None %}
91
+ {% endif %}
92
+ {% if repo_url and lesson.commit_hash and not _is_local %}
93
+ {% if lesson.repository_platform == "gitlab" %}
94
+ {% set commit_url = repo_url ~ "/-/commit/" ~ lesson.commit_hash %}
95
+ {% elif lesson.repository_platform == "bitbucket" %}
96
+ {% set commit_url = repo_url ~ "/commits/" ~ lesson.commit_hash %}
97
+ {% else %}
98
+ {% set commit_url = repo_url ~ "/commit/" ~ lesson.commit_hash %}
99
+ {% endif %}
100
+ {% else %}
101
+ {% set commit_url = None %}
102
+ {% endif %}
103
+
104
+ <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800">
105
+ <div class="flex flex-wrap gap-x-5 gap-y-1.5 text-xs text-gray-400 dark:text-gray-500 font-mono">
106
+
107
+ {% if lesson.project %}
108
+ <span>
109
+ <span class="text-gray-400 dark:text-gray-600">project</span>
110
+ {% if repo_url %}
111
+ <a href="{{ repo_url }}" {% if not _is_local %}target="_blank" rel="noopener"{% endif %}
112
+ class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">{{ lesson.project }}</a>
113
+ {% else %}
114
+ <span class="text-gray-600 dark:text-gray-300">{{ lesson.project }}</span>
115
+ {% endif %}
116
+ </span>
117
+ {% endif %}
118
+
119
+ {% if lesson.repository %}
120
+ <span class="inline-flex items-center gap-1">
121
+ <span class="text-gray-400 dark:text-gray-600">repo</span>
122
+ {% if lesson.repository_platform in ("github", "gitlab", "bitbucket") %}
123
+ <img src="/static/vendor/icons/{{ lesson.repository_platform }}.svg"
124
+ class="w-3 h-3 dark:invert opacity-60 shrink-0" alt="" />
125
+ {% endif %}
126
+ {% if repo_url %}
127
+ <a href="{{ repo_url }}" {% if not _is_local %}target="_blank" rel="noopener"{% endif %}
128
+ class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">{{ lesson.repository }}</a>
129
+ {% else %}
130
+ <span class="text-gray-600 dark:text-gray-300">{{ lesson.repository }}</span>
131
+ {% endif %}
132
+ </span>
133
+ {% endif %}
134
+
135
+ {% if lesson.branch %}
136
+ <span><span class="text-gray-400 dark:text-gray-600">branch</span> <span class="text-indigo-600 dark:text-indigo-400">{{ lesson.branch }}</span></span>
137
+ {% endif %}
138
+
139
+ {% if lesson.commit_hash %}
140
+ <span>
141
+ <span class="text-gray-400 dark:text-gray-600">commit</span>
142
+ {% if commit_url %}
143
+ <a href="{{ commit_url }}" target="_blank" rel="noopener"
144
+ class="text-cyan-600 dark:text-cyan-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">{{ lesson.commit_hash[:7] }}</a>
145
+ {% else %}
146
+ <span class="text-cyan-600 dark:text-cyan-400">{{ lesson.commit_hash[:7] }}</span>
147
+ {% endif %}
148
+ </span>
149
+ {% endif %}
150
+
151
+ {% if lesson.folder %}
152
+ <span class="inline-flex items-center gap-1">
153
+ <span class="text-gray-400 dark:text-gray-600">folder</span>
154
+ <a href="vscode://file/{{ lesson.folder }}"
155
+ class="inline-flex items-center gap-1 text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">
156
+ <img src="/static/vendor/icons/vscode.svg" class="w-3.5 h-3.5 shrink-0" alt="" />
157
+ {{ lesson.folder }}
158
+ </a>
159
+ </span>
160
+ {% endif %}
161
+
162
+ </div>
163
+ </div>
164
+ {% endif %}
165
+
166
+ <!-- Feedback buttons at the bottom (only when no feedback given yet) -->
167
+ {% if not lesson.feedback %}
168
+ <div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 flex flex-wrap gap-2">
169
+ <form method="post" action="/lessons/{{ lesson.id }}/feedback">
170
+ <input type="hidden" name="feedback" value="know" />
171
+ <input type="hidden" name="next" value="/lessons/{{ lesson.id }}" />
172
+ <button type="submit"
173
+ class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-green-100 dark:hover:bg-green-800 hover:text-green-700 dark:hover:text-white">
174
+ ✓ I know this
175
+ </button>
176
+ </form>
177
+ <form method="post" action="/lessons/{{ lesson.id }}/feedback">
178
+ <input type="hidden" name="feedback" value="dont_know" />
179
+ <input type="hidden" name="next" value="/lessons/{{ lesson.id }}" />
180
+ <button type="submit"
181
+ class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-red-100 dark:hover:bg-red-900 hover:text-red-700 dark:hover:text-white">
182
+ ✗ I don't know this
183
+ </button>
184
+ </form>
185
+ </div>
186
+ {% endif %}
187
+
188
+ <!-- Footer -->
189
+ <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-300 dark:text-gray-600">
190
+ ID: {{ lesson.id }}
191
+ </div>
192
+ </div>
193
+ {% endblock %}
194
+
195
+ {% block scripts %}
196
+ <script src="/static/vendor/highlight.min.js"></script>
197
+ <script src="/static/vendor/marked.min.js"></script>
198
+ <script src="/static/relative-time.js"></script>
199
+ <script>
200
+ // Set correct hljs theme immediately (base.html updateHljsTheme targets #hljs-theme)
201
+ updateHljsTheme();
202
+
203
+ marked.setOptions({
204
+ highlight: function(code, lang) {
205
+ const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext';
206
+ return hljs.highlight(code, { language }).value;
207
+ },
208
+ breaks: true,
209
+ gfm: true,
210
+ });
211
+
212
+ const raw = {{ lesson.summary | tojson }};
213
+ document.getElementById('summary-content').innerHTML = marked.parse(raw);
214
+ </script>
215
+ {% endblock %}