ronin-skillforce 0.1.0 → 0.2.0-beta.0
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/CHANGELOG.md +48 -0
- package/LICENSE +202 -21
- package/NOTICE +19 -0
- package/README.md +38 -5
- package/bin/cli.js +15 -3
- package/bin/commands/archive.js +192 -0
- package/bin/commands/capstone-status.js +82 -0
- package/bin/commands/create.js +69 -12
- package/bin/commands/import.js +5 -5
- package/bin/commands/status.js +1 -1
- package/bin/commands/studio-autostart.js +414 -0
- package/bin/commands/studio-state.js +159 -0
- package/bin/commands/studio.js +351 -17
- package/bin/commands/trail.js +153 -3
- package/bin/commands/web.js +8 -10
- package/bin/course-reader.js +6 -15
- package/bin/platforms/_shared.js +154 -28
- package/bin/platforms/claude.js +23 -42
- package/bin/platforms/cursor.js +23 -20
- package/bin/platforms/gemini.js +18 -19
- package/bin/platforms/opencode.js +47 -28
- package/package.json +5 -4
- package/plugin.json +1 -1
- package/shared/SKILL.md +102 -72
- package/shared/agents/coach.md +19 -16
- package/shared/agents/navigator.md +27 -34
- package/shared/agents/professor.md +16 -34
- package/shared/agents/researcher.md +1 -1
- package/shared/agents/spotter.md +3 -4
- package/shared/commands/professor/archive.md +65 -72
- package/shared/commands/professor/capstone-review.md +5 -5
- package/shared/commands/professor/capstone.md +3 -3
- package/shared/commands/professor/done.md +59 -31
- package/shared/commands/professor/hint.md +14 -2
- package/shared/commands/professor/navigator.md +27 -1
- package/shared/commands/professor/new-topic.md +3 -3
- package/shared/commands/professor/next.md +9 -5
- package/shared/commands/professor/note.md +6 -6
- package/shared/commands/professor/progress.md +1 -1
- package/shared/commands/professor/publish.md +5 -5
- package/shared/commands/professor/quiz.md +2 -2
- package/shared/commands/professor/recall.md +1 -1
- package/shared/commands/professor/review.md +14 -0
- package/shared/commands/professor/skill-export.md +3 -4
- package/shared/commands/professor/spotter.md +21 -4
- package/shared/commands/professor/stuck.md +24 -0
- package/shared/commands/professor/switch.md +1 -1
- package/shared/commands/professor/syllabus.md +1 -1
- package/shared/commands/professor/template-export.md +2 -2
- package/shared/commands/professor/worktrees.md +3 -7
- package/shared/hooks/pre-compact.js +4 -15
- package/studio/dist/assets/BetaReportPanel-BJ9HIAnz.js +1 -0
- package/studio/dist/assets/{Capstone-Cv9kH6LY.js → Capstone-CoUU-jP7.js} +1 -1
- package/studio/dist/assets/{CodeWorkspace-BwCEINWo.js → CodeWorkspace-BNrL4qvR.js} +2 -2
- package/studio/dist/assets/{Course-BJ_hafUP.js → Course-BMGTXus_.js} +1 -1
- package/studio/dist/assets/{Editor-CV0mNhlz.js → Editor-Cw9j3cnp.js} +1 -1
- package/studio/dist/assets/{Home-C1ry89ak.js → Home-D2woITq5.js} +1 -1
- package/studio/dist/assets/{Lesson-2e9TD9yL.js → Lesson-CYQhrDCZ.js} +2 -2
- package/studio/dist/assets/{NewCourse-BZeOg2xn.js → NewCourse-CMpwVRQb.js} +1 -1
- package/studio/dist/assets/{Progress-CRc_xEWC.js → Progress-2rS19Fur.js} +1 -1
- package/studio/dist/assets/{Review-6QEbHEfk.js → Review-SlqK-hjp.js} +1 -1
- package/studio/dist/assets/{Settings-NFnqVQui.js → Settings-t42u7n9H.js} +1 -1
- package/studio/dist/assets/{TutorPanel-DXQ6Z3BY.js → TutorPanel-DQDWIU7f.js} +3 -3
- package/studio/dist/assets/{abnfDiagram-WEYOIA3C-qeSjKlGM.js → abnfDiagram-WEYOIA3C-jTUmGBuB.js} +1 -1
- package/studio/dist/assets/{anthropic-D7NiQCUH.js → anthropic-iIm2l85r.js} +3 -3
- package/studio/dist/assets/{architecture-WOLXFQ4H-COG-m35l.js → architecture-WOLXFQ4H-Ch9Ld_vv.js} +1 -1
- package/studio/dist/assets/{architectureDiagram-ZHVYKT53-VO7uKoeD.js → architectureDiagram-ZHVYKT53-Dy-z6wsy.js} +1 -1
- package/studio/dist/assets/{blockDiagram-RJAE7QA3-BGJF_qiL.js → blockDiagram-RJAE7QA3-2fHVrc6N.js} +1 -1
- package/studio/dist/assets/{bridge-BTh0AeNX.js → bridge-Cr0q4rLp.js} +1 -1
- package/studio/dist/assets/{c4Diagram-XDPPPE54-C-S10RgU.js → c4Diagram-XDPPPE54-BuuKlG4V.js} +1 -1
- package/studio/dist/assets/{capstone-Amn6Hztq.js → capstone-DhwBeNqk.js} +1 -1
- package/studio/dist/assets/{card-store-BrbrtX9w.js → card-store-CVMBc-Pv.js} +1 -1
- package/studio/dist/assets/channel-M_JkdHrL.js +1 -0
- package/studio/dist/assets/{chunk-2E7WHVMD-fv-iqTyC.js → chunk-2E7WHVMD-lZdQWttk.js} +1 -1
- package/studio/dist/assets/{chunk-4EMNKR2T-CY9IZrgv.js → chunk-4EMNKR2T-C6kbMwke.js} +1 -1
- package/studio/dist/assets/{chunk-55XCU76K-DyTZz7v_.js → chunk-55XCU76K-rkBzkZog.js} +1 -1
- package/studio/dist/assets/{chunk-7S6XSTFO-BNMzTLRW.js → chunk-7S6XSTFO-Skg347QF.js} +1 -1
- package/studio/dist/assets/{chunk-AQDTSKZB-CEYANohH.js → chunk-AQDTSKZB-0pSDBJ23.js} +1 -1
- package/studio/dist/assets/{chunk-DSNLSHJZ-CDk5EXjH.js → chunk-DSNLSHJZ-BaeQDl2i.js} +1 -1
- package/studio/dist/assets/{chunk-HAGFOTED-hNAi1zh_.js → chunk-HAGFOTED-BEm00juw.js} +1 -1
- package/studio/dist/assets/{chunk-HNE4Q36P-COJnSYoZ.js → chunk-HNE4Q36P-DyCR-LvS.js} +1 -1
- package/studio/dist/assets/{chunk-NQOKRVIF-BU6KVxRr.js → chunk-NQOKRVIF-BNJ9uMmB.js} +1 -1
- package/studio/dist/assets/{chunk-UHW7PFBP-DYvxL9JG.js → chunk-UHW7PFBP-DqJczBLs.js} +1 -1
- package/studio/dist/assets/{chunk-VQ5W62QI-CuRfk65j.js → chunk-VQ5W62QI-CVWYSP5Q.js} +1 -1
- package/studio/dist/assets/{chunk-WQQL4CDK-KaG7x-Mb.js → chunk-WQQL4CDK-DJf0Dx05.js} +1 -1
- package/studio/dist/assets/{chunk-YZO67HFG-DbuqIZmk.js → chunk-YZO67HFG-BFUm_JHA.js} +1 -1
- package/studio/dist/assets/{chunk-ZAUX2LTX-bk4ivUmc.js → chunk-ZAUX2LTX-C5ETkkSE.js} +1 -1
- package/studio/dist/assets/{chunk-ZCHMQSOR-B_LElfwh.js → chunk-ZCHMQSOR-WVX9r9PZ.js} +1 -1
- package/studio/dist/assets/{classDiagram-v2-4MYWR3UA-DtCJ6H_X.js → classDiagram-v2-4MYWR3UA-CI77SpuR.js} +1 -1
- package/studio/dist/assets/{course-export-DpaOTT3a.js → course-export-C8jSPuda.js} +1 -1
- package/studio/dist/assets/{cynefin-EF2NZ3EQ-D9btHJvU.js → cynefin-EF2NZ3EQ-CLffmM25.js} +1 -1
- package/studio/dist/assets/{cynefinDiagram-WF24UDRP-CsFJuv27.js → cynefinDiagram-WF24UDRP-BvLwocOb.js} +1 -1
- package/studio/dist/assets/{dagre-NK4GIZVX-Cg1Duec7.js → dagre-NK4GIZVX-C2nMAVdh.js} +1 -1
- package/studio/dist/assets/{diagram-CBK3MWTC-_KU2m5gd.js → diagram-CBK3MWTC-Bid-pdax.js} +1 -1
- package/studio/dist/assets/{diagram-I5XIIV2D-5RaBrwUA.js → diagram-I5XIIV2D-DYJeAvzl.js} +1 -1
- package/studio/dist/assets/{diagram-ODIL2UCW-C0VKxoD3.js → diagram-ODIL2UCW-B06X2r_F.js} +1 -1
- package/studio/dist/assets/{diagram-PFBEJUXE-BaHcZEP2.js → diagram-PFBEJUXE-CwY--5Rz.js} +1 -1
- package/studio/dist/assets/{diagram-RJA3I4VV-cwSAZ90p.js → diagram-RJA3I4VV-4SqhH8zw.js} +1 -1
- package/studio/dist/assets/{diagram-TYUNFSNR-BJImxCEN.js → diagram-TYUNFSNR-CrW4AIM9.js} +1 -1
- package/studio/dist/assets/{ebnfDiagram-V4P3TTPT-OQQLRXcm.js → ebnfDiagram-V4P3TTPT-xeoCmH-d.js} +1 -1
- package/studio/dist/assets/{elk-QONTBCTQ-BJGhQHxQ.js → elk-QONTBCTQ-Dadmz6iS.js} +1 -1
- package/studio/dist/assets/{erDiagram-KYQIGQAA-D91EKSep.js → erDiagram-KYQIGQAA-CSuH0bbF.js} +1 -1
- package/studio/dist/assets/{eventmodeling-K75KTNOO-DgdTRgnw.js → eventmodeling-K75KTNOO-CciVfx3I.js} +1 -1
- package/studio/dist/assets/flowDiagram-IUGLLWBU-DgNR54qK.js +1 -0
- package/studio/dist/assets/{ganttDiagram-R7GPNTJY-CSYLPWEI.js → ganttDiagram-R7GPNTJY-C0wtA-2y.js} +1 -1
- package/studio/dist/assets/{gitGraph-VSP46ZUC-D6GWaV9y.js → gitGraph-VSP46ZUC-BXoDzaA3.js} +1 -1
- package/studio/dist/assets/{gitGraphDiagram-PDYL7XHY-CaV31bvT.js → gitGraphDiagram-PDYL7XHY-vZBlNRPe.js} +1 -1
- package/studio/dist/assets/{index-DioXTZVc.js → index-CS8XPYbB.js} +2 -2
- package/studio/dist/assets/{info-OHQRW6UA-BN-x8GoN.js → info-OHQRW6UA-bs2Qpf7P.js} +1 -1
- package/studio/dist/assets/{infoDiagram-MVDRKAX5-CIose_cy.js → infoDiagram-MVDRKAX5-rPJio6-1.js} +1 -1
- package/studio/dist/assets/{ishikawaDiagram-HPEHV2GE-DCihSNBw.js → ishikawaDiagram-HPEHV2GE-CXf9XFff.js} +1 -1
- package/studio/dist/assets/{journeyDiagram-BOSHW7O7-Bz3o3RIX.js → journeyDiagram-BOSHW7O7-C3MSH79o.js} +1 -1
- package/studio/dist/assets/{kanban-definition-FNSA4HC5-BUyqvlFE.js → kanban-definition-FNSA4HC5-Bdh7SB4n.js} +1 -1
- package/studio/dist/assets/{line-B08T8KlK.js → line-CHkoQXJF.js} +1 -1
- package/studio/dist/assets/{mermaid-parser.core-BYwreSUy.js → mermaid-parser.core-Do1oxD1S.js} +3 -3
- package/studio/dist/assets/{mermaid.core-DTO1oDmq.js → mermaid.core-DgZUD0sU.js} +4 -4
- package/studio/dist/assets/{mindmap-definition-6OY57UG3-BGLwKheY.js → mindmap-definition-6OY57UG3-PUaK6oE_.js} +1 -1
- package/studio/dist/assets/{node.browser-BISniYn8.js → node.browser-DkpMze_r.js} +1 -1
- package/studio/dist/assets/{ollama-De5w84tG.js → ollama-bJNp3jaD.js} +1 -1
- package/studio/dist/assets/{openai-compatible-Joy1oK3L.js → openai-compatible-0tmAyMlN.js} +1 -1
- package/studio/dist/assets/{packet-JDAUHWVQ-CDIMjHS_.js → packet-JDAUHWVQ-vf2FtqFr.js} +1 -1
- package/studio/dist/assets/{pegDiagram-ECIUJNBX-DWXm4mwS.js → pegDiagram-ECIUJNBX-NEVrgPTY.js} +1 -1
- package/studio/dist/assets/{pie-XZMESJXO-CY3QQiKZ.js → pie-XZMESJXO-BIWydVNZ.js} +1 -1
- package/studio/dist/assets/{pieDiagram-GNMPY74T-M1aCnI7W.js → pieDiagram-GNMPY74T-BI9_u6rT.js} +1 -1
- package/studio/dist/assets/{quadrantDiagram-D57K4MC3-BFFtO7U5.js → quadrantDiagram-D57K4MC3-ryNpoBC5.js} +1 -1
- package/studio/dist/assets/{radar-ABXABTNO-DT0W4_Xn.js → radar-ABXABTNO-D8TjnvBB.js} +1 -1
- package/studio/dist/assets/{railroad-I3PHUGI6-_VCpgZR5.js → railroad-I3PHUGI6-BmEDV5kf.js} +1 -1
- package/studio/dist/assets/{railroad-abnf-LNEFI6M7-DkSpQ5YY.js → railroad-abnf-LNEFI6M7-Cs_sArMc.js} +1 -1
- package/studio/dist/assets/{railroad-ebnf-DWYD2UWJ-BqWILdGb.js → railroad-ebnf-DWYD2UWJ-DYuCq46d.js} +1 -1
- package/studio/dist/assets/{railroad-peg-JR7BVNR7-BB0bQL-Z.js → railroad-peg-JR7BVNR7-BcXSgdMy.js} +1 -1
- package/studio/dist/assets/{railroadDiagram-Y72NRBL7-CjLKjIdJ.js → railroadDiagram-Y72NRBL7-DBcc8b2c.js} +1 -1
- package/studio/dist/assets/{rehype-katex-C-sKddsH.js → rehype-katex-BfUIq5As.js} +1 -1
- package/studio/dist/assets/{remark-math-C-ox67hE.js → remark-math-XvZOjmDO.js} +1 -1
- package/studio/dist/assets/{requirementDiagram-EQYSW5RA-CnwzZqR8.js → requirementDiagram-EQYSW5RA-CVcBgM1L.js} +1 -1
- package/studio/dist/assets/{sankeyDiagram-SY24JHO5-CPDugTUl.js → sankeyDiagram-SY24JHO5-DjZ81hDs.js} +1 -1
- package/studio/dist/assets/{sequenceDiagram-BLEHNHQU-1dLfeU9Y.js → sequenceDiagram-BLEHNHQU-BNwOa2S7.js} +1 -1
- package/studio/dist/assets/{stateDiagram-v2-A6IESKWZ-DR7CmEjj.js → stateDiagram-v2-A6IESKWZ-BFWa5lEx.js} +1 -1
- package/studio/dist/assets/{swimlanes-B3ENRKFQ-BAWB9omQ.js → swimlanes-B3ENRKFQ-LfM_Jg6Z.js} +1 -1
- package/studio/dist/assets/swimlanesDiagram-AYQRV4AK-BDmpoiGp.js +8 -0
- package/studio/dist/assets/{timeline-definition-JXYKGLGB-DNkj8z77.js → timeline-definition-JXYKGLGB-BwGzkWgZ.js} +1 -1
- package/studio/dist/assets/{treeView-D4JQ5SDB-BA-ouhMb.js → treeView-D4JQ5SDB-MLk3VuRt.js} +1 -1
- package/studio/dist/assets/{treemap-SAJKECNS-BrrGxsAb.js → treemap-SAJKECNS-D8iSjp7L.js} +1 -1
- package/studio/dist/assets/{usecaseDiagram-WPOLBNNX-BB3D0Dt_.js → usecaseDiagram-WPOLBNNX-BqxpaNlT.js} +1 -1
- package/studio/dist/assets/{vennDiagram-Q3Y3IDCW-BrtqtKTE.js → vennDiagram-Q3Y3IDCW-CpeUDMhQ.js} +1 -1
- package/studio/dist/assets/{wardley-7MLQ67FV-BKkemDql.js → wardley-7MLQ67FV-tAMbCZAj.js} +1 -1
- package/studio/dist/assets/{wardleyDiagram-IVAIDKWA-DFnI3P5z.js → wardleyDiagram-IVAIDKWA-CDJViWzt.js} +1 -1
- package/studio/dist/assets/{xychartDiagram-4URNHDU4-Da0DZG_q.js → xychartDiagram-4URNHDU4-CwaQrd3_.js} +1 -1
- package/studio/dist/index.html +1 -1
- package/templates/claude/plugin.json +1 -1
- package/templates/gemini/GEMINI.md +18 -16
- package/web/server.js +3 -3
- package/studio/dist/assets/BetaReportPanel-8NrDLiPm.js +0 -1
- package/studio/dist/assets/channel-SJiUQu_N.js +0 -1
- package/studio/dist/assets/flowDiagram-IUGLLWBU-CVnJYMum.js +0 -1
- package/studio/dist/assets/swimlanesDiagram-AYQRV4AK-qeuHru16.js +0 -8
package/bin/commands/trail.js
CHANGED
|
@@ -16,6 +16,7 @@ import { style } from '../registry-helpers.js';
|
|
|
16
16
|
|
|
17
17
|
export const MAX_ROUNDS = 5;
|
|
18
18
|
const TRAIL_RE = /^##\s+.*reasoning trail/i;
|
|
19
|
+
const ENTRIES_RE = /^##\s+.*section entries/i;
|
|
19
20
|
const ROUND_RE = /^Round\s+\d+\b/;
|
|
20
21
|
|
|
21
22
|
/** Pick the course by slug; with no slug only a single course is accepted — never guess. */
|
|
@@ -24,10 +25,10 @@ export function resolveCourse(cwd, slug) {
|
|
|
24
25
|
if (slug) {
|
|
25
26
|
const hit = courses.find((c) => c.slug === slug);
|
|
26
27
|
if (hit) return hit;
|
|
27
|
-
throw new Error(`No course "${slug}" under learning
|
|
28
|
+
throw new Error(`No course "${slug}" under learning/. Found: ${courses.map((c) => c.slug).join(', ') || 'none'}.`);
|
|
28
29
|
}
|
|
29
30
|
if (courses.length === 1) return courses[0];
|
|
30
|
-
if (!courses.length) throw new Error('No course found under learning
|
|
31
|
+
if (!courses.length) throw new Error('No course found under learning/. Run professor:new-topic first.');
|
|
31
32
|
throw new Error(`Several courses found — pass the slug: ${courses.map((c) => c.slug).join(', ')}.`);
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -161,12 +162,148 @@ export function recordRound({ cwd, slug, section, learner, asked, concept, evide
|
|
|
161
162
|
return { recorded: true, path, section: target, rounds, saved };
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
// ─── Section Entries (done gate verdict) ─────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
/** Accepts `solid` or `watch-this: <concept>` (leading ⚠️ tolerated). */
|
|
168
|
+
export function normalizeVerdict(raw) {
|
|
169
|
+
const value = oneLine(raw);
|
|
170
|
+
if (/^solid$/i.test(value)) return { verdict: 'solid', watch: null };
|
|
171
|
+
const m = value.match(/^(?:⚠️\s*)?watch-this\s*:\s*(.+)$/i);
|
|
172
|
+
if (m && oneLine(m[1])) return { verdict: `⚠️ watch-this: ${oneLine(m[1])}`, watch: oneLine(m[1]) };
|
|
173
|
+
throw new Error('Invalid verdict — use "solid" or "watch-this: <concept>". Nothing was recorded.');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Index of the first `##` heading after `start`, or lines.length. */
|
|
177
|
+
function regionEnd(lines, start) {
|
|
178
|
+
let i = start + 1;
|
|
179
|
+
while (i < lines.length && !/^##\s/.test(lines[i])) i++;
|
|
180
|
+
return i;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Find or create `## 📝 Section Entries`; returns its line index. */
|
|
184
|
+
function ensureEntriesHeading(lines) {
|
|
185
|
+
let e = lines.findIndex((l) => ENTRIES_RE.test(l));
|
|
186
|
+
if (e !== -1) return e;
|
|
187
|
+
const attempt = lines.findIndex((l) => /^##\s+.*attempt log/i.test(l));
|
|
188
|
+
let at = attempt === -1 ? lines.length : regionEnd(lines, attempt);
|
|
189
|
+
while (at > 0 && !lines[at - 1].trim()) at--;
|
|
190
|
+
lines.splice(at, 0, '', '## 📝 Section Entries', '');
|
|
191
|
+
return lines.findIndex((l) => ENTRIES_RE.test(l));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function verdictEntryLines(entry) {
|
|
195
|
+
const out = [`### Section ${entry.section}`, '', `- Date: ${entry.date}`];
|
|
196
|
+
if (entry.hardest) out.push(`- Hardest: ${oneLine(entry.hardest)}`);
|
|
197
|
+
if (entry.duration) out.push(`- Duration: ${oneLine(entry.duration)}`);
|
|
198
|
+
for (const { q, a } of entry.answers) out.push(`- Q: "${oneLine(q)}" → A: "${oneLine(a)}"`);
|
|
199
|
+
out.push(`- Verdict: ${entry.verdict}`);
|
|
200
|
+
if (entry.lecture) out.push(`- Archived lecture: ${oneLine(entry.lecture)}`);
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Put the watch-this flag under the section's reasoning trail, replacing an older one. */
|
|
205
|
+
function addTrailWatch(lines, section, watch) {
|
|
206
|
+
let t = lines.findIndex((l) => TRAIL_RE.test(l));
|
|
207
|
+
if (t === -1) {
|
|
208
|
+
const e = lines.findIndex((l) => ENTRIES_RE.test(l));
|
|
209
|
+
let at = e === -1 ? lines.length : e;
|
|
210
|
+
while (at > 0 && !lines[at - 1].trim()) at--;
|
|
211
|
+
lines.splice(at, 0, '', '## 🗣️ Reasoning Trail', '');
|
|
212
|
+
t = at + 1;
|
|
213
|
+
}
|
|
214
|
+
let end = regionEnd(lines, t);
|
|
215
|
+
const key = sectionKey(section);
|
|
216
|
+
let h = -1;
|
|
217
|
+
for (let i = t + 1; i < end; i++) if (sectionHeadRe(key).test(lines[i])) { h = i; break; }
|
|
218
|
+
const flag = `⚠️ watch-this: ${oneLine(watch)}`;
|
|
219
|
+
if (h === -1) {
|
|
220
|
+
while (end > t + 1 && !lines[end - 1].trim()) end--;
|
|
221
|
+
lines.splice(end, 0, '', `### Section ${section}`, '', flag, '');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
let blockEnd = h + 1;
|
|
225
|
+
while (blockEnd < lines.length && !/^#{2,3}\s/.test(lines[blockEnd])) blockEnd++;
|
|
226
|
+
const block = lines.slice(h + 1, blockEnd);
|
|
227
|
+
const { preface, rounds } = splitRounds(block);
|
|
228
|
+
const keep = preface.filter((l) => !/watch-this/i.test(l));
|
|
229
|
+
keep.push(flag);
|
|
230
|
+
const rebuilt = [
|
|
231
|
+
'', ...keep, '',
|
|
232
|
+
...rounds.flatMap((r) => [...r, '']).slice(0, -1),
|
|
233
|
+
'',
|
|
234
|
+
];
|
|
235
|
+
lines.splice(h + 1, block.length, ...rebuilt);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Pure: return LEARNING-LOG.md text with the section entry for `entry`. */
|
|
239
|
+
export function appendVerdict(text, entry) {
|
|
240
|
+
let src = text && text.trim()
|
|
241
|
+
? text
|
|
242
|
+
: `---\ncourse: ${entry.slug}\nupdated: ${entry.date}\n---\n\n# Learning Log\n`;
|
|
243
|
+
src = src.replace(/^(---\n[\s\S]*?^updated:\s*).*$/m, `$1${entry.date}`);
|
|
244
|
+
const lines = src.replace(/\n+$/, '').split('\n');
|
|
245
|
+
|
|
246
|
+
const e = ensureEntriesHeading(lines);
|
|
247
|
+
const headRe = sectionHeadRe(sectionKey(entry.section));
|
|
248
|
+
let h = -1;
|
|
249
|
+
for (let i = e + 1; i < lines.length && !/^##\s/.test(lines[i]); i++) {
|
|
250
|
+
if (headRe.test(lines[i])) { h = i; break; }
|
|
251
|
+
}
|
|
252
|
+
const body = verdictEntryLines(entry);
|
|
253
|
+
if (h === -1) {
|
|
254
|
+
let at = regionEnd(lines, e);
|
|
255
|
+
while (at > e + 1 && !lines[at - 1].trim()) at--;
|
|
256
|
+
lines.splice(at, 0, '', ...body, '');
|
|
257
|
+
} else {
|
|
258
|
+
let end = h + 1;
|
|
259
|
+
while (end < lines.length && !/^#{2,3}\s/.test(lines[end])) end++;
|
|
260
|
+
while (end > h + 1 && !lines[end - 1].trim()) end--;
|
|
261
|
+
lines.splice(h, end - h, ...body, '');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (entry.watch || /watch-this/i.test(entry.verdict)) {
|
|
265
|
+
const watch = entry.watch || normalizeVerdict(entry.verdict).watch;
|
|
266
|
+
if (watch) addTrailWatch(lines, entry.section, watch);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
|
|
270
|
+
return { text: `${lines.join('\n').replace(/\n{3,}/g, '\n\n')}\n` };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Record the done-gate verdict; Professor is the only writer of LEARNING-LOG.md. */
|
|
274
|
+
export function recordVerdict({
|
|
275
|
+
cwd, slug, section, hardest, answers, verdict, duration, lecture, date = todayISO(),
|
|
276
|
+
}) {
|
|
277
|
+
if (!slug) throw new Error('Pass the course slug — verdict never guesses which course to write.');
|
|
278
|
+
const normalized = normalizeVerdict(verdict);
|
|
279
|
+
if (!oneLine(hardest)) throw new Error('Missing hardest — nothing was recorded.');
|
|
280
|
+
const cleaned = (Array.isArray(answers) ? answers : [])
|
|
281
|
+
.map((a) => (typeof a === 'string' ? { q: 'What was the hardest concept?', a } : a || {}))
|
|
282
|
+
.map((a) => ({ q: oneLine(a.q), a: oneLine(a.a) }))
|
|
283
|
+
.filter((a) => a.q && a.a);
|
|
284
|
+
if (!cleaned.length) throw new Error('Missing answers — nothing was recorded.');
|
|
285
|
+
const ctx = buildContext({ cwd, slug });
|
|
286
|
+
const target = section || ctx.section;
|
|
287
|
+
if (!target) throw new Error('No current section in COURSE.md — pass --section "N — Title".');
|
|
288
|
+
const path = ctx.learningLog;
|
|
289
|
+
const before = existsSync(path) ? readFileSync(path, 'utf-8') : '';
|
|
290
|
+
const { text } = appendVerdict(before, {
|
|
291
|
+
slug, section: target, date, duration, hardest, answers: cleaned,
|
|
292
|
+
verdict: normalized.verdict, watch: normalized.watch, lecture,
|
|
293
|
+
});
|
|
294
|
+
writeFileSync(path, text);
|
|
295
|
+
return { recorded: true, path, section: target, verdict: normalized.verdict, watch: normalized.watch };
|
|
296
|
+
}
|
|
297
|
+
|
|
164
298
|
function flagValue(args, name) {
|
|
165
299
|
const i = args.indexOf(name);
|
|
166
300
|
return i !== -1 && args[i + 1] !== undefined && !args[i + 1].startsWith('--') ? args[i + 1] : null;
|
|
167
301
|
}
|
|
168
302
|
|
|
169
|
-
const VALUE_FLAGS = [
|
|
303
|
+
const VALUE_FLAGS = [
|
|
304
|
+
'--section', '--learner', '--asked', '--concept', '--evidence', '--watch', '--date',
|
|
305
|
+
'--hardest', '--answers', '--verdict', '--duration', '--lecture',
|
|
306
|
+
];
|
|
170
307
|
|
|
171
308
|
function positionals(args) {
|
|
172
309
|
const out = [];
|
|
@@ -197,6 +334,19 @@ export async function run({ args = [], cwd = process.cwd(), command = 'context'
|
|
|
197
334
|
}
|
|
198
335
|
return;
|
|
199
336
|
}
|
|
337
|
+
if (command === 'verdict') {
|
|
338
|
+
// Same stdin-only rule as trail: the learner's words never appear inside a shell flag.
|
|
339
|
+
const fromStdin = args.includes('--stdin') ? JSON.parse(readFileSync(0, 'utf-8')) : {};
|
|
340
|
+
const f = (n) => fromStdin[n.slice(2)] ?? flagValue(args, n);
|
|
341
|
+
const r = recordVerdict({
|
|
342
|
+
cwd, slug, section: f('--section'), hardest: f('--hardest'), answers: f('--answers'),
|
|
343
|
+
verdict: f('--verdict'), duration: f('--duration'), lecture: f('--lecture'),
|
|
344
|
+
date: f('--date') || undefined,
|
|
345
|
+
});
|
|
346
|
+
if (json) console.log(JSON.stringify(r));
|
|
347
|
+
else console.log(style('success', `Recorded verdict "${r.verdict}" for Section ${r.section} in ${relative(cwd, r.path)}`));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
200
350
|
const ctx = buildContext({ cwd, slug });
|
|
201
351
|
if (json) { console.log(JSON.stringify(ctx, null, 2)); return; }
|
|
202
352
|
console.log(`${ctx.title} (${ctx.slug}, ${ctx.kind}) — ${ctx.dir}`);
|
package/bin/commands/web.js
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* Legacy mode is the explicit opt-in (`--legacy` or `PROFESSOR_LEGACY=1`) and keeps the old
|
|
6
6
|
* Anthropic/OpenAI fallback prompt. The server itself owns legacy-dev Vite startup (D-08).
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, readFileSync, writeFileSync
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
9
9
|
import { join, dirname } from 'path';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import { createRequire } from 'module';
|
|
12
12
|
import inquirer from 'inquirer';
|
|
13
13
|
import ora from 'ora';
|
|
14
|
-
import { buildPairingUrl } from './studio.js';
|
|
14
|
+
import { buildPairingUrl, loopbackUrl, toLoopbackUrl } from './studio.js';
|
|
15
15
|
import * as security from '../../web/security.js';
|
|
16
16
|
|
|
17
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -297,13 +297,7 @@ export async function run({ args = [], style, startServerImpl } = {}) {
|
|
|
297
297
|
|
|
298
298
|
process.env.PORT = positionalPort;
|
|
299
299
|
|
|
300
|
-
|
|
301
|
-
if (existsSync('./courses')) {
|
|
302
|
-
if (readdirSync('./courses').length === 0) defaultCourses = './learning';
|
|
303
|
-
} else if (existsSync('./learning')) {
|
|
304
|
-
defaultCourses = './learning';
|
|
305
|
-
}
|
|
306
|
-
process.env.COURSES_DIR = process.env.COURSES_DIR || defaultCourses;
|
|
300
|
+
process.env.COURSES_DIR = process.env.COURSES_DIR || './learning';
|
|
307
301
|
|
|
308
302
|
const token =
|
|
309
303
|
rotatedToken ||
|
|
@@ -330,7 +324,11 @@ export async function run({ args = [], style, startServerImpl } = {}) {
|
|
|
330
324
|
host: process.env.HOST,
|
|
331
325
|
logBanner: false,
|
|
332
326
|
});
|
|
333
|
-
|
|
327
|
+
// Printed links use the IPv4 literal: `localhost` can resolve to `::1`, where another dev server
|
|
328
|
+
// may answer instead of this one (the server itself binds 127.0.0.1). Legacy dev is left alone
|
|
329
|
+
// because Vite owns that port.
|
|
330
|
+
const uiBase = started?.uiUrl || loopbackUrl(expectedUiPort);
|
|
331
|
+
const uiUrl = legacy && !isProduction ? uiBase : toLoopbackUrl(uiBase);
|
|
334
332
|
const uiPort = (() => {
|
|
335
333
|
try {
|
|
336
334
|
return new URL(uiUrl).port || expectedUiPort;
|
package/bin/course-reader.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Course reader — pure parsers for course files on disk.
|
|
3
3
|
*
|
|
4
|
-
* Reads `learning/{slug}
|
|
4
|
+
* Reads `learning/{slug}/`, the single course location. Every
|
|
5
5
|
* function here is side-effect free apart from reading files, so Phase 31
|
|
6
6
|
* (`studio`, `review`, `import`) can reuse the same shapes without going
|
|
7
7
|
* through the CLI.
|
|
@@ -252,7 +252,7 @@ export function parseSections(content) {
|
|
|
252
252
|
return parseListSections(region);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
function parseCapstoneStatus(content, rowStatus) {
|
|
255
|
+
export function parseCapstoneStatus(content, rowStatus) {
|
|
256
256
|
const raw = metaValue(content, 'Capstone status');
|
|
257
257
|
if (raw) {
|
|
258
258
|
const lower = raw.toLowerCase();
|
|
@@ -488,10 +488,10 @@ export function resolveExercise(course, { lecture = null } = {}) {
|
|
|
488
488
|
|
|
489
489
|
/**
|
|
490
490
|
* Find every course directory on disk.
|
|
491
|
-
* Scans `learning/*/`
|
|
491
|
+
* Scans `learning/*/` only — it is the single course location.
|
|
492
492
|
* @param {string} cwd
|
|
493
|
-
* @param {{ dir?: string }} [opts] `dir` overrides the
|
|
494
|
-
* @returns {{ slug: string, dir: string, location: 'learning'|'
|
|
493
|
+
* @param {{ dir?: string }} [opts] `dir` overrides the root (single course or parent)
|
|
494
|
+
* @returns {{ slug: string, dir: string, location: 'learning'|'custom' }[]}
|
|
495
495
|
*/
|
|
496
496
|
export function findCourses(cwd, { dir } = {}) {
|
|
497
497
|
if (dir) {
|
|
@@ -501,16 +501,7 @@ export function findCourses(cwd, { dir } = {}) {
|
|
|
501
501
|
}
|
|
502
502
|
return scanRoot(abs, 'custom');
|
|
503
503
|
}
|
|
504
|
-
|
|
505
|
-
const seen = new Set();
|
|
506
|
-
for (const [name, location] of [['learning', 'learning'], ['courses', 'courses']]) {
|
|
507
|
-
for (const course of scanRoot(join(cwd, name), location)) {
|
|
508
|
-
if (seen.has(course.slug)) continue;
|
|
509
|
-
seen.add(course.slug);
|
|
510
|
-
results.push(course);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return results;
|
|
504
|
+
return scanRoot(join(cwd, 'learning'), 'learning');
|
|
514
505
|
}
|
|
515
506
|
|
|
516
507
|
function scanRoot(root, location) {
|
package/bin/platforms/_shared.js
CHANGED
|
@@ -1,9 +1,139 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
readFileSync,
|
|
3
|
+
writeFileSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
statSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
copyFileSync,
|
|
9
|
+
} from 'fs';
|
|
2
10
|
import { join, dirname } from 'path';
|
|
3
11
|
import { fileURLToPath } from 'url';
|
|
4
12
|
|
|
5
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
14
|
|
|
15
|
+
/** Markers that delimit a plugin-managed region inside a learner-editable file. */
|
|
16
|
+
export const BLOCK_START = '<!-- professor:start -->';
|
|
17
|
+
export const BLOCK_END = '<!-- professor:end -->';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Counter for one setup run: what was created or updated, what was already current, and how
|
|
21
|
+
* many files were backed up.
|
|
22
|
+
* @returns {{ updated: number, unchanged: number, backedUp: number, backedUpPaths: Set<string> }}
|
|
23
|
+
*/
|
|
24
|
+
export function createInstallRun() {
|
|
25
|
+
return { updated: 0, unchanged: 0, backedUp: 0, backedUpPaths: new Set() };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Save the content a write is about to replace, at most once per run per file.
|
|
30
|
+
* @param {string} dest
|
|
31
|
+
* @param {{ backedUp: number, backedUpPaths: Set<string> }} run
|
|
32
|
+
*/
|
|
33
|
+
function _backupOnce(dest, run) {
|
|
34
|
+
if (run.backedUpPaths.has(dest)) return;
|
|
35
|
+
copyFileSync(dest, `${dest}.bak`);
|
|
36
|
+
run.backedUpPaths.add(dest);
|
|
37
|
+
run.backedUp++;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Write a file the plugin owns: create it, leave it alone when it matches, otherwise back it up and
|
|
42
|
+
* replace it. This is what makes re-running setup refresh an upgrade.
|
|
43
|
+
* @param {string} dest
|
|
44
|
+
* @param {string} content
|
|
45
|
+
* @param {ReturnType<typeof createInstallRun>} run
|
|
46
|
+
* @param {string} [label] path shown in the log
|
|
47
|
+
* @returns {'created'|'updated'|'unchanged'}
|
|
48
|
+
*/
|
|
49
|
+
export function writeManagedFile(dest, content, run, label = dest) {
|
|
50
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
51
|
+
|
|
52
|
+
if (!existsSync(dest)) {
|
|
53
|
+
writeFileSync(dest, content);
|
|
54
|
+
run.updated++;
|
|
55
|
+
console.log(` + ${label}`);
|
|
56
|
+
return 'created';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (readFileSync(dest, 'utf-8') === content) {
|
|
60
|
+
run.unchanged++;
|
|
61
|
+
console.log(` = ${label} (unchanged)`);
|
|
62
|
+
return 'unchanged';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_backupOnce(dest, run);
|
|
66
|
+
writeFileSync(dest, content);
|
|
67
|
+
run.updated++;
|
|
68
|
+
console.log(` ~ ${label} (updated, backup saved)`);
|
|
69
|
+
return 'updated';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Refresh the marker-delimited Professor block inside a file the learner may also edit.
|
|
74
|
+
* Text outside the markers is preserved; a file without markers gets the block appended.
|
|
75
|
+
* @param {string} dest
|
|
76
|
+
* @param {string} blockContent
|
|
77
|
+
* @param {ReturnType<typeof createInstallRun>} run
|
|
78
|
+
* @param {string} [label]
|
|
79
|
+
* @returns {'created'|'updated'|'unchanged'}
|
|
80
|
+
*/
|
|
81
|
+
export function writeManagedBlock(dest, blockContent, run, label = dest) {
|
|
82
|
+
const block = `${BLOCK_START}\n${blockContent}\n${BLOCK_END}`;
|
|
83
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
84
|
+
|
|
85
|
+
if (!existsSync(dest)) {
|
|
86
|
+
writeFileSync(dest, `${block}\n`);
|
|
87
|
+
run.updated++;
|
|
88
|
+
console.log(` + ${label}`);
|
|
89
|
+
return 'created';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const current = readFileSync(dest, 'utf-8');
|
|
93
|
+
const start = current.indexOf(BLOCK_START);
|
|
94
|
+
const end = start === -1 ? -1 : current.indexOf(BLOCK_END, start);
|
|
95
|
+
let next;
|
|
96
|
+
if (start !== -1 && end !== -1) {
|
|
97
|
+
next = current.slice(0, start) + block + current.slice(end + BLOCK_END.length);
|
|
98
|
+
} else {
|
|
99
|
+
next = `${current}${current.endsWith('\n') ? '\n' : '\n\n'}${block}\n`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (next === current) {
|
|
103
|
+
run.unchanged++;
|
|
104
|
+
console.log(` = ${label} (unchanged)`);
|
|
105
|
+
return 'unchanged';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_backupOnce(dest, run);
|
|
109
|
+
writeFileSync(dest, next);
|
|
110
|
+
run.updated++;
|
|
111
|
+
console.log(` ~ ${label} (updated, backup saved)`);
|
|
112
|
+
return 'updated';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* True when the content is the skill document an older release wrote wholesale into a file.
|
|
117
|
+
* @param {string} content
|
|
118
|
+
* @returns {boolean}
|
|
119
|
+
*/
|
|
120
|
+
export function isLegacySkillDocument(content) {
|
|
121
|
+
const frontmatter = content.match(/^---\n([\s\S]*?)\n---/);
|
|
122
|
+
return !!frontmatter && /^name:\s*professor\s*$/m.test(frontmatter[1]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* One summary line per platform run.
|
|
127
|
+
* @param {string} platform
|
|
128
|
+
* @param {ReturnType<typeof createInstallRun>} run
|
|
129
|
+
*/
|
|
130
|
+
export function printInstallSummary(platform, run) {
|
|
131
|
+
console.log(
|
|
132
|
+
`✎ Setup summary (${platform}): ${run.updated} updated, ` +
|
|
133
|
+
`${run.unchanged} unchanged, ${run.backedUp} backed up.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
7
137
|
// Maps platform name → tool substitutions.
|
|
8
138
|
// null means: strip the AskUserQuestion call and replace with plain inline text.
|
|
9
139
|
export const TOOL_SUBSTITUTIONS = {
|
|
@@ -46,7 +176,9 @@ export function substituteTokens(content, platform) {
|
|
|
46
176
|
*/
|
|
47
177
|
export function copySharedFiles(targetDir, platform) {
|
|
48
178
|
const sharedDir = join(__dirname, '..', '..', 'shared');
|
|
49
|
-
|
|
179
|
+
const run = createInstallRun();
|
|
180
|
+
_copyRecursive(sharedDir, targetDir, platform, true, run);
|
|
181
|
+
return run;
|
|
50
182
|
}
|
|
51
183
|
|
|
52
184
|
/**
|
|
@@ -54,23 +186,24 @@ export function copySharedFiles(targetDir, platform) {
|
|
|
54
186
|
* By default, applies token substitution to .md/.js files.
|
|
55
187
|
*/
|
|
56
188
|
export function copyDirectoryFiles(sourceDir, targetDir, platform, { substitute = true } = {}) {
|
|
57
|
-
|
|
189
|
+
const run = createInstallRun();
|
|
190
|
+
_copyRecursive(sourceDir, targetDir, platform, substitute, run);
|
|
191
|
+
return run;
|
|
58
192
|
}
|
|
59
193
|
|
|
60
|
-
function _copyRecursive(src, dest, platform, substitute) {
|
|
194
|
+
function _copyRecursive(src, dest, platform, substitute, run) {
|
|
61
195
|
if (!existsSync(src)) return;
|
|
62
196
|
const stat = statSync(src);
|
|
63
197
|
if (stat.isDirectory()) {
|
|
64
|
-
if (!existsSync(dest)) mkdirSync(dest, { recursive: true });
|
|
65
198
|
for (const file of readdirSync(src)) {
|
|
66
|
-
_copyRecursive(join(src, file), join(dest, file), platform, substitute);
|
|
199
|
+
_copyRecursive(join(src, file), join(dest, file), platform, substitute, run);
|
|
67
200
|
}
|
|
68
|
-
} else
|
|
201
|
+
} else {
|
|
69
202
|
let content = readFileSync(src, 'utf-8');
|
|
70
203
|
if (substitute && (src.endsWith('.md') || src.endsWith('.js'))) {
|
|
71
204
|
content = substituteTokens(content, platform);
|
|
72
205
|
}
|
|
73
|
-
|
|
206
|
+
writeManagedFile(dest, content, run);
|
|
74
207
|
}
|
|
75
208
|
}
|
|
76
209
|
|
|
@@ -167,16 +300,18 @@ function _parseFrontmatter(content) {
|
|
|
167
300
|
|
|
168
301
|
/**
|
|
169
302
|
* Read every .md file from shared/commands/professor/, apply substituteTokens(),
|
|
170
|
-
* then write per-platform command files into targetDir.
|
|
303
|
+
* then write per-platform command files into targetDir.
|
|
171
304
|
*
|
|
172
|
-
* platform = 'gemini' → writes targetDir/<slug>.toml
|
|
305
|
+
* platform = 'gemini' → writes targetDir/<slug>.toml (arguments become {{args}})
|
|
173
306
|
* platform = 'opencode' → writes targetDir/professor-<slug>.md
|
|
307
|
+
*
|
|
308
|
+
* @param {string} targetDir
|
|
309
|
+
* @param {'gemini'|'opencode'} platform
|
|
310
|
+
* @param {ReturnType<typeof createInstallRun>} [run]
|
|
174
311
|
*/
|
|
175
|
-
export function generateCommandFiles(targetDir, platform) {
|
|
312
|
+
export function generateCommandFiles(targetDir, platform, run = createInstallRun()) {
|
|
176
313
|
const sharedDir = join(__dirname, '..', '..', 'shared', 'commands', 'professor');
|
|
177
|
-
if (!existsSync(sharedDir)) return;
|
|
178
|
-
|
|
179
|
-
mkdirSync(targetDir, { recursive: true });
|
|
314
|
+
if (!existsSync(sharedDir)) return run;
|
|
180
315
|
|
|
181
316
|
for (const file of readdirSync(sharedDir)) {
|
|
182
317
|
if (!file.endsWith('.md')) continue;
|
|
@@ -184,28 +319,19 @@ export function generateCommandFiles(targetDir, platform) {
|
|
|
184
319
|
|
|
185
320
|
let raw = readFileSync(join(sharedDir, file), 'utf-8');
|
|
186
321
|
raw = substituteTokens(raw, platform);
|
|
322
|
+
if (platform === 'gemini') raw = raw.replaceAll('$ARGUMENTS', '{{args}}');
|
|
187
323
|
const { description, body } = _parseFrontmatter(raw);
|
|
188
324
|
|
|
189
325
|
if (platform === 'gemini') {
|
|
190
326
|
const filename = `${stem}.toml`;
|
|
191
|
-
const destPath = join(targetDir, filename);
|
|
192
|
-
if (existsSync(destPath)) {
|
|
193
|
-
console.log(` ↔ ${filename} (already exists)`);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
327
|
const output = `description = "${description}"\n\nprompt = """\n${body}"""\n`;
|
|
197
|
-
|
|
198
|
-
console.log(` + ${filename}`);
|
|
328
|
+
writeManagedFile(join(targetDir, filename), output, run, filename);
|
|
199
329
|
} else if (platform === 'opencode') {
|
|
200
330
|
const filename = `professor-${stem}.md`;
|
|
201
|
-
const destPath = join(targetDir, filename);
|
|
202
|
-
if (existsSync(destPath)) {
|
|
203
|
-
console.log(` ↔ ${filename} (already exists)`);
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
331
|
const output = `---\ndescription: ${description}\nsubtask: true\n---\n\n${body}`;
|
|
207
|
-
|
|
208
|
-
console.log(` + ${filename}`);
|
|
332
|
+
writeManagedFile(join(targetDir, filename), output, run, filename);
|
|
209
333
|
}
|
|
210
334
|
}
|
|
335
|
+
|
|
336
|
+
return run;
|
|
211
337
|
}
|
package/bin/platforms/claude.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
substituteTokens,
|
|
6
|
+
createInstallRun,
|
|
7
|
+
writeManagedFile,
|
|
8
|
+
printInstallSummary,
|
|
9
|
+
} from './_shared.js';
|
|
5
10
|
|
|
6
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
12
|
const PLUGIN_DIR = join(__dirname, '..', '..');
|
|
@@ -25,6 +30,7 @@ export async function install(scope = 'local') {
|
|
|
25
30
|
: join(process.cwd(), '.claude');
|
|
26
31
|
|
|
27
32
|
const label = scope === 'global' ? '~/.claude/' : '.claude/';
|
|
33
|
+
const run = createInstallRun();
|
|
28
34
|
|
|
29
35
|
if (!existsSync(base)) {
|
|
30
36
|
mkdirSync(base, { recursive: true });
|
|
@@ -32,67 +38,42 @@ export async function install(scope = 'local') {
|
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
// 1. agents/ — one .md file per agent
|
|
35
|
-
const agentsDestDir = join(base, 'agents');
|
|
36
|
-
mkdirSync(agentsDestDir, { recursive: true });
|
|
37
|
-
const agentsSrcDir = join(PLUGIN_DIR, 'shared', 'agents');
|
|
38
41
|
console.log('✓ Installing agents...');
|
|
42
|
+
const agentsSrcDir = join(PLUGIN_DIR, 'shared', 'agents');
|
|
43
|
+
const agentsDestDir = join(base, 'agents');
|
|
39
44
|
for (const file of readdirSync(agentsSrcDir)) {
|
|
40
45
|
if (!file.endsWith('.md')) continue;
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
console.log(` ↔ agents/${file} (already exists)`);
|
|
44
|
-
} else {
|
|
45
|
-
const content = substituteTokens(readFileSync(join(agentsSrcDir, file), 'utf-8'), 'claude');
|
|
46
|
-
writeFileSync(dest, content);
|
|
47
|
-
console.log(` + agents/${file}`);
|
|
48
|
-
}
|
|
46
|
+
const content = substituteTokens(readFileSync(join(agentsSrcDir, file), 'utf-8'), 'claude');
|
|
47
|
+
writeManagedFile(join(agentsDestDir, file), content, run, `agents/${file}`);
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
// 2. commands/professor/ — one .md file per command
|
|
52
|
-
const commandsDestDir = join(base, 'commands', 'professor');
|
|
53
|
-
mkdirSync(commandsDestDir, { recursive: true });
|
|
54
|
-
const commandsSrcDir = join(PLUGIN_DIR, 'shared', 'commands', 'professor');
|
|
55
51
|
console.log('✓ Installing commands...');
|
|
52
|
+
const commandsSrcDir = join(PLUGIN_DIR, 'shared', 'commands', 'professor');
|
|
53
|
+
const commandsDestDir = join(base, 'commands', 'professor');
|
|
56
54
|
for (const file of readdirSync(commandsSrcDir)) {
|
|
57
55
|
if (!file.endsWith('.md')) continue;
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
console.log(` ↔ commands/professor/${file} (already exists)`);
|
|
61
|
-
} else {
|
|
62
|
-
const content = substituteTokens(readFileSync(join(commandsSrcDir, file), 'utf-8'), 'claude');
|
|
63
|
-
writeFileSync(dest, content);
|
|
64
|
-
console.log(` + commands/professor/${file}`);
|
|
65
|
-
}
|
|
56
|
+
const content = substituteTokens(readFileSync(join(commandsSrcDir, file), 'utf-8'), 'claude');
|
|
57
|
+
writeManagedFile(join(commandsDestDir, file), content, run, `commands/professor/${file}`);
|
|
66
58
|
}
|
|
67
59
|
|
|
68
60
|
// 3. hooks/ — pre-compact.js
|
|
69
|
-
const hooksDestDir = join(base, 'hooks');
|
|
70
|
-
mkdirSync(hooksDestDir, { recursive: true });
|
|
71
61
|
const hooksSrcDir = join(PLUGIN_DIR, 'shared', 'hooks');
|
|
72
62
|
if (existsSync(hooksSrcDir)) {
|
|
73
63
|
console.log('✓ Installing hooks...');
|
|
64
|
+
const hooksDestDir = join(base, 'hooks');
|
|
74
65
|
for (const file of readdirSync(hooksSrcDir)) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
console.log(` ↔ hooks/${file} (already exists)`);
|
|
78
|
-
} else {
|
|
79
|
-
writeFileSync(dest, readFileSync(join(hooksSrcDir, file), 'utf-8'));
|
|
80
|
-
console.log(` + hooks/${file}`);
|
|
81
|
-
}
|
|
66
|
+
const content = readFileSync(join(hooksSrcDir, file), 'utf-8');
|
|
67
|
+
writeManagedFile(join(hooksDestDir, file), content, run, `hooks/${file}`);
|
|
82
68
|
}
|
|
83
69
|
}
|
|
84
70
|
|
|
85
71
|
// 4. skills/professor/ — SKILL.md
|
|
86
|
-
const skillsDestDir = join(base, 'skills', 'professor');
|
|
87
|
-
mkdirSync(skillsDestDir, { recursive: true });
|
|
88
72
|
const skillSrc = join(PLUGIN_DIR, 'shared', 'SKILL.md');
|
|
89
|
-
const skillDest = join(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
writeFileSync(skillDest, readFileSync(skillSrc, 'utf-8'));
|
|
94
|
-
console.log(' + skills/professor/SKILL.md');
|
|
95
|
-
}
|
|
73
|
+
const skillDest = join(base, 'skills', 'professor', 'SKILL.md');
|
|
74
|
+
writeManagedFile(skillDest, readFileSync(skillSrc, 'utf-8'), run, 'skills/professor/SKILL.md');
|
|
75
|
+
|
|
76
|
+
printInstallSummary('Claude Code', run);
|
|
96
77
|
|
|
97
78
|
console.log(`
|
|
98
79
|
✅ Setup complete for Claude Code!
|