devcoach 2.7.0-next.604.g26d3007 → 2.7.0-next.606.gfaf861d

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.
@@ -0,0 +1,26 @@
1
+ # Third-party notices
2
+
3
+ devcoach is licensed under AGPL-3.0-only (see `LICENSE`). It redistributes the libraries below,
4
+ vendored as files under `assets/static/vendor/`. Each keeps its own licence; all of them are
5
+ compatible with distribution under the AGPL.
6
+
7
+ | Library | Version | Licence | Where it is used | Notice |
8
+ |---|---|---|---|---|
9
+ | [highlight.js](https://github.com/highlightjs/highlight.js) | 11.10.0 | BSD-3-Clause | code blocks in lesson pages; **inlined into course documents** | header of `highlight.min.js`, full text in `highlight.LICENSE.txt` |
10
+ | [marked](https://github.com/markedjs/marked) | 4.3.0 | MIT | lesson markdown in the dashboard | header of `marked.min.js` |
11
+ | [DOMPurify](https://github.com/cure53/DOMPurify) | 3.4.13 | Apache-2.0 OR MPL-2.0 | sanitising rendered markdown | header of `purify.min.js` |
12
+ | [flatpickr](https://github.com/flatpickr/flatpickr) | 4.6.13 | MIT | the date range picker | header of `flatpickr.min.js` |
13
+ | [Alpine.js](https://github.com/alpinejs/alpine) | 3.14.1 | MIT | dashboard interactions | upstream repository (the minified file carries no header) |
14
+ | [htmx](https://github.com/bigskysoftware/htmx) | 1.9.12 | BSD-2-Clause | partial page updates | upstream repository (the minified file carries no header) |
15
+ | [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) (Play CDN build) | 3.4.17 | MIT | dashboard styling | upstream repository (the file carries no header) |
16
+
17
+ ## Course documents and artifacts
18
+
19
+ A course document that shows code carries highlight.js **inside the file**: devcoach fills the
20
+ document's `<script data-devcoach="highlighter">` placeholder with the vendored build. The
21
+ BSD-3-Clause terms ask that the copyright notice, the conditions and the disclaimer travel with
22
+ every redistribution in binary form, so the inlined block starts with the full licence text.
23
+ Keep that comment when you copy, publish or share a course document — publishing it as a Claude
24
+ artifact redistributes the library too.
25
+
26
+ The course document itself (the text, examples and scripts written for you) is yours.
@@ -110,6 +110,33 @@ translated into JS just to make it runnable:
110
110
  output** (typed or picked, then Reveal), **fill the blank**, or **spot the bug** (3–4
111
111
  candidates). No button may pretend to execute Java, Rust or Go.
112
112
 
113
+ **Code and highlighting.** Every snippet is a `<pre><code class="language-…">` with the language
114
+ named (`js ts python java kotlin csharp go rust swift sql bash json xml css yaml diff`, 36 in
115
+ all) and `&`, `<`, `>` escaped in its text. The document carries ONE placeholder, before your
116
+ own script:
117
+
118
+ ```html
119
+ <script data-devcoach="highlighter"></script>
120
+ ```
121
+
122
+ devcoach fills it when you register steps, in a document that shows code (a course without
123
+ code never carries the library; leave the placeholder in anyway) — with highlight.js (BSD-3-Clause, the licence text
124
+ travels in the block), the stylesheet that colours it, and the call that highlights every
125
+ `pre code`. The library then lives inside the file, so the course is highlighted opened from
126
+ disk and as an artifact, not only in the dashboard.
127
+
128
+ - **Never write, paste or load a highlighter yourself**, and never colour code by hand with
129
+ `<span>`s: write plain escaped text and the language class.
130
+ - Colours are tokens with defaults matched to the palette: `--syn-keyword --syn-string
131
+ --syn-comment --syn-number --syn-title --syn-type --syn-attr --syn-meta`. Define them in your
132
+ token blocks only to retune them, each at 4.5:1 or better on `--code-bg`.
133
+ - `class="nohighlight"` on a `<code>` keeps it plain: terminal transcripts, program output. An
134
+ editable `<textarea>` is never highlighted.
135
+ - Once filled, the file holds the library (about 120 KB on a few very long lines). Change the
136
+ document with **targeted edits**; do not rewrite it whole and do not quote the block. If a
137
+ rewrite is unavoidable, write the empty placeholder again: it is refilled on the next
138
+ `add_course_step` or `update_course_progress`.
139
+
113
140
  **Design — conform to the dashboard, do not copy it.** One measure (~68ch), generous spacing,
114
141
  quiet Tufte-like typography, light + dark. Every colour is a **token** on `:root`; nothing is
115
142
  hard-coded further down (no stray `#fff` on a button, no grey typed by hand in a rule):
@@ -161,12 +188,15 @@ Skeleton (adapt, keep the ids, the class names and the token names):
161
188
  <nav class="steps"><a href="#step-1">1 · Sums</a><a href="#step-2">2 · Powers</a>
162
189
  <button class="theme-switch" type="button"></button></nav>
163
190
  <main>
164
- <section id="step-1"><h2>1 · Sums</h2><p class="goal">…</p> … <div class="check">…<button>Reveal</button></div>
191
+ <section id="step-1"><h2>1 · Sums</h2><p class="goal">…</p> …
192
+ <pre><code class="language-js">const total = [1, 2, 3].reduce((a, b) =&gt; a + b, 0);</code></pre>
193
+ <div class="check">…<button>Reveal</button></div>
165
194
  <div class="pager"><a href="#step-1">← Previous</a><a href="#step-2">Next →</a></div>
166
195
  <p class="ask">Stuck? Ask in the chat — that's where the course is being taught.</p></section>
167
196
  <section id="step-2">…</section>
168
197
  </main>
169
198
  </div>
199
+ <script data-devcoach="highlighter"></script>
170
200
  <script>
171
201
  // show one section: on load + hashchange, toggle .current on the section and its nav link, scrollTo(0,0)
172
202
  // reveal buttons; the live examples (new Function inside try/catch) or the simulations
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2006, Ivan Sagalaev.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import {a as a$2,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g,j as j$1,l,n}from'./chunk-75PGZUMS.js';import {w,A,x,v,a,C,T,xa,Ba,ya,N,R,O,S,za,P as P$1,Q as Q$1,H,Ca,na,ra,J as J$1,I,L,D,G,Aa,F,K as K$1,U,t,wa,va,ka,ma,o,pa,oa,qa,ta,sa}from'./chunk-HFG5R5JB.js';import {existsSync,readFileSync,mkdirSync,writeFileSync}from'fs';import {dirname,join,extname}from'path';import {fileURLToPath}from'url';import {serve}from'@hono/node-server';import {Hono}from'hono';import {html,raw}from'hono/html';function j(r){return JSON.stringify(r??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function Ye(r,e){return r+e.replace(/[^A-Za-z0-9_-]/g,g=>`_${(g.codePointAt(0)??0).toString(16)}`)}var nt=html`<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><circle cx="4" cy="10" r="1.75"/><circle cx="10" cy="10" r="1.75"/><circle cx="16" cy="10" r="1.75"/></svg>`;function Y(r,e="",g=""){return html`<div class="relative ${e}" ${raw(g)} x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
3
- <button type="button" @click="open = !open" :aria-expanded="open" aria-label="More actions" title="More actions" class="w-8 h-8 inline-flex items-center justify-center rounded-lg border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-accent-400 hover:text-gray-800 dark:hover:text-gray-100">${nt}</button>
2
+ import {a as a$2,c as c$1}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g,j as j$1,l,n}from'./chunk-XSHHBWGK.js';import {A,E as E$1,B as B$1,z,a,G,X as X$1,Ba,Fa,Ca,R,V as V$1,S,W as W$1,Da,T,U,L,Ga,ra,va,N,M,P as P$1,H,K as K$1,Ea,J as J$1,O,Y as Y$1,x,Aa,za,c,d,b as b$1,oa,qa,s,ta,sa,ua,xa,wa}from'./chunk-K4GUNPZT.js';import {existsSync,readFileSync,mkdirSync,writeFileSync}from'fs';import {dirname,join,extname}from'path';import {fileURLToPath}from'url';import {serve}from'@hono/node-server';import {Hono}from'hono';import {html,raw}from'hono/html';function j(r){return JSON.stringify(r??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function We(r,e){return r+e.replace(/[^A-Za-z0-9_-]/g,g=>`_${(g.codePointAt(0)??0).toString(16)}`)}var lt=html`<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><circle cx="4" cy="10" r="1.75"/><circle cx="10" cy="10" r="1.75"/><circle cx="16" cy="10" r="1.75"/></svg>`;function Y(r,e="",g=""){return html`<div class="relative ${e}" ${raw(g)} x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
3
+ <button type="button" @click="open = !open" :aria-expanded="open" aria-label="More actions" title="More actions" class="w-8 h-8 inline-flex items-center justify-center rounded-lg border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-accent-400 hover:text-gray-800 dark:hover:text-gray-100">${lt}</button>
4
4
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-56 p-1" style="display:none">
5
5
  ${r}
6
6
  </div>
@@ -130,7 +130,7 @@ import {a as a$2,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g
130
130
  </script>
131
131
  ${r.scripts??""}
132
132
  </body>
133
- </html>`}function Je(r){let e=d=>r.allGroups.filter(t=>t!=="Other").map(t=>html`<option value="${t}" ${t===d?"selected":""}>${t}</option>`),g=html`
133
+ </html>`}function Ze(r){let e=d=>r.allGroups.filter(t=>t!=="Other").map(t=>html`<option value="${t}" ${t===d?"selected":""}>${t}</option>`),g=html`
134
134
  <div id="knowledge-map" x-data="{ editMode: JSON.parse(localStorage.getItem('km-edit-mode') || 'false'), toggle() { this.editMode = !this.editMode; localStorage.setItem('km-edit-mode', this.editMode); } }">
135
135
 
136
136
  <div class="flex items-center justify-between mb-6">
@@ -214,7 +214,7 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
214
214
  </div>
215
215
  </div>
216
216
  <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-8 gap-y-0">
217
- ${t.map(a=>{let n=Ye("topic-row-",a.topic),i=a.confidence,s=Math.trunc(i/10*100),c=i>=7?"bg-green-500":i>=4?"bg-yellow-500":"bg-red-500",u=i>=7?"text-green-600 dark:text-green-400":i>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
217
+ ${t.map(a=>{let n=We("topic-row-",a.topic),i=a.confidence,s=Math.trunc(i/10*100),c=i>=7?"bg-green-500":i>=4?"bg-yellow-500":"bg-red-500",u=i>=7?"text-green-600 dark:text-green-400":i>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
218
218
  <div id="${n}" class="flex items-center gap-2 py-1.5 border-b border-gray-100 dark:border-gray-800/50 group/row">
219
219
  <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative shrink-0 flex items-center">
220
220
  <button type="button" @click="open = !open" class="text-xs text-gray-200 dark:text-gray-700 hover:text-accent-500 dark:hover:text-accent-400 transition w-4 text-center opacity-0 group-hover/row:opacity-100" title="Move to group">⇄</button>
@@ -249,7 +249,7 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
249
249
  </div>`})}
250
250
  </div>
251
251
  </section>`)}
252
- </div>`;return P({title:"Profile \u2014 devcoach",currentPath:"/knowledge",uiTheme:r.uiTheme,body:g})}var Fe={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},Ge={know:"\u2713 Knew it",understood:"\u{1F4A1} Understood",dont_know:"\u2717 Couldn't follow",none:"\u2014 No response"},ze={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function h(r,e={}){let g={period:r.period,category:r.category,level:r.level,project:r.project,repository:r.repository,branch:r.branch,commit:r.commit,feedback:r.feedback,imported:r.imported,shared_by:r.shared_by,search:r.search,date_from:r.date_from,date_to:r.date_to,sort:r.sort,order:r.order};r.starred&&(g.starred="1"),Object.assign(g,e);let d=new URLSearchParams;for(let[t,a]of Object.entries(g))a&&d.set(t,a);return `?${d.toString()}`}function Ve(r){let e=r.s,g=!!(e.date_from||e.date_to),d=e.period!=="all"&&!g||g||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.imported||e.shared_by||e.search||e.feedback),t=g?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:Fe[e.period]??"All time",a=e.feedback?Ge[e.feedback]??"Feedback":"Feedback",n=e.shared_by?`\u{1F91D} from ${e.shared_by}`:e.imported==="1"?"\u{1F91D} Shared with me":e.imported==="0"?"\u{1F464} My own":"\u{1F91D} Shared",i=[["","","All lessons"],["0","","\u{1F464} My own"],["1","","\u{1F91D} Shared with me"],...r.allSharedBy.map(l=>["1",l,`\u{1F91D} from ${l}`])],s={junior:"text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-900/20",mid:"text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20",senior:"text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-900/20"},c=(l,x)=>html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-accent-50 dark:bg-accent-900/20 text-accent-700 dark:text-accent-300 border border-accent-200 dark:border-accent-800">${l}<a href="${x}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-accent-200 dark:hover:bg-accent-700 transition">×</a></span>`,u=(l,x,$="")=>{let S=e.sort===x,k=S&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap relative overflow-hidden text-ellipsis ${$}">
252
+ </div>`;return P({title:"Profile \u2014 devcoach",currentPath:"/knowledge",uiTheme:r.uiTheme,body:g})}var Ke={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},Ye={know:"\u2713 Knew it",understood:"\u{1F4A1} Understood",dont_know:"\u2717 Couldn't follow",none:"\u2014 No response"},Je={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function h(r,e={}){let g={period:r.period,category:r.category,level:r.level,project:r.project,repository:r.repository,branch:r.branch,commit:r.commit,feedback:r.feedback,imported:r.imported,shared_by:r.shared_by,search:r.search,date_from:r.date_from,date_to:r.date_to,sort:r.sort,order:r.order};r.starred&&(g.starred="1"),Object.assign(g,e);let d=new URLSearchParams;for(let[t,a]of Object.entries(g))a&&d.set(t,a);return `?${d.toString()}`}function Qe(r){let e=r.s,g=!!(e.date_from||e.date_to),d=e.period!=="all"&&!g||g||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.imported||e.shared_by||e.search||e.feedback),t=g?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:Ke[e.period]??"All time",a=e.feedback?Ye[e.feedback]??"Feedback":"Feedback",n=e.shared_by?`\u{1F91D} from ${e.shared_by}`:e.imported==="1"?"\u{1F91D} Shared with me":e.imported==="0"?"\u{1F464} My own":"\u{1F91D} Shared",i=[["","","All lessons"],["0","","\u{1F464} My own"],["1","","\u{1F91D} Shared with me"],...r.allSharedBy.map(l=>["1",l,`\u{1F91D} from ${l}`])],s={junior:"text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-900/20",mid:"text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20",senior:"text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-900/20"},c=(l,x)=>html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-accent-50 dark:bg-accent-900/20 text-accent-700 dark:text-accent-300 border border-accent-200 dark:border-accent-800">${l}<a href="${x}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-accent-200 dark:hover:bg-accent-700 transition">×</a></span>`,u=(l,x,$="")=>{let S=e.sort===x,k=S&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap relative overflow-hidden text-ellipsis ${$}">
253
253
  <a href="${h(e,{sort:x,order:k})}" class="hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${l}
254
254
  ${S?html`<span class="text-accent-400">${e.order==="asc"?"\u2191":"\u2193"}</span>`:html`<span class="text-gray-200 dark:text-gray-700 group-hover/sort:text-gray-400 dark:group-hover/sort:text-gray-500 transition">↕</span>`}
255
255
  </a></th>`},p=html`<svg class="w-3 h-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>`,m=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
@@ -306,7 +306,7 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
306
306
  </button>
307
307
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95" @click.outside="close()" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-64 overflow-hidden" style="display:none">
308
308
  <div class="p-1">
309
- ${Object.entries(Fe).map(([l,x])=>{let $=e.period===l&&!g;return html`<button type="button" @click="selectPreset('${l}', '${x}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${$?"bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${$?html`<span class="text-accent-500">✓</span>`:""}</button>`})}
309
+ ${Object.entries(Ke).map(([l,x])=>{let $=e.period===l&&!g;return html`<button type="button" @click="selectPreset('${l}', '${x}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${$?"bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${$?html`<span class="text-accent-500">✓</span>`:""}</button>`})}
310
310
  </div>
311
311
  <div class="border-t border-gray-100 dark:border-gray-800 p-1">
312
312
  <button type="button" @click="showCustom = !showCustom" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${g?"bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">
@@ -328,7 +328,7 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
328
328
  </div>
329
329
 
330
330
  <div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
331
- <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-accent-600 text-white border-accent-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400"}">${e.level?`${ze[e.level]??""} ${e.level}`:"Level"}${p}</button>
331
+ <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-accent-600 text-white border-accent-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400"}">${e.level?`${Je[e.level]??""} ${e.level}`:"Level"}${p}</button>
332
332
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-40 p-1 overflow-hidden" style="display:none">
333
333
  ${[["","All levels"],["junior","\u{1F7E2} Junior"],["mid","\u{1F7E1} Mid"],["senior","\u{1F534} Senior"]].map(([l,x])=>html`<button type="button" onclick="document.getElementById('h-level').value='${l}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.level===l?"bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${e.level===l?html`<span class="text-accent-500 ml-auto">✓</span>`:""}</button>`)}
334
334
  </div>
@@ -368,12 +368,12 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
368
368
  ${d?html`<div class="flex flex-wrap gap-1.5 mb-4">
369
369
  ${g?c(`\u{1F4C5} ${t}`,h(e,{date_from:"",date_to:""})):""}
370
370
  ${e.period!=="all"&&!g?c(`\u{1F550} ${t}`,h(e,{period:"all"})):""}
371
- ${e.feedback?c(Ge[e.feedback]??e.feedback,h(e,{feedback:""})):""}
371
+ ${e.feedback?c(Ye[e.feedback]??e.feedback,h(e,{feedback:""})):""}
372
372
  ${e.starred?html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300 border border-yellow-200 dark:border-yellow-800">★ Starred<a href="${h({...e,starred:false})}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-yellow-200 dark:hover:bg-yellow-700 transition">×</a></span>`:""}
373
373
  ${e.imported||e.shared_by?c(n,h(e,{imported:"",shared_by:""})):""}
374
374
  ${e.search?c(`\u{1F50D} "${e.search}"`,h(e,{search:""})):""}
375
375
  ${e.category?c(e.category,h(e,{category:""})):""}
376
- ${e.level?c(`${ze[e.level]??""} ${e.level}`,h(e,{level:""})):""}
376
+ ${e.level?c(`${Je[e.level]??""} ${e.level}`,h(e,{level:""})):""}
377
377
  ${e.project?c(`\u{1F4C1} ${e.project}`,h(e,{project:""})):""}
378
378
  ${e.repository?c(`\u2387 ${e.repository}`,h(e,{repository:""})):""}
379
379
  ${e.branch?c(`\u2387 ${e.branch}`,h(e,{branch:""})):""}
@@ -408,7 +408,7 @@ ${r.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
408
408
  </tr>
409
409
  </thead>
410
410
  <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
411
- ${r.lessons.map(l=>{let x=Ye("lesson-row-",l.id),$=l.timestamp.slice(0,10),S=l.timestamp.slice(0,16).replace("T"," ");return html`<tr id="${x}" data-id="${l.id}" data-title="${l.title}" data-href="/lessons/${encodeURIComponent(l.id)}" class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" role="link" @click="if (selectMode) toggle($el.dataset.id); else if (!$event.target.closest('a, button, input, label')) window.location = $el.dataset.href" @keydown.enter="if (selectMode) toggle($el.dataset.id); else if ($event.target === $el) window.location = $el.dataset.href" @keydown.space.prevent="selectMode && toggle($el.dataset.id)" :class="selected.includes($el.dataset.id) && 'bg-accent-50 dark:bg-accent-900/20'">
411
+ ${r.lessons.map(l=>{let x=We("lesson-row-",l.id),$=l.timestamp.slice(0,10),S=l.timestamp.slice(0,16).replace("T"," ");return html`<tr id="${x}" data-id="${l.id}" data-title="${l.title}" data-href="/lessons/${encodeURIComponent(l.id)}" class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" role="link" @click="if (selectMode) toggle($el.dataset.id); else if (!$event.target.closest('a, button, input, label')) window.location = $el.dataset.href" @keydown.enter="if (selectMode) toggle($el.dataset.id); else if ($event.target === $el) window.location = $el.dataset.href" @keydown.space.prevent="selectMode && toggle($el.dataset.id)" :class="selected.includes($el.dataset.id) && 'bg-accent-50 dark:bg-accent-900/20'">
412
412
  <td class="px-3 py-3">
413
413
  <form x-show="!selectMode" method="post" action="/lessons/${encodeURIComponent(l.id)}/star" hx-post="/lessons/${encodeURIComponent(l.id)}/star" hx-target="#${x}" hx-select="#${x}" hx-swap="outerHTML">
414
414
  <input type="hidden" name="starred" value="${l.starred?"0":"1"}" />
@@ -435,7 +435,7 @@ ${r.totalPages>1?html`<div class="flex items-center justify-between mt-4">
435
435
  <p class="text-xs text-gray-400 dark:text-gray-500">Page ${r.page} of ${r.totalPages}</p>
436
436
  <div class="flex items-center gap-1">
437
437
  ${r.page>1?html`<a href="${h(e,{page:String(r.page-1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-accent-400 transition">← Prev</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">← Prev</span>`}
438
- ${it(r.page,r.totalPages).map(l=>l===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:l===r.page?html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-accent-600 text-white border border-accent-600">${l}</span>`:html`<a href="${h(e,{page:String(l)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-accent-400 transition">${l}</a>`)}
438
+ ${ct(r.page,r.totalPages).map(l=>l===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:l===r.page?html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-accent-600 text-white border border-accent-600">${l}</span>`:html`<a href="${h(e,{page:String(l)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-accent-400 transition">${l}</a>`)}
439
439
  ${r.page<r.totalPages?html`<a href="${h(e,{page:String(r.page+1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-accent-400 transition">Next →</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">Next →</span>`}
440
440
  </div>
441
441
  </div>`:""}`:html`<div class="flex flex-col items-center justify-center py-16 text-center">
@@ -509,7 +509,7 @@ function periodPicker() {
509
509
  },
510
510
  };
511
511
  }
512
- </script>`;return P({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:r.uiTheme,head:m,body:R,scripts:L})}function it(r,e){let g=[],d=0;for(let t=1;t<=e;t++)(t===1||t===e||t>=r-2&&t<=r+2)&&(d&&t-d>1&&g.push(0),g.push(t),d=t);return g}var K={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function V(r){let e=`/lessons/${encodeURIComponent(r.id)}/share?format=md&name=${encodeURIComponent(r.name)}&include_context=${r.includeContext?"1":"0"}`;return html`<div id="share-payloads" data-text="${r.text}" data-link="${r.link}" class="space-y-2">
512
+ </script>`;return P({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:r.uiTheme,head:m,body:R,scripts:L})}function ct(r,e){let g=[],d=0;for(let t=1;t<=e;t++)(t===1||t===e||t>=r-2&&t<=r+2)&&(d&&t-d>1&&g.push(0),g.push(t),d=t);return g}var K={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function V(r){let e=`/lessons/${encodeURIComponent(r.id)}/share?format=md&name=${encodeURIComponent(r.name)}&include_context=${r.includeContext?"1":"0"}`;return html`<div id="share-payloads" data-text="${r.text}" data-link="${r.link}" class="space-y-2">
513
513
  <div class="flex flex-wrap gap-2">
514
514
  <button type="button" onclick="copyShare('text', this)" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-accent-100 dark:hover:bg-accent-900/40 text-gray-700 dark:text-gray-200 hover:text-accent-700 dark:hover:text-accent-300 transition">📋 Copy text</button>
515
515
  <button type="button" onclick="copyShare('link', this)" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-accent-100 dark:hover:bg-accent-900/40 text-gray-700 dark:text-gray-200 hover:text-accent-700 dark:hover:text-accent-300 transition">🔗 Copy link</button>
@@ -528,7 +528,7 @@ function periodPicker() {
528
528
  <label class="flex items-start gap-2 text-xs text-gray-600 dark:text-gray-300 cursor-pointer"><input type="checkbox" name="include_context" value="1" ${e.includeContext?"checked":""} class="mt-0.5" /><span>Include where it happened <span class="text-gray-400 dark:text-gray-500">(project, branch, commit — never local paths)</span></span></label>
529
529
  </form>
530
530
  ${V(e)}
531
- `}function We(r){let e=r.lesson,g=r.share,d=e.level==="junior"?"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700":e.level==="mid"?"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700":"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700",t=e.repository_platform==="local",a=null;t&&e.repository?a=`vscode://file/${e.repository}`:e.repository_platform&&K[e.repository_platform]&&e.repository&&(a=`https://${K[e.repository_platform]}/${e.repository}`);let n=null;a&&e.commit_hash&&!t&&(n=e.repository_platform==="gitlab"?`${a}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${a}/commits/${e.commit_hash}`:`${a}/commit/${e.commit_hash}`);let i=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),s=e.timestamp.slice(0,10),c=e.timestamp.slice(0,16).replace("T"," "),u=`hx-post="/lessons/${encodeURIComponent(e.id)}/feedback" hx-target="#lesson-meta" hx-select="#lesson-meta" hx-swap="outerHTML" hx-select-oob="#lesson-feedback:outerHTML"`,p=html`
531
+ `}function Xe(r){let e=r.lesson,g=r.share,d=e.level==="junior"?"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700":e.level==="mid"?"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700":"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700",t=e.repository_platform==="local",a=null;t&&e.repository?a=`vscode://file/${e.repository}`:e.repository_platform&&K[e.repository_platform]&&e.repository&&(a=`https://${K[e.repository_platform]}/${e.repository}`);let n=null;a&&e.commit_hash&&!t&&(n=e.repository_platform==="gitlab"?`${a}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${a}/commits/${e.commit_hash}`:`${a}/commit/${e.commit_hash}`);let i=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),s=e.timestamp.slice(0,10),c=e.timestamp.slice(0,16).replace("T"," "),u=`hx-post="/lessons/${encodeURIComponent(e.id)}/feedback" hx-target="#lesson-meta" hx-select="#lesson-meta" hx-swap="outerHTML" hx-select-oob="#lesson-feedback:outerHTML"`,p=html`
532
532
  <div x-data="{ confirmOpen: false }" @keydown.escape.window="confirmOpen = false">
533
533
  <div class="mb-4"><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></div>
534
534
  ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-700 text-green-700 dark:text-green-300">${r.imported==="new"?html`✓ Imported “${e.title}”${e.shared_by?html`, shared by ${e.shared_by}`:""}. It's in your log now — feedback works as usual and it never counts against your daily limit.`:html`“${e.title}” was already in your log — nothing changed.`}</div>`:""}
@@ -603,7 +603,7 @@ ${J("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class=
603
603
  });
604
604
  document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(e.summary))}));
605
605
  document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(e.body))}));
606
- </script>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:r.uiTheme,head:m,body:p,scripts:v})}var dt={junior:"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700",mid:"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700",senior:"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700"};function Ze(r){let e=r.preview,g=html`<form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="space-y-3">
606
+ </script>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:r.uiTheme,head:m,body:p,scripts:v})}var gt={junior:"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700",mid:"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700",senior:"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700"};function et(r){let e=r.preview,g=html`<form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="space-y-3">
607
607
  <input type="hidden" name="from" value="lessons" />
608
608
  <textarea name="text" rows="5" placeholder="devcoach:lesson:1:… — or the link, a URL, or the whole card" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-accent-500 font-mono"></textarea>
609
609
  <div class="flex items-center gap-3">
@@ -618,7 +618,7 @@ ${J("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class=
618
618
  ${r.error?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-rose-50 dark:bg-rose-900/20 border-rose-200 dark:border-rose-700 text-rose-700 dark:text-rose-300">${r.error}</div>`:""}
619
619
  ${e?html`<div class="flex flex-wrap items-center gap-3 mb-2">
620
620
  <h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.lesson.title}</h1>
621
- <span class="text-xs font-semibold px-2 py-0.5 rounded-full border ${dt[e.lesson.level]??""} shrink-0">${e.lesson.level}</span>
621
+ <span class="text-xs font-semibold px-2 py-0.5 rounded-full border ${gt[e.lesson.level]??""} shrink-0">${e.lesson.level}</span>
622
622
  </div>
623
623
  <div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400 mb-5">
624
624
  <span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.lesson.topic_id}</span></span>
@@ -655,7 +655,7 @@ ${J("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class=
655
655
  });
656
656
  ${e?raw(`document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${j(e.lesson.summary)}));
657
657
  document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${j(e.lesson.body??"")}));`):""}
658
- </script>`;return P({title:e?`${e.lesson.title} \u2014 devcoach`:"Import a lesson \u2014 devcoach",currentPath:"/lessons",uiTheme:r.uiTheme,head:t,body:d,scripts:a})}var lt=[[0,"No cooldown"],[15,"15 minutes"],[30,"30 minutes"],[60,"1 hour"],[90,"1 hour 30 min"],[120,"2 hours"],[180,"3 hours"],[240,"4 hours"],[360,"6 hours"],[480,"8 hours"],[720,"12 hours"],[1440,"24 hours"]],Ke={active:"bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 border-accent-200 dark:border-accent-700",completed:"bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-400 border-green-200 dark:border-green-700",abandoned:"bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700"},ct={todo:"\u25CB",done:"\u25CF",skipped:"\u25CC"};function Qe(r){return html`<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${Ke[r]??Ke.abandoned}">${r}</span>`}function Xe(r){let e=html`
658
+ </script>`;return P({title:e?`${e.lesson.title} \u2014 devcoach`:"Import a lesson \u2014 devcoach",currentPath:"/lessons",uiTheme:r.uiTheme,head:t,body:d,scripts:a})}var pt=[[0,"No cooldown"],[15,"15 minutes"],[30,"30 minutes"],[60,"1 hour"],[90,"1 hour 30 min"],[120,"2 hours"],[180,"3 hours"],[240,"4 hours"],[360,"6 hours"],[480,"8 hours"],[720,"12 hours"],[1440,"24 hours"]],Ve={active:"bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 border-accent-200 dark:border-accent-700",completed:"bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-400 border-green-200 dark:border-green-700",abandoned:"bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700"},ut={todo:"\u25CB",done:"\u25CF",skipped:"\u25CC"};function tt(r){return html`<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${Ve[r]??Ve.abandoned}">${r}</span>`}function rt(r){let e=html`
659
659
  <div class="flex items-center justify-between mb-6">
660
660
  <h1 class="text-2xl font-bold text-accent-600 dark:text-accent-400">Courses</h1>
661
661
  <p class="text-sm text-gray-400 dark:text-gray-500">${r.courses.length} course${r.courses.length!==1?"s":""}</p>
@@ -677,7 +677,7 @@ ${r.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
677
677
  <td class="px-3 py-3">
678
678
  <div class="flex items-center gap-2"><div class="flex-1 h-1.5 rounded-full bg-gray-100 dark:bg-gray-800 overflow-hidden"><div class="h-full bg-accent-500" style="width:${a}%"></div></div><span class="text-xs tabular-nums text-gray-500 dark:text-gray-400 shrink-0">${t}/${d.steps.length}</span></div>
679
679
  </td>
680
- <td class="px-3 py-3">${Qe(d.status)}</td>
680
+ <td class="px-3 py-3">${tt(d.status)}</td>
681
681
  <td class="px-3 py-3 whitespace-nowrap text-gray-400 dark:text-gray-500 tabular-nums"><span data-ts="${d.updated_at}">${d.updated_at.slice(0,10)}</span></td>
682
682
  </tr>`})}
683
683
  </tbody>
@@ -686,18 +686,18 @@ ${r.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
686
686
  <p class="text-3xl mb-3">🎓</p>
687
687
  <p class="text-gray-700 dark:text-gray-200 font-medium">No courses yet.</p>
688
688
  <p class="text-sm text-gray-500 dark:text-gray-400 mt-2 max-w-md mx-auto">A course starts in your agent: answer <span class="font-mono">n</span> (couldn't follow) under a lesson card and say yes to the offer, or run <span class="font-mono">/devcoach:course</span> on any lesson or concept. The AI first asks what you already know, then writes the course here.</p>
689
- </div>`}`,g=html`<script src="/static/relative-time.js"></script>`;return P({title:"Courses \u2014 devcoach",currentPath:"/courses",uiTheme:r.uiTheme,body:e,scripts:g})}function et(r){return html`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><title>Read-only — devcoach</title>
689
+ </div>`}`,g=html`<script src="/static/relative-time.js"></script>`;return P({title:"Courses \u2014 devcoach",currentPath:"/courses",uiTheme:r.uiTheme,body:e,scripts:g})}function at(r){return html`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><title>Read-only — devcoach</title>
690
690
  <link rel="stylesheet" href="/static/style.css" /></head>
691
691
  <body style="font-family: ui-monospace, monospace; max-width: 40rem; margin: 4rem auto; padding: 0 1rem; line-height: 1.5">
692
692
  <h1 style="font-size: 1.25rem">This devcoach is older than your database</h1>
693
693
  <p>${r}</p>
694
694
  <p><a href="javascript:history.back()">← Back</a></p>
695
- </body></html>`}function tt(r){let e=r.course,g=e.steps.filter(s=>s.status!=="todo").length,d=e.steps.find(s=>s.position===r.selected)??e.steps[0],t=`/courses/${encodeURIComponent(e.id)}/index.html${d?`#${d.anchor}`:""}`,a=(s,c,u,p)=>html`<form method="post" action="/courses/${encodeURIComponent(e.id)}/steps/${s}" class="inline"><input type="hidden" name="status" value="${c}" /><input type="hidden" name="next" value="/courses/${encodeURIComponent(e.id)}?step=${s}" /><button type="submit" class="${p}">${u}</button></form>`,n="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400",i=html`
695
+ </body></html>`}function ot(r){let e=r.course,g=e.steps.filter(s=>s.status!=="todo").length,d=e.steps.find(s=>s.position===r.selected)??e.steps[0],t=`/courses/${encodeURIComponent(e.id)}/index.html${d?`#${d.anchor}`:""}`,a=(s,c,u,p)=>html`<form method="post" action="/courses/${encodeURIComponent(e.id)}/steps/${s}" class="inline"><input type="hidden" name="status" value="${c}" /><input type="hidden" name="next" value="/courses/${encodeURIComponent(e.id)}?step=${s}" /><button type="submit" class="${p}">${u}</button></form>`,n="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400",i=html`
696
696
  <div x-data="{ confirmOpen: false }" @keydown.escape.window="confirmOpen = false">
697
697
  <div class="sticky top-0 z-20 -mx-4 sm:-mx-6 -mt-8 mb-5 px-4 sm:px-6 h-12 flex items-center gap-3 bg-gray-50/95 dark:bg-gray-950/95 backdrop-blur border-b border-gray-200 dark:border-gray-800">
698
698
  <a href="/courses" title="Back to courses" class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white text-sm transition shrink-0">←</a>
699
699
  <span class="text-sm font-semibold text-gray-900 dark:text-white truncate min-w-0">${e.title}</span>
700
- ${Qe(e.status)}
700
+ ${tt(e.status)}
701
701
  <span class="text-xs text-gray-500 dark:text-gray-400 shrink-0">${g}/${e.steps.length} steps</span>
702
702
  ${Y(html`<button type="button" @click="confirmOpen = true; open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 hover:!bg-rose-50 dark:hover:!bg-rose-900/30 hover:text-rose-700 dark:hover:text-rose-300"><span class="w-4 h-4 inline-flex items-center justify-center shrink-0 text-[13px] leading-none" aria-hidden="true">🗑</span>Delete course…</button>`,"ml-auto shrink-0")}
703
703
  </div>
@@ -706,7 +706,7 @@ ${r.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
706
706
  <p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 px-2 mb-2">Steps</p>
707
707
  ${e.steps.length?html`<ol class="space-y-0.5">${e.steps.map(s=>html`<li data-anchor="${s.anchor}" class="rounded-lg px-2 py-1.5 ${d&&s.position===d.position?"bg-accent-50 dark:bg-accent-900/30":""}">
708
708
  <a href="/courses/${encodeURIComponent(e.id)}?step=${s.position}" data-anchor="${s.anchor}" class="flex items-start gap-2 text-sm">
709
- <span class="${s.status==="done"?"text-green-500":s.status==="skipped"?"text-gray-400":"text-accent-400"}">${ct[s.status]??"\u25CB"}</span>
709
+ <span class="${s.status==="done"?"text-green-500":s.status==="skipped"?"text-gray-400":"text-accent-400"}">${ut[s.status]??"\u25CB"}</span>
710
710
  <span class="min-w-0 flex-1"><span class="block text-gray-800 dark:text-gray-100">${s.position}. ${s.title}</span><span class="block text-[11px] text-gray-400 dark:text-gray-500">${s.kind}${s.status!=="todo"?` \xB7 ${s.status}`:""}</span></span>
711
711
  </a>
712
712
  <div data-step-actions class="flex flex-wrap gap-1.5 mt-2 pl-6 ${d&&s.position===d.position?"":"hidden"}">
@@ -736,7 +736,7 @@ ${J("confirmOpen","confirmOpen = false",html`Delete this course?`,html`<p class=
736
736
  <input type="hidden" name="id" value="${e.id}" />
737
737
  <button type="submit" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-rose-600 text-white border-rose-600 hover:bg-rose-500">🗑 Delete course</button>
738
738
  </form>`)}
739
- </div>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/courses",uiTheme:r.uiTheme,body:i,scripts:html`<script src="/static/course-viewer.js"></script>`,wide:true})}function rt(r){let e=r.settings.min_gap_minutes,g=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],d=[["auto","Auto","\u{1F3E0}"],["lessons","Lessons","\u{1F4DA}"],["knowledge","Knowledge map","\u{1F9ED}"]],t=r.flash,a=html`
739
+ </div>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/courses",uiTheme:r.uiTheme,body:i,scripts:html`<script src="/static/course-viewer.js"></script>`,wide:true})}function st(r){let e=r.settings.min_gap_minutes,g=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],d=[["auto","Auto","\u{1F3E0}"],["lessons","Lessons","\u{1F4DA}"],["knowledge","Knowledge map","\u{1F9ED}"]],t=r.flash,a=html`
740
740
  <h1 class="text-2xl font-bold mb-6 text-accent-600 dark:text-accent-400">Settings</h1>
741
741
  ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-700 text-yellow-800 dark:text-yellow-300":"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-700 text-green-700 dark:text-green-300"}">
742
742
  ${t.imported} lesson${t.imported!==1?"s":""} imported.${t.skipped?` ${t.skipped} skipped (already in DB).`:""}${t.invalid?` ${t.invalid} rejected (failed validation).`:""}${t.groups?` ${t.groups} group${t.groups!==1?"s":""} added.`:""}${t.notebook?" Notebook restored.":""}
@@ -754,7 +754,7 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
754
754
  <div>
755
755
  <label for="min-gap-minutes" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Minimum gap between lessons</label>
756
756
  <select id="min-gap-minutes" name="min_gap_minutes" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-accent-500">
757
- ${lt.map(([i,s])=>html`<option value="${i}" ${e===i?"selected":""}>${s}</option>`)}
757
+ ${pt.map(([i,s])=>html`<option value="${i}" ${e===i?"selected":""}>${s}</option>`)}
758
758
  </select>
759
759
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Minimum gap between lessons.</p>
760
760
  </div>
@@ -862,9 +862,10 @@ function updateLabel(inputId, labelId, submitId) {
862
862
  }
863
863
  marked.setOptions({ breaks: true, gfm: true });
864
864
  document.getElementById('notebook-preview-settings').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(r.notebookContent))}));
865
- </script>`;return P({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:r.uiTheme,body:a,scripts:n})}var Z=25;function B(r,e="/lessons"){return r?.startsWith("/")&&!r.startsWith("//")?r:e}function b(r,e,g=""){let d=r[e];return typeof d=="string"?d:g}var Q=dirname(fileURLToPath(import.meta.url)),X=[join(Q,"../assets/static"),join(Q,"../../assets/static")].find(r=>existsSync(r))??join(Q,"../assets/static"),bt={".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".woff2":"font/woff2",".woff":"font/woff",".ico":"image/x-icon"},ht="https://ultimaphoenix.github.io",at={"access-control-allow-origin":ht,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function E(r){let e=r.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var ee;function ft(){return ee===void 0&&(ee=n()),ee}function ot(r,e,g){let d=pa({explicit:g.name??null,setting:e,gitUserName:ft()}),t=g.includeContext??false,a=oa(r,{includeContext:t,sharedBy:d}),n=qa(a);return {payload:a,state:{id:r.id,open:g.open??false,name:d??"",includeContext:t,text:sa(a,n),link:ta(a,n)}}}function kt(r,e){return r==="lessons"?"/lessons":r==="knowledge"?"/knowledge":e?"/lessons":"/knowledge"}function vt(){try{return A(r=>T(r).ui_theme)}catch{return "system"}}function wt(r={}){let e$1=new Hono;e$1.onError((t,a)=>t instanceof w?a.html(et(t.message),503):(console.error(t),a.text("Internal Server Error",500))),e$1.get("/static/*",t=>{let a=decodeURIComponent(t.req.path.slice(8)),n=join(X,a);if(!n.startsWith(X))return t.notFound();try{let i=readFileSync(n);return new Response(new Uint8Array(i),{headers:{"content-type":bt[extname(n)]??"application/octet-stream","cache-control":"no-cache"}})}catch{return t.notFound()}}),e$1.get("/ping",t=>{let a$1=null;try{a$1=A(n=>{let i=x(n);return i?{...i,message:v(i)}:null});}catch{}return t.json({ok:true,version:a,compat:a$1},200,at)}),e$1.options("/ping",t=>t.body(null,204,at)),e$1.post("/shutdown",t=>E(t)?t.text("Forbidden",403):(setTimeout(()=>r.onShutdown?.(),50),t.json({ok:true}))),e$1.get("/shutdown",t=>t.text("Method Not Allowed",405)),e$1.get("/",t=>{let{home:a,hasLessons:n}=A(i=>({home:T(i).ui_home,hasLessons:C(i)>0}));return t.redirect(kt(a,n),302)}),e$1.get("/knowledge",t=>{let{profile:a,stats:n,rateLimit:i,settings:s}=A(p=>({profile:ya(p),stats:Ba(p),rateLimit:xa(p),settings:T(p)})),c=new Map;for(let p of a.groups)for(let m of p.topics)c.set(m,p.name);let u={};for(let p of a.groups)u[p.name]=[];for(let p of a.knowledge){let m=c.get(p.topic)??"Other",v=u[m]??[];v.push(p),u[m]=v;}return t.html(Je({categorised:u,allGroups:a.groups.map(p=>p.name),stats:n,rateLimit:i,maxPerDay:s.max_per_day,uiTheme:s.ui_theme}))}),e$1.post("/knowledge",async t=>{let a=await t.req.parseBody(),n=b(a,"topic").trim();if(n){let i=Number.parseInt(b(a,"confidence","5"),10)||5,s=b(a,"group").trim();A(c=>{N(c,n,i),s&&s!=="Other"&&R(c,n,s);});}return t.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic/delete",t=>(A(a=>O(a,t.req.param("topic"))),t.redirect("/knowledge",303))),e$1.post("/knowledge/:topic/group",async t=>{let a=b(await t.req.parseBody(),"group").trim(),n=t.req.param("topic");return A(i=>{a&&a!=="Other"?R(i,n,a):S(i,n);}),t.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic",async t=>{let a=Number.parseInt(b(await t.req.parseBody(),"delta","0"),10)||0;return A(n=>za(n,t.req.param("topic"),a)),t.redirect("/knowledge",303)}),e$1.post("/groups",async t=>{let a=b(await t.req.parseBody(),"group_name").trim();return a&&a!=="Other"&&A(n=>P$1(n,a)),t.redirect("/knowledge",303)}),e$1.post("/groups/:group_name/delete",t=>(A(a=>Q$1(a,t.req.param("group_name"))),t.redirect("/knowledge",303))),e$1.get("/lessons/export",()=>{let t=A(a=>H(a));return new Response(JSON.stringify(t,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),e$1.post("/lessons/import",async t=>{let a=await t.req.parseBody(),n=b(a,"from")==="lessons",i=p=>t.redirect(n?`/lessons?import=1&error=${p}`:"/settings?imported=0&skipped=0&invalid=1",303);if(E(t))return i("cross");let s=a.file,u=(s instanceof File&&s.size>0?await s.text():null)??b(a,"text").trim();try{p(u)&&!u.includes("#devcoach:lesson:")&&(u=await q(u));let p$1=A(m=>Ca(m,u));return p$1.kind==="lessons"||!p$1.lesson?t.redirect(`/settings?imported=${p$1.inserted}&skipped=${p$1.duplicated}&invalid=${p$1.invalid}`,303):t.redirect(`/lessons/${encodeURIComponent(p$1.lesson.id)}?imported=${p$1.inserted?"1":"dup"}`,303)}catch(p){if(p instanceof na)return i("invalid");throw p}}),e$1.get("/lessons/import",t=>{let a=(t.req.query("code")??"").trim(),n=null,i=null;if(a)try{n=ra(a);}catch(s){i=s instanceof na?s.message:"That code could not be read.";}return t.html(Ze({code:a,preview:n,error:i,uiTheme:vt()}))}),e$1.get("/lessons",t=>{let a=t.req.query(),n=new Set(["junior","mid","senior"]),i=a.level&&n.has(a.level)?a.level:"",s=a.date_from||"",c=a.date_to||"",u=a.starred==="1",p=a.imported==="1"?"1":a.imported==="0"?"0":"",m=a.shared_by||null,v=a.sort||"timestamp",T$1=a.order==="asc"?"asc":"desc",R=Math.max(1,Number.parseInt(a.page??"1",10)||1),L$1=a.period||"all",l={period:s||c?null:L$1!=="all"?L$1:null,category:a.category||null,level:i||null,project:a.project||null,repository:a.repository||null,branch:a.branch||null,commit:a.commit||null,starred:u?true:null,imported:m||p==="1"?true:p==="0"?false:null,shared_by:m,search:a.search||null,feedback:a.feedback||null,date_from:s||null,date_to:c||null},x=A(k=>{let re=C(k,l),ae=Math.max(1,Math.ceil(re/Z));return R=Math.min(R,ae),{total:re,totalPages:ae,lessons:D(k,{...l,sort:v,order:T$1,page:R,per_page:Z}),allCategories:L(k),allProjects:I(k,"project"),allRepositories:I(k,"repository"),allBranches:I(k,"branch"),allCommits:I(k,"commit_hash"),allSharedBy:J$1(k),theme:T(k).ui_theme}}),$={period:L$1,category:a.category||"",level:i,project:a.project||"",repository:a.repository||"",branch:a.branch||"",commit:a.commit||"",starred:u,imported:m?"1":p,shared_by:m??"",search:a.search||"",feedback:a.feedback||"",date_from:s,date_to:c,sort:v,order:T$1},S=a.error==="invalid"||a.error==="cross"?a.error:null;return t.html(Ve({importOpen:a.import==="1",importError:S,lessons:x.lessons,allCategories:x.allCategories,allProjects:x.allProjects,allRepositories:x.allRepositories,allBranches:x.allBranches,allCommits:x.allCommits,allSharedBy:x.allSharedBy,s:$,page:R,perPage:Z,total:x.total,totalPages:x.totalPages,uiTheme:x.theme}))}),e$1.post("/lessons/:lesson_id/star",async t=>{let a=await t.req.parseBody();return A(n=>G(n,t.req.param("lesson_id"),a.starred==="1")),t.redirect(B(b(a,"next")||void 0),303)}),e$1.post("/lessons/:lesson_id/feedback",async t=>{let a=await t.req.parseBody(),n=b(a,"feedback");if(!["","clear","know","understood","dont_know"].includes(n))return t.text("Unknown feedback value",400);let i=n===""||n==="clear"?null:n;return A(s=>Aa(s,t.req.param("lesson_id"),i)),t.redirect(B(b(a,"next")||void 0),303)}),e$1.post("/lessons/delete",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody({all:true}),n=a.id,i=(Array.isArray(n)?n:[n]).filter(s=>typeof s=="string"&&s!=="");return A(s=>F(s,i)),t.redirect(B(b(a,"next")||void 0),303)}),e$1.on(["GET","POST"],"/lessons/:lesson_id/share",async t$1=>{if(E(t$1))return t$1.text("Forbidden",403);let a,n,i=false;if(t$1.req.method==="POST"){let m=await t$1.req.parseBody();a=b(m,"name"),n=b(m,"include_context")==="1",i=b(m,"persist")==="1";}else a=t$1.req.query("name"),n=t$1.req.query("include_context")==="1";let s=A(m=>{let v=K$1(m,t$1.req.param("lesson_id")??"");return v?(i&&a!==void 0&&U(m,"share_name",t(a)),{lesson:v,settingName:T(m).share_name}):null});if(!s)return t$1.text("Lesson not found",404);let{state:c,payload:u}=ot(s.lesson,s.settingName,{name:a,includeContext:n}),p=t$1.req.query("format");return p==="text"?t$1.text(c.text):p==="link"?t$1.text(c.link):p==="md"?new Response(wa(u),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${va(u)}"`}}):p==="panel"?t$1.html(W(s.lesson.id,c,s.lesson.title)):t$1.html(V(c))}),e$1.get("/lessons/:lesson_id",t=>{let a=A(s=>{let c=K$1(s,t.req.param("lesson_id"));if(!c)return null;let u=T(s),p=h$1(s,{lesson_id:c.id})[0],m$1=p?{id:p.id,title:p.title,status:p.status,...m(p)}:null;return {lesson:c,course:m$1,settingName:u.share_name,uiTheme:u.ui_theme}});if(!a)return t.html("<h1>Lesson not found</h1>",404);let n=t.req.query(),i=n.imported==="1"?"new":n.imported==="dup"?"dup":null;return t.html(We({lesson:a.lesson,uiTheme:a.uiTheme,share:ot(a.lesson,a.settingName,{open:n.share==="1"}).state,imported:i,course:a.course}))});let g$1={"content-type":"text/html; charset=utf-8","content-security-policy":"sandbox allow-scripts allow-forms; default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; style-src 'unsafe-inline'; img-src data:; font-src data:; form-action 'none'; base-uri 'none'; frame-ancestors 'self'","referrer-policy":"no-referrer","x-content-type-options":"nosniff","cache-control":"no-cache"},d=(()=>{try{return `
865
+ </script>`;return P({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:r.uiTheme,body:a,scripts:n})}var Z=25;function B(r,e="/lessons"){return r?.startsWith("/")&&!r.startsWith("//")?r:e}function b(r,e,g=""){let d=r[e];return typeof d=="string"?d:g}var Q=dirname(fileURLToPath(import.meta.url)),X=[join(Q,"../assets/static"),join(Q,"../../assets/static")].find(r=>existsSync(r))??join(Q,"../assets/static"),kt={".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".woff2":"font/woff2",".woff":"font/woff",".ico":"image/x-icon"},vt="https://ultimaphoenix.github.io",nt={"access-control-allow-origin":vt,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function E(r){let e=r.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var ee;function wt(){return ee===void 0&&(ee=n()),ee}function it(r,e,g){let d=ta({explicit:g.name??null,setting:e,gitUserName:wt()}),t=g.includeContext??false,a=sa(r,{includeContext:t,sharedBy:d}),n=ua(a);return {payload:a,state:{id:r.id,open:g.open??false,name:d??"",includeContext:t,text:wa(a,n),link:xa(a,n)}}}function $t(r,e){return r==="lessons"?"/lessons":r==="knowledge"?"/knowledge":e?"/lessons":"/knowledge"}function _t(){try{return E$1(r=>X$1(r).ui_theme)}catch{return "system"}}function St(r={}){let e$1=new Hono;e$1.onError((t,a)=>t instanceof A?a.html(at(t.message),503):(console.error(t),a.text("Internal Server Error",500))),e$1.get("/static/*",t=>{let a=decodeURIComponent(t.req.path.slice(8)),n=join(X,a);if(!n.startsWith(X))return t.notFound();try{let i=readFileSync(n);return new Response(new Uint8Array(i),{headers:{"content-type":kt[extname(n)]??"application/octet-stream","cache-control":"no-cache"}})}catch{return t.notFound()}}),e$1.get("/ping",t=>{let a$1=null;try{a$1=E$1(n=>{let i=B$1(n);return i?{...i,message:z(i)}:null});}catch{}return t.json({ok:true,version:a,compat:a$1},200,nt)}),e$1.options("/ping",t=>t.body(null,204,nt)),e$1.post("/shutdown",t=>E(t)?t.text("Forbidden",403):(setTimeout(()=>r.onShutdown?.(),50),t.json({ok:true}))),e$1.get("/shutdown",t=>t.text("Method Not Allowed",405)),e$1.get("/",t=>{let{home:a,hasLessons:n}=E$1(i=>({home:X$1(i).ui_home,hasLessons:G(i)>0}));return t.redirect($t(a,n),302)}),e$1.get("/knowledge",t=>{let{profile:a,stats:n,rateLimit:i,settings:s}=E$1(p=>({profile:Ca(p),stats:Fa(p),rateLimit:Ba(p),settings:X$1(p)})),c=new Map;for(let p of a.groups)for(let m of p.topics)c.set(m,p.name);let u={};for(let p of a.groups)u[p.name]=[];for(let p of a.knowledge){let m=c.get(p.topic)??"Other",v=u[m]??[];v.push(p),u[m]=v;}return t.html(Ze({categorised:u,allGroups:a.groups.map(p=>p.name),stats:n,rateLimit:i,maxPerDay:s.max_per_day,uiTheme:s.ui_theme}))}),e$1.post("/knowledge",async t=>{let a=await t.req.parseBody(),n=b(a,"topic").trim();if(n){let i=Number.parseInt(b(a,"confidence","5"),10)||5,s=b(a,"group").trim();E$1(c=>{R(c,n,i),s&&s!=="Other"&&V$1(c,n,s);});}return t.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic/delete",t=>(E$1(a=>S(a,t.req.param("topic"))),t.redirect("/knowledge",303))),e$1.post("/knowledge/:topic/group",async t=>{let a=b(await t.req.parseBody(),"group").trim(),n=t.req.param("topic");return E$1(i=>{a&&a!=="Other"?V$1(i,n,a):W$1(i,n);}),t.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic",async t=>{let a=Number.parseInt(b(await t.req.parseBody(),"delta","0"),10)||0;return E$1(n=>Da(n,t.req.param("topic"),a)),t.redirect("/knowledge",303)}),e$1.post("/groups",async t=>{let a=b(await t.req.parseBody(),"group_name").trim();return a&&a!=="Other"&&E$1(n=>T(n,a)),t.redirect("/knowledge",303)}),e$1.post("/groups/:group_name/delete",t=>(E$1(a=>U(a,t.req.param("group_name"))),t.redirect("/knowledge",303))),e$1.get("/lessons/export",()=>{let t=E$1(a=>L(a));return new Response(JSON.stringify(t,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),e$1.post("/lessons/import",async t=>{let a=await t.req.parseBody(),n=b(a,"from")==="lessons",i=p=>t.redirect(n?`/lessons?import=1&error=${p}`:"/settings?imported=0&skipped=0&invalid=1",303);if(E(t))return i("cross");let s=a.file,u=(s instanceof File&&s.size>0?await s.text():null)??b(a,"text").trim();try{p(u)&&!u.includes("#devcoach:lesson:")&&(u=await q(u));let p$1=E$1(m=>Ga(m,u));return p$1.kind==="lessons"||!p$1.lesson?t.redirect(`/settings?imported=${p$1.inserted}&skipped=${p$1.duplicated}&invalid=${p$1.invalid}`,303):t.redirect(`/lessons/${encodeURIComponent(p$1.lesson.id)}?imported=${p$1.inserted?"1":"dup"}`,303)}catch(p){if(p instanceof ra)return i("invalid");throw p}}),e$1.get("/lessons/import",t=>{let a=(t.req.query("code")??"").trim(),n=null,i=null;if(a)try{n=va(a);}catch(s){i=s instanceof ra?s.message:"That code could not be read.";}return t.html(et({code:a,preview:n,error:i,uiTheme:_t()}))}),e$1.get("/lessons",t=>{let a=t.req.query(),n=new Set(["junior","mid","senior"]),i=a.level&&n.has(a.level)?a.level:"",s=a.date_from||"",c=a.date_to||"",u=a.starred==="1",p=a.imported==="1"?"1":a.imported==="0"?"0":"",m=a.shared_by||null,v=a.sort||"timestamp",T=a.order==="asc"?"asc":"desc",R=Math.max(1,Number.parseInt(a.page??"1",10)||1),L=a.period||"all",l={period:s||c?null:L!=="all"?L:null,category:a.category||null,level:i||null,project:a.project||null,repository:a.repository||null,branch:a.branch||null,commit:a.commit||null,starred:u?true:null,imported:m||p==="1"?true:p==="0"?false:null,shared_by:m,search:a.search||null,feedback:a.feedback||null,date_from:s||null,date_to:c||null},x=E$1(k=>{let re=G(k,l),ae=Math.max(1,Math.ceil(re/Z));return R=Math.min(R,ae),{total:re,totalPages:ae,lessons:H(k,{...l,sort:v,order:T,page:R,per_page:Z}),allCategories:P$1(k),allProjects:M(k,"project"),allRepositories:M(k,"repository"),allBranches:M(k,"branch"),allCommits:M(k,"commit_hash"),allSharedBy:N(k),theme:X$1(k).ui_theme}}),$={period:L,category:a.category||"",level:i,project:a.project||"",repository:a.repository||"",branch:a.branch||"",commit:a.commit||"",starred:u,imported:m?"1":p,shared_by:m??"",search:a.search||"",feedback:a.feedback||"",date_from:s,date_to:c,sort:v,order:T},S=a.error==="invalid"||a.error==="cross"?a.error:null;return t.html(Qe({importOpen:a.import==="1",importError:S,lessons:x.lessons,allCategories:x.allCategories,allProjects:x.allProjects,allRepositories:x.allRepositories,allBranches:x.allBranches,allCommits:x.allCommits,allSharedBy:x.allSharedBy,s:$,page:R,perPage:Z,total:x.total,totalPages:x.totalPages,uiTheme:x.theme}))}),e$1.post("/lessons/:lesson_id/star",async t=>{let a=await t.req.parseBody();return E$1(n=>K$1(n,t.req.param("lesson_id"),a.starred==="1")),t.redirect(B(b(a,"next")||void 0),303)}),e$1.post("/lessons/:lesson_id/feedback",async t=>{let a=await t.req.parseBody(),n=b(a,"feedback");if(!["","clear","know","understood","dont_know"].includes(n))return t.text("Unknown feedback value",400);let i=n===""||n==="clear"?null:n;return E$1(s=>Ea(s,t.req.param("lesson_id"),i)),t.redirect(B(b(a,"next")||void 0),303)}),e$1.post("/lessons/delete",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody({all:true}),n=a.id,i=(Array.isArray(n)?n:[n]).filter(s=>typeof s=="string"&&s!=="");return E$1(s=>J$1(s,i)),t.redirect(B(b(a,"next")||void 0),303)}),e$1.on(["GET","POST"],"/lessons/:lesson_id/share",async t=>{if(E(t))return t.text("Forbidden",403);let a,n,i=false;if(t.req.method==="POST"){let m=await t.req.parseBody();a=b(m,"name"),n=b(m,"include_context")==="1",i=b(m,"persist")==="1";}else a=t.req.query("name"),n=t.req.query("include_context")==="1";let s=E$1(m=>{let v=O(m,t.req.param("lesson_id")??"");return v?(i&&a!==void 0&&Y$1(m,"share_name",x(a)),{lesson:v,settingName:X$1(m).share_name}):null});if(!s)return t.text("Lesson not found",404);let{state:c,payload:u}=it(s.lesson,s.settingName,{name:a,includeContext:n}),p=t.req.query("format");return p==="text"?t.text(c.text):p==="link"?t.text(c.link):p==="md"?new Response(Aa(u),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${za(u)}"`}}):p==="panel"?t.html(W(s.lesson.id,c,s.lesson.title)):t.html(V(c))}),e$1.get("/lessons/:lesson_id",t=>{let a=E$1(s=>{let c=O(s,t.req.param("lesson_id"));if(!c)return null;let u=X$1(s),p=h$1(s,{lesson_id:c.id})[0],m$1=p?{id:p.id,title:p.title,status:p.status,...m(p)}:null;return {lesson:c,course:m$1,settingName:u.share_name,uiTheme:u.ui_theme}});if(!a)return t.html("<h1>Lesson not found</h1>",404);let n=t.req.query(),i=n.imported==="1"?"new":n.imported==="dup"?"dup":null;return t.html(Xe({lesson:a.lesson,uiTheme:a.uiTheme,share:it(a.lesson,a.settingName,{open:n.share==="1"}).state,imported:i,course:a.course}))});let g$1={"content-type":"text/html; charset=utf-8","content-security-policy":"sandbox allow-scripts allow-forms; default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; style-src 'unsafe-inline'; img-src data:; font-src data:; form-action 'none'; base-uri 'none'; frame-ancestors 'self'","referrer-policy":"no-referrer","x-content-type-options":"nosniff","cache-control":"no-cache"},d$1=(()=>{try{return `
866
866
  <script>${readFileSync(join(X,"course-frame.js"),"utf8")}</script>
867
- `}catch{return ""}})();return e$1.get("/courses",t=>{let a=A(n=>({courses:h$1(n).map(i=>({...i,lesson_title:i.lesson_id?K$1(n,i.lesson_id)?.title??null:null})),uiTheme:T(n).ui_theme}));return t.html(Xe(a))}),e$1.get("/courses/:course_id/index.html",t=>{let a=t.req.param("course_id");if(!a$1.test(a))return t.notFound();let n=e(a);if(!n)return t.notFound();try{let i=Buffer.concat([readFileSync(n),Buffer.from(d)]);return new Response(new Uint8Array(i),{headers:new Headers(g$1)})}catch{return t.notFound()}}),e$1.get("/courses/:course_id",t=>{let a=t.req.param("course_id"),n=A(c=>{let u=g(c,a);return u?{course:u,lessonTitle:u.lesson_id?K$1(c,u.lesson_id)?.title??null:null,uiTheme:T(c).ui_theme}:null});if(!n)return t.html("<h1>Course not found</h1>",404);let i=n.course.steps.find(c=>c.status==="todo")?.position??1,s=Number.parseInt(t.req.query("step")??"",10)||i;return t.html(tt({...n,selected:s,hasDocument:e(a)!==null}))}),e$1.post("/courses/:course_id/steps/:position",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody(),n=b(a,"status");if(!["todo","done","skipped"].includes(n))return t.text("Unknown status",400);let i=Number.parseInt(t.req.param("position"),10),s=t.req.param("course_id");return A(u=>Number.isFinite(i)?j$1(u,s,i,n):null)?t.redirect(B(b(a,"next")||void 0,`/courses/${encodeURIComponent(s)}`),303):t.text("Step not found",404)}),e$1.post("/courses/delete",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody();return A(n=>l(n,b(a,"id"))),t.redirect("/courses",303)}),e$1.get("/settings/export",()=>{let t=A(a=>ka(a));return new Response(new Uint8Array(t),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),e$1.post("/settings/import",async t=>{let a=(await t.req.parseBody()).file;if(!(a instanceof File))return t.redirect("/settings?imported=0&skipped=0&invalid=1",303);let n=new Uint8Array(await a.arrayBuffer()),i=A(s=>ma(s,n));return t.redirect(`/settings?imported=${i.lessons}&skipped=${i.skipped}&invalid=${i.invalid}&groups=${i.groups}&notebook=${i.learning_state}`,303)}),e$1.get("/settings/notebook/download",()=>{let t=existsSync(o)?readFileSync(o,"utf8"):"";return new Response(t,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),e$1.post("/settings/notebook",async t=>{let a=await t.req.parseBody();return mkdirSync(dirname(o),{recursive:true}),writeFileSync(o,b(a,"content"),"utf8"),t.redirect(B(b(a,"next")||void 0,"/settings?notebook_saved=1"),303)}),e$1.post("/settings",async t$1=>{let a=await t$1.req.parseBody(),n=Number.parseInt(b(a,"max_per_day","2"),10)||2,i=Number.parseInt(b(a,"min_gap_minutes","240"),10),s=b(a,"ui_theme","system");["system","dark","light"].includes(s)||(s="system");let c=b(a,"ui_home","auto");["auto","lessons","knowledge"].includes(c)||(c="auto");let u=Math.max(0,Number.parseInt(b(a,"nudge_every","10"),10)||0),p=b(a,"nudge_scope","session");return p!=="session"&&p!=="global"&&(p="session"),A(m=>{U(m,"max_per_day",String(n)),U(m,"min_gap_minutes",String(Number.isNaN(i)?240:i)),U(m,"ui_theme",s),U(m,"ui_home",c),U(m,"nudge_every",String(u)),U(m,"nudge_scope",p),U(m,"share_name",t(b(a,"share_name")));}),t$1.redirect("/settings",303)}),e$1.get("/settings",t=>{let a=A(c=>T(c)),n=existsSync(o)?readFileSync(o,"utf8"):"",i=t.req.query(),s=i.imported!==void 0?{imported:Number(i.imported??0),skipped:Number(i.skipped??0),invalid:Number(i.invalid??0),groups:Number(i.groups??0),notebook:Number(i.notebook??0)}:null;return t.html(rt({settings:a,notebookContent:n,notebookPath:o,uiTheme:a.ui_theme,flash:s}))}),e$1}function $t(r,e=2e3){let g=r;return new Promise(d=>{let t=setTimeout(()=>{g.closeAllConnections?.();},e),a=setInterval(()=>g.closeIdleConnections?.(),100);r.close(()=>{clearTimeout(t),clearInterval(a),d();}),g.closeIdleConnections?.();})}async function _t(r,e=fetch){try{let g=await e(`http://127.0.0.1:${r}/ping`,{signal:AbortSignal.timeout(1500)});if(!g.ok)return null;let d=await g.json();return d.ok===!0?{version:String(d.version??"")}:null}catch{return null}}async function St(r,e,g=_t){if(e.code==="EADDRINUSE"){let d=await g(r);if(d){let t=`http://localhost:${r}`,a=d.version?` (v${d.version})`:"";return {existingUrl:t,exitCode:0,message:`${a$2.yellow("devcoach UI is already running at")} ${a$2.cyan(c(t))}${a}
867
+ `}catch{return ""}})();return e$1.get("/courses",t=>{let a=E$1(n=>({courses:h$1(n).map(i=>({...i,lesson_title:i.lesson_id?O(n,i.lesson_id)?.title??null:null})),uiTheme:X$1(n).ui_theme}));return t.html(rt(a))}),e$1.get("/courses/:course_id/index.html",t=>{let a=t.req.param("course_id");if(!a$1.test(a))return t.notFound();let n=e(a);if(!n)return t.notFound();try{let i=readFileSync(n),s=i.toString("utf8"),c$1=!c(s)&&d(s)?`
868
+ ${b$1()??""}`:"",u=Buffer.concat([i,Buffer.from(d$1+c$1)]);return new Response(new Uint8Array(u),{headers:new Headers(g$1)})}catch{return t.notFound()}}),e$1.get("/courses/:course_id",t=>{let a=t.req.param("course_id"),n=E$1(c=>{let u=g(c,a);return u?{course:u,lessonTitle:u.lesson_id?O(c,u.lesson_id)?.title??null:null,uiTheme:X$1(c).ui_theme}:null});if(!n)return t.html("<h1>Course not found</h1>",404);let i=n.course.steps.find(c=>c.status==="todo")?.position??1,s=Number.parseInt(t.req.query("step")??"",10)||i;return t.html(ot({...n,selected:s,hasDocument:e(a)!==null}))}),e$1.post("/courses/:course_id/steps/:position",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody(),n=b(a,"status");if(!["todo","done","skipped"].includes(n))return t.text("Unknown status",400);let i=Number.parseInt(t.req.param("position"),10),s=t.req.param("course_id");return E$1(u=>Number.isFinite(i)?j$1(u,s,i,n):null)?t.redirect(B(b(a,"next")||void 0,`/courses/${encodeURIComponent(s)}`),303):t.text("Step not found",404)}),e$1.post("/courses/delete",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody();return E$1(n=>l(n,b(a,"id"))),t.redirect("/courses",303)}),e$1.get("/settings/export",()=>{let t=E$1(a=>oa(a));return new Response(new Uint8Array(t),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),e$1.post("/settings/import",async t=>{let a=(await t.req.parseBody()).file;if(!(a instanceof File))return t.redirect("/settings?imported=0&skipped=0&invalid=1",303);let n=new Uint8Array(await a.arrayBuffer()),i=E$1(s=>qa(s,n));return t.redirect(`/settings?imported=${i.lessons}&skipped=${i.skipped}&invalid=${i.invalid}&groups=${i.groups}&notebook=${i.learning_state}`,303)}),e$1.get("/settings/notebook/download",()=>{let t=existsSync(s)?readFileSync(s,"utf8"):"";return new Response(t,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),e$1.post("/settings/notebook",async t=>{let a=await t.req.parseBody();return mkdirSync(dirname(s),{recursive:true}),writeFileSync(s,b(a,"content"),"utf8"),t.redirect(B(b(a,"next")||void 0,"/settings?notebook_saved=1"),303)}),e$1.post("/settings",async t=>{let a=await t.req.parseBody(),n=Number.parseInt(b(a,"max_per_day","2"),10)||2,i=Number.parseInt(b(a,"min_gap_minutes","240"),10),s=b(a,"ui_theme","system");["system","dark","light"].includes(s)||(s="system");let c=b(a,"ui_home","auto");["auto","lessons","knowledge"].includes(c)||(c="auto");let u=Math.max(0,Number.parseInt(b(a,"nudge_every","10"),10)||0),p=b(a,"nudge_scope","session");return p!=="session"&&p!=="global"&&(p="session"),E$1(m=>{Y$1(m,"max_per_day",String(n)),Y$1(m,"min_gap_minutes",String(Number.isNaN(i)?240:i)),Y$1(m,"ui_theme",s),Y$1(m,"ui_home",c),Y$1(m,"nudge_every",String(u)),Y$1(m,"nudge_scope",p),Y$1(m,"share_name",x(b(a,"share_name")));}),t.redirect("/settings",303)}),e$1.get("/settings",t=>{let a=E$1(c=>X$1(c)),n=existsSync(s)?readFileSync(s,"utf8"):"",i=t.req.query(),s$1=i.imported!==void 0?{imported:Number(i.imported??0),skipped:Number(i.skipped??0),invalid:Number(i.invalid??0),groups:Number(i.groups??0),notebook:Number(i.notebook??0)}:null;return t.html(st({settings:a,notebookContent:n,notebookPath:s,uiTheme:a.ui_theme,flash:s$1}))}),e$1}function Ct(r,e=2e3){let g=r;return new Promise(d=>{let t=setTimeout(()=>{g.closeAllConnections?.();},e),a=setInterval(()=>g.closeIdleConnections?.(),100);r.close(()=>{clearTimeout(t),clearInterval(a),d();}),g.closeIdleConnections?.();})}async function Tt(r,e=fetch){try{let g=await e(`http://127.0.0.1:${r}/ping`,{signal:AbortSignal.timeout(1500)});if(!g.ok)return null;let d=await g.json();return d.ok===!0?{version:String(d.version??"")}:null}catch{return null}}async function It(r,e,g=Tt){if(e.code==="EADDRINUSE"){let d=await g(r);if(d){let t=`http://localhost:${r}`,a=d.version?` (v${d.version})`:"";return {existingUrl:t,exitCode:0,message:`${a$2.yellow("devcoach UI is already running at")} ${a$2.cyan(c$1(t))}${a}
868
869
  ${a$2.dim("Open it there, start another with --port <n>, or stop it with: devcoach ui --stop")}`}}return {exitCode:1,message:`${a$2.red(`\u2717 Port ${r} is already in use by another process`)}
869
- ${a$2.dim("Pick another port: devcoach ui --port <n>")}`}}return e.code==="EACCES"?{exitCode:1,message:a$2.red(`\u2717 Port ${r} is not allowed (permission denied) \u2014 use a port \u2265 1024`)}:{exitCode:1,message:a$2.red(`\u2717 Could not start the devcoach UI on port ${r}: ${e.message}`)}}function qt(r,e={}){let g=false,d=(s,c)=>{if(g){c&&process.exit(130);return}g=true,process.stdout.isTTY&&console.log(a$2.dim(`devcoach UI stopping (${s})\u2026`)),$t(a).then(()=>{c&&process.exit(0);});},t=wt({onShutdown:()=>d("stop requested",false)}),a=serve({fetch:t.fetch,port:r,hostname:"127.0.0.1"},s=>{let c$1=`http://localhost:${s.port}`;console.log(`devcoach UI running at ${a$2.cyan(c(c$1))}`),process.stdout.isTTY&&console.log(a$2.dim("Press Ctrl+C to stop")),e.onReady?.(c$1);}),n=["SIGINT","SIGTERM","SIGHUP"],i={SIGINT:()=>d("SIGINT",true),SIGTERM:()=>d("SIGTERM",true),SIGHUP:()=>d("SIGHUP",true)};if(e.handleSignals!==false)for(let s of n)process.on(s,i[s]);return a.once("error",s=>{for(let c of n)process.off(c,i[c]);St(r,s).then(c=>{if(e.onListenError){e.onListenError(c);return}console.error(c.message),process.exit(c.exitCode);});}),a}async function Ft(r,e=fetch){try{return (await e(`http://127.0.0.1:${r}/shutdown`,{method:"POST",signal:AbortSignal.timeout(3e3)})).ok}catch{return false}}
870
- export{wt as createApp,St as explainListenError,$t as gracefulShutdown,_t as pingUi,kt as resolveHomePath,qt as startUi,Ft as stopUi};
870
+ ${a$2.dim("Pick another port: devcoach ui --port <n>")}`}}return e.code==="EACCES"?{exitCode:1,message:a$2.red(`\u2717 Port ${r} is not allowed (permission denied) \u2014 use a port \u2265 1024`)}:{exitCode:1,message:a$2.red(`\u2717 Could not start the devcoach UI on port ${r}: ${e.message}`)}}function Kt(r,e={}){let g=false,d=(s,c)=>{if(g){c&&process.exit(130);return}g=true,process.stdout.isTTY&&console.log(a$2.dim(`devcoach UI stopping (${s})\u2026`)),Ct(a).then(()=>{c&&process.exit(0);});},t=St({onShutdown:()=>d("stop requested",false)}),a=serve({fetch:t.fetch,port:r,hostname:"127.0.0.1"},s=>{let c=`http://localhost:${s.port}`;console.log(`devcoach UI running at ${a$2.cyan(c$1(c))}`),process.stdout.isTTY&&console.log(a$2.dim("Press Ctrl+C to stop")),e.onReady?.(c);}),n=["SIGINT","SIGTERM","SIGHUP"],i={SIGINT:()=>d("SIGINT",true),SIGTERM:()=>d("SIGTERM",true),SIGHUP:()=>d("SIGHUP",true)};if(e.handleSignals!==false)for(let s of n)process.on(s,i[s]);return a.once("error",s=>{for(let c of n)process.off(c,i[c]);It(r,s).then(c=>{if(e.onListenError){e.onListenError(c);return}console.error(c.message),process.exit(c.exitCode);});}),a}async function Yt(r,e=fetch){try{return (await e(`http://127.0.0.1:${r}/shutdown`,{method:"POST",signal:AbortSignal.timeout(3e3)})).ok}catch{return false}}
871
+ export{St as createApp,It as explainListenError,Ct as gracefulShutdown,Tt as pingUi,$t as resolveHomePath,Kt as startUi,Yt as stopUi};
package/dist/bin.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- var r=process.argv[2]??"",e=new Set(["stop-hook","prompt-hook","gemini-stop-hook","gemini-prompt-hook","codex-stop-hook","codex-prompt-hook","onboard-hook","lesson-ready"]);try{if(e.has(r)){let{runHook:o}=await import('./hooks-HLUPV2BO.js');o(r);}else {let{runCli:o}=await import('./commands-JRR4AYBW.js');await o();}}catch(o){o?.name==="SchemaTooNewError"&&(console.error(o.message),process.exit(3)),console.error(o),process.exit(1);}
2
+ var r=process.argv[2]??"",e=new Set(["stop-hook","prompt-hook","gemini-stop-hook","gemini-prompt-hook","codex-stop-hook","codex-prompt-hook","onboard-hook","lesson-ready"]);try{if(e.has(r)){let{runHook:o}=await import('./hooks-IWKZYIBR.js');o(r);}else {let{runCli:o}=await import('./commands-Z3R2H3GB.js');await o();}}catch(o){o?.name==="SchemaTooNewError"&&(console.error(o.message),process.exit(3)),console.error(o),process.exit(1);}
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import {r,E as E$1,B as B$1,ba,na,Ja,q,D,Ia}from'./chunk-K4GUNPZT.js';import {existsSync,mkdirSync,statSync,writeFileSync,appendFileSync,fstatSync,readFileSync}from'fs';import {join}from'path';function N(e){try{let o=JSON.parse(e);return {stop_hook_active:o?.stop_hook_active===!0,permission_mode:typeof o?.permission_mode=="string"?o.permission_mode:null,session_id:typeof o?.session_id=="string"?o.session_id:null}}catch{return {stop_hook_active:false,permission_mode:null,session_id:null}}}function l(){let e={stop_hook_active:false,permission_mode:null,session_id:null};try{let o=fstatSync(0);return !o.isFIFO()&&!o.isFile()&&!o.isSocket()?e:N(readFileSync(0,"utf8"))}catch{return e}}function $(e){return e.stop_hook_active||e.permission_mode==="plan"}function s(e,o,t){if(process.env.DEVCOACH_HOOK_DEBUG==="1")try{mkdirSync(q,{recursive:!0});let n=join(q,"hook.log");existsSync(n)&&statSync(n).size>256*1024&&writeFileSync(n,""),appendFileSync(n,`${new Date().toISOString()} ${e} ${(o??"-").slice(0,8)} ${t}
3
+ `);}catch{}}var E={claude:"block",gemini:"deny",codex:"block"};function u(e,o,t){let n={decision:E[e],reason:o};t&&(n.systemMessage=t),process.stdout.write(`${JSON.stringify(n)}
4
+ `),process.exit(0);}var g="devcoach: the user has no coaching profile yet. Ask them how they want to set it up and STRONGLY recommend Automatic \u2014 devcoach scans their full Claude Code history across all their projects (call get_onboarding for the detected stack and per-project evidence) so the profile reflects what they actually build. Also offer Guided (a short conversation to map topics and confidence levels) and Import (restore from a backup), but lead with Automatic; do not pick for them. After they choose, build the profile and save it by calling the devcoach `complete_onboarding` MCP tool (not a shell command) before ending your turn.";function I(e=l()){if($(e)&&process.exit(0),existsSync(r)){let o;try{o=E$1(t=>B$1(t)!==null||ba(t).knowledge_ready);}catch(t){s("onboard-hook",e.session_id,`error: ${t}`),process.exit(0);}o&&process.exit(0);}s("onboard-hook",e.session_id,"onboarding cue"),u("claude",g);}var a=10,w={claude:{cue:"Invoke the `devcoach` skill (Skill tool)",prime:"invoke the `devcoach` skill (Skill tool)"},gemini:{cue:"Activate the `devcoach` skill (activate_skill tool)",prime:"activate the `devcoach` skill (activate_skill tool)"},codex:{cue:"Activate the `devcoach` skill",prime:"activate the `devcoach` skill"}};function T(e,o="claude"){let t=e%a===0,n=Math.ceil(e/a)*a,i=t?`This is lesson #${e} \u2014 a notebook checkpoint (every ${a} lessons): after log_lesson returns the feedback, also read references/calibration.md in the devcoach skill and run its notebook-update step (a direct file write), recording "(updated after ${e} lessons)".`:`Skip the notebook update this time \u2014 it is refreshed only every ${a} lessons (next checkpoint at lesson #${n}).`;return `devcoach: a lesson is due for the technical work just completed. Do it now \u2014 do not acknowledge this message and do not explain what you are about to do.
5
+
6
+ ${w[o].cue} NOW and follow it exactly \u2014 the skill is the single source of truth for the entire flow, including the explicit no-op when the completed work does not warrant a lesson.
7
+ ${i}
8
+
9
+ If the devcoach skill is not available, fall back to: call get_briefing (one call: profile, taught topics, notebook); pick ONE untaught profile topic at or above the user's confidence band; call log_lesson silently (body = 3\u20136 short paragraphs tied to the task plus a \`\u{1F4A1} *Senior tip:*\` line, clean markdown without bands or a title line); THEN write the card as the FINAL message of the turn: plain markdown (no '>' blockquote) between two band headings \`### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u{1F393} devcoach \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\` and \`### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 [topic] \xB7 [level] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\`, with \`**[Title]** \xB7 [Category] \xB7 [Level]\`, the body you passed, and the tip \u2014 saving is not showing; the card in the final message is the only thing the user sees. If instead the work does not warrant a lesson (pure questions, chat, nothing technical), call the devcoach \`skip_lesson\` tool with a one-line reason.
10
+
11
+ The rate-limit check is already done by this hook \u2014 skip it. Work in silence: never narrate devcoach tool calls or resource reads ('checking your profile\u2026'). Output only the lesson card (or nothing). No preamble, no meta-commentary.`}function R(e,o,t){let n=o.permission_mode==="plan";if(o.stop_hook_active)return {kind:"silent",note:"hook-forced continuation"};if(B$1(e))return {kind:"silent",note:"read-only: database newer than this build"};if(!ba(e).knowledge_ready)return !t.onboardCue||n?{kind:"silent",note:"onboarding not complete"}:{kind:"onboard",note:"onboarding cue (empty knowledge)"};if(na(e))return {kind:"silent",note:"course in progress \u2014 cues paused"};let i=Ia(e,o.session_id,{planMode:n});return i.cue?{kind:"cue",nextLessonNumber:i.nextLessonNumber,note:i.reason}:{kind:"silent",note:i.reason}}function C(e,o,t){let n;try{n=E$1(i=>D(i,()=>R(i,o,t)));}catch(i){s(e,o.session_id,`error: ${i}`),process.exit(0);}switch(s(e,o.session_id,n.note),n.kind){case "onboard":u(t.client,g);break;case "cue":u(t.client,T(n.nextLessonNumber,t.client),"\u{1F393} devcoach: checking whether a lesson is due\u2026");break;}process.exit(0);}function B(e=l()){existsSync(r)||process.exit(0),C("lesson-ready",e,{onboardCue:false,client:"claude"});}function f(e=l(),o="claude"){let t=o==="claude"?"stop-hook":`${o}-stop-hook`;existsSync(r)||((e.stop_hook_active||e.permission_mode==="plan")&&process.exit(0),s(t,e.session_id,"onboarding cue (no DB)"),u(o,g)),C(t,e,{onboardCue:true,client:o});}function L(e="claude"){return `devcoach: the lesson pacing threshold will be reached at the end of this turn. Resolve it before finishing:
12
+ - If this task is technical: ${w[e].prime} and follow it exactly to close your reply \u2014 the skill is the single source of truth for the entire lesson flow.
13
+ - If it is not technical: silently call the devcoach \`skip_lesson\` tool with a one-line reason and output nothing about it.
14
+ - Either way, work in silence: never narrate devcoach tool calls or resource reads ('checking your profile\u2026').`}var F={claude:"UserPromptSubmit",gemini:"BeforeAgent",codex:"UserPromptSubmit"};function b(e=l(),o="claude"){let t=o==="claude"?"prompt-hook":`${o}-prompt-hook`;e.permission_mode==="plan"&&process.exit(0),existsSync(r)||process.exit(0);let n;try{n=E$1(i=>B$1(i)===null&&!na(i)&&Ja(i,e.session_id).wouldCue);}catch(i){s(t,e.session_id,`error: ${i}`),process.exit(0);}s(t,e.session_id,n?"priming":"no prime"),n||process.exit(0),process.stdout.write(`${JSON.stringify({hookSpecificOutput:{hookEventName:F[o],additionalContext:L(o)}})}
15
+ `),process.exit(0);}function K(e,o){let t=()=>o??l();switch(e){case "stop-hook":f(t());break;case "gemini-stop-hook":f(t(),"gemini");break;case "codex-stop-hook":f(t(),"codex");break;case "prompt-hook":b(t());break;case "gemini-prompt-hook":b(t(),"gemini");break;case "codex-prompt-hook":b(t(),"codex");break;case "onboard-hook":I(t());break;case "lesson-ready":B(t());break;default:process.exit(0);}}export{N as a,I as b,T as c,B as d,f as e,L as f,b as g,K as h};