devcoach 2.6.3 → 2.7.0-next.604.g26d3007
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.
- package/assets/references/course.md +1 -31
- package/dist/{app-J5OHP5HJ.js → app-HYACQCHI.js} +27 -28
- package/dist/bin.js +1 -1
- package/dist/chunk-75PGZUMS.js +2 -0
- package/dist/chunk-HFG5R5JB.js +111 -0
- package/dist/chunk-ZFX3B4HE.js +15 -0
- package/dist/{commands-4RHEYCFS.js → commands-JRR4AYBW.js} +13 -13
- package/dist/{hooks-WFB6SJ3D.js → hooks-HLUPV2BO.js} +1 -1
- package/dist/server-7J2GDOFJ.js +9 -0
- package/package.json +2 -3
- package/THIRD_PARTY_NOTICES.md +0 -26
- package/assets/static/vendor/highlight.LICENSE.txt +0 -29
- package/dist/chunk-LNOPC5QA.js +0 -118
- package/dist/chunk-WETN2PWT.js +0 -2
- package/dist/chunk-WUAD3KLD.js +0 -15
- package/dist/server-ZHIAMBWN.js +0 -9
|
@@ -110,33 +110,6 @@ 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
|
-
|
|
140
113
|
**Design — conform to the dashboard, do not copy it.** One measure (~68ch), generous spacing,
|
|
141
114
|
quiet Tufte-like typography, light + dark. Every colour is a **token** on `:root`; nothing is
|
|
142
115
|
hard-coded further down (no stray `#fff` on a button, no grey typed by hand in a rule):
|
|
@@ -188,15 +161,12 @@ Skeleton (adapt, keep the ids, the class names and the token names):
|
|
|
188
161
|
<nav class="steps"><a href="#step-1">1 · Sums</a><a href="#step-2">2 · Powers</a>
|
|
189
162
|
<button class="theme-switch" type="button"></button></nav>
|
|
190
163
|
<main>
|
|
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) => a + b, 0);</code></pre>
|
|
193
|
-
<div class="check">…<button>Reveal</button></div>
|
|
164
|
+
<section id="step-1"><h2>1 · Sums</h2><p class="goal">…</p> … <div class="check">…<button>Reveal</button></div>
|
|
194
165
|
<div class="pager"><a href="#step-1">← Previous</a><a href="#step-2">Next →</a></div>
|
|
195
166
|
<p class="ask">Stuck? Ask in the chat — that's where the course is being taught.</p></section>
|
|
196
167
|
<section id="step-2">…</section>
|
|
197
168
|
</main>
|
|
198
169
|
</div>
|
|
199
|
-
<script data-devcoach="highlighter"></script>
|
|
200
170
|
<script>
|
|
201
171
|
// show one section: on load + hashchange, toggle .current on the section and its nav link, scrollTo(0,0)
|
|
202
172
|
// reveal buttons; the live examples (new Function inside try/catch) or the simulations
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {a as a$2,c
|
|
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">${
|
|
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>
|
|
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 as c$1}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as
|
|
|
130
130
|
</script>
|
|
131
131
|
${r.scripts??""}
|
|
132
132
|
</body>
|
|
133
|
-
</html>`}function
|
|
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`
|
|
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=
|
|
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`
|
|
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
|
|
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 ${$}">
|
|
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(
|
|
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>`})}
|
|
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?`${
|
|
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>
|
|
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(
|
|
371
|
+
${e.feedback?c(Ge[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(`${
|
|
376
|
+
${e.level?c(`${ze[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=
|
|
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'">
|
|
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
|
-
${
|
|
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>`)}
|
|
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
|
|
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">
|
|
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
|
|
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`
|
|
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
|
|
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">
|
|
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 ${
|
|
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>
|
|
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
|
|
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`
|
|
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">${
|
|
680
|
+
<td class="px-3 py-3">${Qe(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
|
|
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>
|
|
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
|
|
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`
|
|
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
|
-
${
|
|
700
|
+
${Qe(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"}">${
|
|
709
|
+
<span class="${s.status==="done"?"text-green-500":s.status==="skipped"?"text-gray-400":"text-accent-400"}">${ct[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
|
|
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`
|
|
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
|
-
${
|
|
757
|
+
${lt.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,10 +862,9 @@ 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"),
|
|
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 `
|
|
866
866
|
<script>${readFileSync(join(X,"course-frame.js"),"utf8")}</script>
|
|
867
|
-
`}catch{return ""}})();return e$1.get("/courses",t=>{let a=
|
|
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}¬ebook=${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}
|
|
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}¬ebook=${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}
|
|
869
868
|
${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`)}
|
|
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
|
|
871
|
-
export{
|
|
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};
|
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-
|
|
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);}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {p,q,la,$ as $$1,aa,ca,ba,z,da,ea,ha,fa,ga,ia,na,ua,a}from'./chunk-HFG5R5JB.js';import {lstatSync,mkdirSync,readFileSync,rmSync,existsSync}from'fs';import {join,sep,basename}from'path';import {execFileSync}from'child_process';import nt from'dns';import rt from'http';import ot from'https';import {isIP}from'net';var w=/^[a-z0-9-]+$/,X=q,h=class extends Error{};function Y(e){if(!w.test(e))throw new h(`Invalid course id '${e}'`)}function C(e){return Y(e),join(p,e)}function M(e){return join(C(e),X)}function J(e){let t=M(e);if(!t.startsWith(C(e)+sep))return null;try{let n=lstatSync(t);return !n.isFile()||n.size>la?null:t}catch{return null}}function Q(e,t){let n=ua(t,"course"),r=n;for(let o=2;aa(e,r)!==null||existsSync(join(p,r));o++)r=`${n}-${o}`;return r}function dt(e,t){let n=new Date().toISOString(),r={id:Q(e,t.title),lesson_id:t.lesson_id??null,topic_id:t.topic_id,title:t.title,goal:t.goal??null,prerequisites:t.prerequisites??[],status:"active",created_at:n,updated_at:n,completed_at:null};return $$1(e,r),mkdirSync(C(r.id),{recursive:true}),{...r,steps:[]}}function $(e,t){if(!w.test(t))return null;let n=aa(e,t);return n?{...n,steps:ca(e,t)}:null}function St(e,t={}){return ba(e,t).map(n=>({...n,steps:ca(e,n.id)}))}function wt(e,t,n){return z(e,()=>{let r=$(e,t);if(!r)throw new h(`Course '${t}' not found`);let o=r.steps.length,i=n.after??o;if(!Number.isInteger(i)||i<0||i>o)throw new h(`'after' must be between 0 and ${o} (the course has ${o} steps)`);let a=J(t);if(!a)throw new h(`Course '${t}' has no document yet \u2014 write ${M(t)} first`);let f=readFileSync(a,"utf8");if(!new RegExp(`\\sid=["']${n.anchor}["']`).test(f))throw new h(`No element with id="${n.anchor}" in ${a}`);if(r.steps.some(s=>s.anchor===n.anchor))throw new h(`Step anchor '${n.anchor}' is already registered`);let l={course_id:t,position:i+1,title:n.title,kind:n.kind,anchor:n.anchor,status:"todo",done_at:null};return i<o&&da(e,t,i+1),ea(e,l),ha(e,t,new Date().toISOString()),l})}function Ct(e,t,n,r){let o=new Date().toISOString();return z(e,()=>{if(!fa(e,t,n,r,r==="done"?o:null))return null;let a=ca(e,t),f=a.length>0&&a.every(s=>s.status!=="todo"),l=aa(e,t);return l&&f&&l.status==="active"?ga(e,t,"completed",o):l&&!f&&l.status==="completed"?ga(e,t,"active",o):ha(e,t,o),$(e,t)})}function yt(e,t,n){return w.test(t)&&ga(e,t,n,new Date().toISOString())?$(e,t):null}function bt(e,t){if(!w.test(t))return false;let n=ia(e,t);return rmSync(C(t),{recursive:true,force:true}),n}function xt(e){return {done:e.steps.filter(t=>t.status!=="todo").length,total:e.steps.length}}function y(...e){try{return execFileSync("git",e,{encoding:"utf8",timeout:3e3,stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}}var F={"github.com":"github","gitlab.com":"gitlab","bitbucket.org":"bitbucket"};function et(e){if(!e)return [null,null];let t=/^git@([^:]+):(.+?)(?:\.git)?$/.exec(e);if(t){let r=(t[1]??"").toLowerCase();return [t[2]??e,F[r]??"local"]}let n=/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/.exec(e);if(n){let r=(n[1]??"").toLowerCase();return [n[2]??e,F[r]??"local"]}return [e,"local"]}function Dt(){return y("config","user.name")}function $t(){let e=process.cwd(),t=y("rev-parse","--abbrev-ref","HEAD"),n=y("rev-parse","HEAD"),r=y("remote","get-url","origin"),[o,i]=et(r),a;return o?a=o.replace(/\/+$/,"").split("/").pop()??null:a=basename(e)||null,t==="HEAD"&&(t=null),{project:a,repository:o,branch:t,commit_hash:n,folder:e,repository_platform:i}}var W="That URL is too large to be a lesson.",b="Only public http(s) URLs can be imported.",st=new Set([301,302,303,307,308]);function it(e){return /^https?:\/\/\S+$/i.test(e.trim())}function B(e){let[t=0,n=0]=e.split(".").map(Number);return t===0||t===10||t===127||t===169&&n===254||t===172&&n>=16&&n<=31||t===192&&n===168||t===100&&n>=64&&n<=127||t>=224}function G(e){let t=e.replace(/^\[|\]$/g,"").replace(/%.*$/,"").toLowerCase();if(isIP(t)===4)return B(t);if(isIP(t)!==6)return true;let n=/^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(t);if(n?.[1])return B(n[1]);if(t==="::"||t==="::1")return true;let r=t.split(":")[0]??"";return !!(/^f[cd]/.test(r)||/^fe[89ab]/.test(r))}async function at(e){return (await nt.promises.lookup(e,{all:true})).map(n=>n.address)}async function ut(e,t){let n=e.hostname.replace(/^\[|\]$/g,"").toLowerCase();if(n==="localhost"||n.endsWith(".localhost"))throw new na(b);if(isIP(n)){if(G(n))throw new na(b);return n}let r;try{r=await t(n);}catch{throw new na("The URL could not be reached \u2014 copy the lesson text instead.")}let o=r[0];if(!o||r.some(i=>G(i)))throw new na(b);return o}var lt=(e,t,n)=>new Promise((r,o)=>{let i=isIP(t)===6?6:4,a$1=(s,c,m)=>{c.all?m(null,[{address:t,family:i}]):m(null,t,i);},l=(e.protocol==="https:"?ot:rt).request(e,{method:"GET",lookup:a$1,signal:n,headers:{accept:"text/plain, text/markdown, application/json;q=0.9, */*;q=0.5","user-agent":`devcoach/${a}`}},s=>{let c=s.headers["content-length"];r({status:s.statusCode??0,location:s.headers.location??null,contentLength:c===void 0?null:Number(c),body:s,abort:()=>s.destroy()});});l.on("error",o),l.end();});async function ct(e,t){if(e.contentLength!==null&&e.contentLength>t)throw e.abort(),new na(W);let n=[],r=0;for await(let o of e.body){if(r+=o.byteLength,r>t)throw e.abort(),new na(W);n.push(o);}return Buffer.concat(n).toString("utf8")}async function kt(e,t={}){let n=t.timeoutMs??5e3,r=t.maxBytes??256*1024,o=t.maxRedirects??3,i=t.lookup??at,a=t.transport??lt;if(!it(e))throw new na("Only http(s) URLs can be imported.");let f=new AbortController,l=setTimeout(()=>f.abort(),n);try{let s=new URL(e.trim());for(let c=0;;c++){let m=await ut(s,i),p=await a(s,m,f.signal);if(st.has(p.status)){if(p.abort(),!p.location||c>=o)throw new na("The URL redirects too many times \u2014 copy the lesson text instead.");if(s=new URL(p.location,s),!/^https?:$/.test(s.protocol))throw new na(b);continue}if(p.status<200||p.status>=300)throw p.abort(),new na(`The URL answered ${p.status} \u2014 nothing to import there.`);return await ct(p,r)}}catch(s){if(s instanceof na)throw s;let c=s?.name==="AbortError"?"timed out":"could not be reached";throw new na(`The URL ${c} \u2014 copy the lesson text instead.`)}finally{clearTimeout(l);}}export{w as a,h as b,C as c,M as d,J as e,dt as f,$ as g,St as h,wt as i,Ct as j,yt as k,bt as l,xt as m,Dt as n,$t as o,it as p,kt as q};
|