@erclx/canon 5.51.0 → 5.53.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 +20 -0
- package/README.md +1 -4
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +0 -3
- package/claude/skills/context-fold/REQUIREMENT.md +0 -1
- package/claude/skills/design-taste/references/kinds.md +0 -1
- package/claude/skills/draw-figure/REQUIREMENT.md +2 -3
- package/claude/skills/draw-figure/SKILL.md +4 -5
- package/claude/skills/draw-figure/references/figures.md +0 -1
- package/claude/skills/draw-figure/references/hand-drawn.md +1 -1
- package/claude/skills/git-pr/references/pr.md +1 -1
- package/claude/skills/pick-by-eye/REQUIREMENT.md +0 -1
- package/claude/skills/pick-by-eye/SKILL.md +1 -1
- package/claude/skills/review-address/SKILL.md +1 -1
- package/claude/skills/review-address/references/post-review-findings.md +1 -1
- package/claude/skills/review-pr/references/post.md +2 -2
- package/claude/skills/target-check/SKILL.md +2 -0
- package/claude/skills/target-setup/SKILL.md +1 -1
- package/claude/skills/target-update/REQUIREMENT.md +33 -0
- package/claude/skills/target-update/SKILL.md +136 -0
- package/claude/skills/teach-craft/REQUIREMENT.md +30 -0
- package/claude/skills/teach-craft/SKILL.md +39 -0
- package/claude/skills/teach-craft/references/lesson-craft.md +52 -0
- package/claude/skills/{teach-workspace → teach-craft}/references/pedagogy.md +12 -4
- package/docs/agents/audits.md +0 -1
- package/docs/agents/migrate.md +2 -1
- package/docs/target/migrations.md +12 -1
- package/docs/target/projects.md +1 -1
- package/docs/target/sync.md +1 -1
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map-on-request.md +10 -22
- package/docs/workflow/skill-map.md +13 -14
- package/package.json +1 -4
- package/scripts/core/regen-web-previews.ts +2 -67
- package/src/audits/catalog.ts +0 -1
- package/src/browser/engine.ts +3 -6
- package/src/capture/render.ts +1 -1
- package/src/claude/cases/authoring.ts +0 -12
- package/src/claude/cases/misc.ts +0 -17
- package/src/claude/cases/setup.ts +7 -0
- package/src/claude/cases/workflow.ts +2 -38
- package/src/claude/skills-audit.ts +0 -1
- package/src/cli/help.ts +0 -7
- package/src/cli.ts +0 -6
- package/src/commands/labels.ts +9 -7
- package/src/commands/records.ts +0 -1
- package/src/commands/serve.ts +2 -2
- package/src/design/components.ts +8 -1514
- package/src/design/css.ts +5 -6
- package/src/design/fonts.ts +3 -5
- package/src/driver/drive.ts +3 -4
- package/src/driver/steps.ts +2 -5
- package/src/inventory/walk.ts +1 -1
- package/src/markdown/scope.ts +4 -31
- package/src/migrate/status.ts +5 -1
- package/src/records/validate.ts +4 -176
- package/src/sandbox/equivalence.ts +0 -5
- package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +1 -1
- package/claude/skills/draft-and-pick/REQUIREMENT.md +0 -26
- package/claude/skills/draft-and-pick/SKILL.md +0 -11
- package/claude/skills/draft-doc/REQUIREMENT.md +0 -26
- package/claude/skills/draft-doc/SKILL.md +0 -11
- package/claude/skills/draft-figure/REQUIREMENT.md +0 -26
- package/claude/skills/draft-figure/SKILL.md +0 -11
- package/claude/skills/draft-identity/REQUIREMENT.md +0 -26
- package/claude/skills/draft-identity/SKILL.md +0 -11
- package/claude/skills/draft-ready/REQUIREMENT.md +0 -26
- package/claude/skills/draft-ready/SKILL.md +0 -11
- package/claude/skills/draft-screencast/REQUIREMENT.md +0 -26
- package/claude/skills/draft-screencast/SKILL.md +0 -11
- package/claude/skills/draft-slides/REQUIREMENT.md +0 -26
- package/claude/skills/draft-slides/SKILL.md +0 -11
- package/claude/skills/make-deck/REQUIREMENT.md +0 -36
- package/claude/skills/make-deck/SKILL.md +0 -80
- package/claude/skills/make-deck/references/deck.md +0 -183
- package/claude/skills/read-frames/REQUIREMENT.md +0 -30
- package/claude/skills/read-frames/SKILL.md +0 -55
- package/claude/skills/record-screencast/REQUIREMENT.md +0 -35
- package/claude/skills/record-screencast/SKILL.md +0 -99
- package/claude/skills/script-screencast/REQUIREMENT.md +0 -34
- package/claude/skills/script-screencast/SKILL.md +0 -70
- package/claude/skills/script-screencast/references/beats-template.md +0 -119
- package/claude/skills/teach-workspace/REQUIREMENT.md +0 -34
- package/claude/skills/teach-workspace/SKILL.md +0 -196
- package/claude/skills/teach-workspace/references/glossary.md +0 -90
- package/claude/skills/teach-workspace/references/lesson-build.md +0 -106
- package/claude/skills/teach-workspace/references/lesson-craft.md +0 -135
- package/claude/skills/teach-workspace/references/promotion-fold.md +0 -35
- package/claude/skills/teach-workspace/references/promotion.md +0 -116
- package/claude/skills/teach-workspace/references/revisit.md +0 -28
- package/claude/skills/teach-workspace/references/teach.md +0 -211
- package/claude/skills/video-craft/REQUIREMENT.md +0 -35
- package/claude/skills/video-craft/SKILL.md +0 -96
- package/claude/skills/video-craft/references/adopted.md +0 -110
- package/src/commands/demo.ts +0 -542
- package/src/commands/slides.ts +0 -184
- package/src/commands/teach.ts +0 -1401
- package/src/demo/beats.ts +0 -135
- package/src/demo/compile.ts +0 -350
- package/src/demo/container.ts +0 -219
- package/src/demo/cursors.ts +0 -55
- package/src/demo/drive.ts +0 -442
- package/src/demo/pointer.ts +0 -178
- package/src/demo/theme.ts +0 -112
- package/src/demo/timeline.ts +0 -64
- package/src/slides/convert/chart.ts +0 -165
- package/src/slides/convert/deck.ts +0 -244
- package/src/slides/convert/export.ts +0 -567
- package/src/slides/convert/master.ts +0 -198
- package/src/slides/convert/shapes.ts +0 -804
- package/src/slides/convert/svg.ts +0 -38
- package/src/slides/convert/walk.ts +0 -671
- package/src/slides/decks.ts +0 -110
- package/src/slides/open.ts +0 -18
- package/src/slides/package/fonts.ts +0 -301
- package/src/slides/package/motion.ts +0 -316
- package/src/slides/package/test-face.ts +0 -113
- package/src/teach/browser/compile.ts +0 -44
- package/src/teach/browser/dismiss.ts +0 -76
- package/src/teach/browser/focus-line.ts +0 -19
- package/src/teach/browser/glossary-filter.ts +0 -44
- package/src/teach/browser/head.ts +0 -30
- package/src/teach/browser/quiz.ts +0 -19
- package/src/teach/browser/reload.ts +0 -9
- package/src/teach/browser/sidebar.ts +0 -233
- package/src/teach/browser/theme.ts +0 -18
- package/src/teach/components/code.tsx +0 -24
- package/src/teach/components/heading.tsx +0 -11
- package/src/teach/components/list.tsx +0 -17
- package/src/teach/components/paragraph.tsx +0 -29
- package/src/teach/components/refs.tsx +0 -28
- package/src/teach/cross-references.ts +0 -78
- package/src/teach/fonts.ts +0 -16
- package/src/teach/highlight.ts +0 -71
- package/src/teach/html/jsx-dev-runtime.ts +0 -13
- package/src/teach/html/jsx-runtime.ts +0 -113
- package/src/teach/html/raw.ts +0 -28
- package/src/teach/lesson.ts +0 -178
- package/src/teach/nav.ts +0 -1337
- package/src/teach/render.ts +0 -240
- package/src/teach/workspace.ts +0 -1164
- package/src/teach/write-if-changed.ts +0 -17
package/src/commands/teach.ts
DELETED
|
@@ -1,1401 +0,0 @@
|
|
|
1
|
-
import { readdirSync, statSync } from 'node:fs'
|
|
2
|
-
import { isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
3
|
-
import type { Command } from 'commander'
|
|
4
|
-
import { LiveStream } from '@/serve/live'
|
|
5
|
-
import {
|
|
6
|
-
logServing,
|
|
7
|
-
parsePort,
|
|
8
|
-
serveFields,
|
|
9
|
-
waitForInterrupt,
|
|
10
|
-
} from '@/serve/report'
|
|
11
|
-
import { DEFAULT_PORT, LIVE_EVENTS_PATH, startServer } from '@/serve/static'
|
|
12
|
-
import { compileScript } from '@/teach/browser/compile'
|
|
13
|
-
import { reload } from '@/teach/browser/reload'
|
|
14
|
-
import { type LessonOutcome, planLesson } from '@/teach/lesson'
|
|
15
|
-
import { type NavGenerated, type NavOutcome, generateNav } from '@/teach/nav'
|
|
16
|
-
import type { RenderOutcome } from '@/teach/render'
|
|
17
|
-
import {
|
|
18
|
-
defineTerms,
|
|
19
|
-
type ListOutcome,
|
|
20
|
-
listWorkspaces,
|
|
21
|
-
type OpenOutcome,
|
|
22
|
-
openWorkspace,
|
|
23
|
-
type ReadOutcome,
|
|
24
|
-
readWorkspace,
|
|
25
|
-
recordSources,
|
|
26
|
-
type RevisitItem,
|
|
27
|
-
type Source,
|
|
28
|
-
type SourceOutcome,
|
|
29
|
-
type TeachRefused,
|
|
30
|
-
type Term,
|
|
31
|
-
type TermOutcome,
|
|
32
|
-
teachDir,
|
|
33
|
-
type WorkspaceSummary,
|
|
34
|
-
writeStylesheet,
|
|
35
|
-
} from '@/teach/workspace'
|
|
36
|
-
import {
|
|
37
|
-
intro,
|
|
38
|
-
logAdd,
|
|
39
|
-
logError,
|
|
40
|
-
logInfo,
|
|
41
|
-
logStep,
|
|
42
|
-
logWarn,
|
|
43
|
-
outro,
|
|
44
|
-
pipeOutput,
|
|
45
|
-
} from '@/cli/ui'
|
|
46
|
-
import { mainWorktreeRoot } from '@/git/worktree'
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* A pair as the command line spells it, split on the first `=` so a value
|
|
50
|
-
* carrying its own separator survives. A source URL is the case that needs it.
|
|
51
|
-
*/
|
|
52
|
-
const PAIR = /^([^=]+)=([\s\S]+)$/
|
|
53
|
-
|
|
54
|
-
interface ListCommandOptions {
|
|
55
|
-
readonly json?: boolean
|
|
56
|
-
readonly root?: string
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface OpenCommandOptions {
|
|
60
|
-
readonly date?: string
|
|
61
|
-
readonly json?: boolean
|
|
62
|
-
readonly outOfScope?: readonly string[]
|
|
63
|
-
readonly root?: string
|
|
64
|
-
readonly startingPoint?: string
|
|
65
|
-
readonly subject?: string
|
|
66
|
-
readonly success?: readonly string[]
|
|
67
|
-
readonly title?: string
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
interface ResourceCommandOptions {
|
|
71
|
-
readonly json?: boolean
|
|
72
|
-
readonly lead?: readonly string[]
|
|
73
|
-
readonly read?: readonly string[]
|
|
74
|
-
readonly root?: string
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
interface LessonCommandOptions {
|
|
78
|
-
readonly json?: boolean
|
|
79
|
-
readonly options?: string
|
|
80
|
-
readonly questions?: string
|
|
81
|
-
readonly root?: string
|
|
82
|
-
readonly slug?: string
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
interface GlossaryCommandOptions {
|
|
86
|
-
readonly firstSeen?: string
|
|
87
|
-
readonly json?: boolean
|
|
88
|
-
readonly root?: string
|
|
89
|
-
readonly term?: readonly string[]
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
interface NavCommandOptions {
|
|
93
|
-
readonly json?: boolean
|
|
94
|
-
readonly root?: string
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
interface UpCommandOptions {
|
|
98
|
-
readonly json?: boolean
|
|
99
|
-
readonly port?: string
|
|
100
|
-
readonly root?: string
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
interface RenderCommandOptions {
|
|
104
|
-
readonly json?: boolean
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function register(program: Command): void {
|
|
108
|
-
const teach = program
|
|
109
|
-
.command('teach')
|
|
110
|
-
.description('Manage learning workspaces in .canon/teach/')
|
|
111
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
112
|
-
.addHelpText(
|
|
113
|
-
'after',
|
|
114
|
-
[
|
|
115
|
-
'',
|
|
116
|
-
'Viewing a workspace:',
|
|
117
|
-
' canon teach up <topic>',
|
|
118
|
-
'',
|
|
119
|
-
'It refreshes the chrome and serves the workspace. To serve without',
|
|
120
|
-
'rewriting anything, canon teach list <topic> prints the canon serve',
|
|
121
|
-
'line, with the teach folder written from where you stand.',
|
|
122
|
-
'',
|
|
123
|
-
'Reasons, on stderr or in the JSON record:',
|
|
124
|
-
' no-teach the root carries no .canon/teach/ folder',
|
|
125
|
-
' no-workspace no workspace matches the topic',
|
|
126
|
-
' ambiguous two workspaces claim one topic',
|
|
127
|
-
' exists a workspace already covers the topic an open names',
|
|
128
|
-
' no-file the workspace carries no file the verb writes into',
|
|
129
|
-
' no-section RESOURCES.md carries no heading the entries belong under',
|
|
130
|
-
' listed a URL is already read, or a lead is offered as listed',
|
|
131
|
-
' defined a term already carries a glossary entry',
|
|
132
|
-
' bad-input the command line is malformed',
|
|
133
|
-
' no-port up found every port in reach taken',
|
|
134
|
-
' bind-failed up could not bind, such as a privileged port',
|
|
135
|
-
'',
|
|
136
|
-
].join('\n'),
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
teach
|
|
140
|
-
.command('list')
|
|
141
|
-
.description('List learning workspaces, or what one workspace holds')
|
|
142
|
-
.argument('[topic]', 'Workspace folder or topic, as in regular-expressions')
|
|
143
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
144
|
-
.option('--json', 'Emit a machine-readable record on stdout')
|
|
145
|
-
.option('--root <path>', 'Teach root, defaulting to the main worktree')
|
|
146
|
-
.addHelpText(
|
|
147
|
-
'after',
|
|
148
|
-
[
|
|
149
|
-
'',
|
|
150
|
-
'Exit codes:',
|
|
151
|
-
' 0 the workspaces or their contents were listed',
|
|
152
|
-
' 1 refused, with the reason on stderr or in the JSON record',
|
|
153
|
-
'',
|
|
154
|
-
'With no topic it reports one line per workspace and the ordinal an',
|
|
155
|
-
'open would take. With one it reports the files behind each count.',
|
|
156
|
-
'',
|
|
157
|
-
'Every record carries due, what the learning records schedule, each',
|
|
158
|
-
'entry with its date, its rung, and the date to write on a hit and',
|
|
159
|
-
'on a miss, so a session picks the next lesson from the schedule',
|
|
160
|
-
'rather than from the last session alone.',
|
|
161
|
-
'',
|
|
162
|
-
'A workspace not named NN-<topic> is still listed. It sorts last and',
|
|
163
|
-
'moves no ordinal, since dropping it hides the folder needing a fix.',
|
|
164
|
-
'',
|
|
165
|
-
'The human output ends with the canon serve line that opens a',
|
|
166
|
-
'workspace, a template with no topic and the exact line with one.',
|
|
167
|
-
'',
|
|
168
|
-
'Examples:',
|
|
169
|
-
' canon teach list',
|
|
170
|
-
' canon teach list regular-expressions --json',
|
|
171
|
-
'',
|
|
172
|
-
].join('\n'),
|
|
173
|
-
)
|
|
174
|
-
.action(async (topic: string | undefined, opts: ListCommandOptions) => {
|
|
175
|
-
process.exitCode = await runList(topic, opts)
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
teach
|
|
179
|
-
.command('open')
|
|
180
|
-
.description('Open a workspace at the next ordinal with its required files')
|
|
181
|
-
.argument('<topic>', 'Kebab-case topic, as in regular-expressions')
|
|
182
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
183
|
-
.option('--subject <line>', 'One line stating what the workspace covers')
|
|
184
|
-
.option('--starting-point <text>', 'What the learner already knows')
|
|
185
|
-
.option(
|
|
186
|
-
'--success <line>',
|
|
187
|
-
'Observable thing the learner will be able to do, repeatable',
|
|
188
|
-
collect,
|
|
189
|
-
[] as string[],
|
|
190
|
-
)
|
|
191
|
-
.option(
|
|
192
|
-
'--out-of-scope <line>',
|
|
193
|
-
'What this workspace does not cover, repeatable',
|
|
194
|
-
collect,
|
|
195
|
-
[] as string[],
|
|
196
|
-
)
|
|
197
|
-
.option('--title <text>', 'Title, defaulting to the topic in sentence case')
|
|
198
|
-
.option('--date <YYYY-MM-DD>', 'Opening date, defaulting to today')
|
|
199
|
-
.option('--json', 'Emit a machine-readable record on stdout')
|
|
200
|
-
.option('--root <path>', 'Teach root, defaulting to the main worktree')
|
|
201
|
-
.addHelpText(
|
|
202
|
-
'after',
|
|
203
|
-
[
|
|
204
|
-
'',
|
|
205
|
-
'Exit codes:',
|
|
206
|
-
' 0 the workspace was created',
|
|
207
|
-
' 1 refused, with the reason on stderr or in the JSON record',
|
|
208
|
-
'',
|
|
209
|
-
'It derives the ordinal from the highest already present and writes',
|
|
210
|
-
'MISSION.md, RESOURCES.md, and GLOSSARY.md. A topic another workspace',
|
|
211
|
-
'already covers is refused, since a second one forks the records.',
|
|
212
|
-
'It creates a workspace and serves nothing: canon teach up views one.',
|
|
213
|
-
'',
|
|
214
|
-
'Examples:',
|
|
215
|
-
' canon teach open regular-expressions --subject "Reading and writing regular expressions" \\',
|
|
216
|
-
' --starting-point "Comfortable with the shell, has never written a group" \\',
|
|
217
|
-
' --success "Write a pattern matching a date" --success "Explain a backreference"',
|
|
218
|
-
'',
|
|
219
|
-
].join('\n'),
|
|
220
|
-
)
|
|
221
|
-
.action(async (topic: string, opts: OpenCommandOptions) => {
|
|
222
|
-
process.exitCode = await runOpen(topic, opts)
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
teach
|
|
226
|
-
.command('resource')
|
|
227
|
-
.description('Record sources and leads in a workspace RESOURCES.md')
|
|
228
|
-
.argument('<topic>', 'Workspace folder or topic, as in regular-expressions')
|
|
229
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
230
|
-
.option(
|
|
231
|
-
'--read <title=url>',
|
|
232
|
-
'Source that stands behind the material, repeatable',
|
|
233
|
-
collect,
|
|
234
|
-
[] as string[],
|
|
235
|
-
)
|
|
236
|
-
.option(
|
|
237
|
-
'--lead <title=url>',
|
|
238
|
-
'Source found and not opened, repeatable',
|
|
239
|
-
collect,
|
|
240
|
-
[] as string[],
|
|
241
|
-
)
|
|
242
|
-
.option('--json', 'Emit a machine-readable record on stdout')
|
|
243
|
-
.option('--root <path>', 'Teach root, defaulting to the main worktree')
|
|
244
|
-
.addHelpText(
|
|
245
|
-
'after',
|
|
246
|
-
[
|
|
247
|
-
'',
|
|
248
|
-
'Exit codes:',
|
|
249
|
-
' 0 every source was written',
|
|
250
|
-
' 1 refused, with the reason on stderr or in the JSON record',
|
|
251
|
-
'',
|
|
252
|
-
'The pair splits on the first =, so a URL carrying one survives. Say',
|
|
253
|
-
'in the title which claims rest on the source, since the entry is the',
|
|
254
|
-
'only place a later session reads that from.',
|
|
255
|
-
'',
|
|
256
|
-
'A URL already listed under either heading is refused rather than',
|
|
257
|
-
'repeated, because two entries split what rests on one source. The one',
|
|
258
|
-
'exception is a --read URL listed under Leads, which moves to Read under',
|
|
259
|
-
'its new title.',
|
|
260
|
-
'',
|
|
261
|
-
'Examples:',
|
|
262
|
-
' canon teach resource regular-expressions --read "MDN regular expressions=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions"',
|
|
263
|
-
' canon teach resource regular-expressions --lead "RE2 syntax=https://github.com/google/re2/wiki/Syntax" --json',
|
|
264
|
-
'',
|
|
265
|
-
].join('\n'),
|
|
266
|
-
)
|
|
267
|
-
.action(async (topic: string, opts: ResourceCommandOptions) => {
|
|
268
|
-
process.exitCode = await runResource(topic, opts)
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
teach
|
|
272
|
-
.command('glossary')
|
|
273
|
-
.description('Add terms to a workspace GLOSSARY.md, alphabetically')
|
|
274
|
-
.argument('<topic>', 'Workspace folder or topic, as in regular-expressions')
|
|
275
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
276
|
-
.option(
|
|
277
|
-
'--term <term=definition>',
|
|
278
|
-
'Term the subject defines, repeatable',
|
|
279
|
-
collect,
|
|
280
|
-
[] as string[],
|
|
281
|
-
)
|
|
282
|
-
.option(
|
|
283
|
-
'--first-seen <file>',
|
|
284
|
-
'Lesson or reference page the batch first defines these in',
|
|
285
|
-
)
|
|
286
|
-
.option('--json', 'Emit a machine-readable record on stdout')
|
|
287
|
-
.option('--root <path>', 'Teach root, defaulting to the main worktree')
|
|
288
|
-
.addHelpText(
|
|
289
|
-
'after',
|
|
290
|
-
[
|
|
291
|
-
'',
|
|
292
|
-
'Exit codes:',
|
|
293
|
-
' 0 every term now carries an entry',
|
|
294
|
-
' 1 refused, with the reason on stderr or in the JSON record',
|
|
295
|
-
'',
|
|
296
|
-
'One call writes one file, which is what keeps a batch from racing on',
|
|
297
|
-
'the glossary every term shares. --first-seen names one page for the',
|
|
298
|
-
'whole batch, since a batch comes from one lesson.',
|
|
299
|
-
'',
|
|
300
|
-
'A term already defined is refused rather than replaced. A definition',
|
|
301
|
-
'the subject has moved under is a revision of the entry it has.',
|
|
302
|
-
'',
|
|
303
|
-
'Examples:',
|
|
304
|
-
' canon teach glossary regular-expressions --term "capture group=A parenthesised part of a pattern whose match is kept" --first-seen 0002-groups.html',
|
|
305
|
-
'',
|
|
306
|
-
].join('\n'),
|
|
307
|
-
)
|
|
308
|
-
.action(async (topic: string, opts: GlossaryCommandOptions) => {
|
|
309
|
-
process.exitCode = await runGlossary(topic, opts)
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
teach
|
|
313
|
-
.command('lesson')
|
|
314
|
-
.description('Resolve what the next lesson needs before it is written')
|
|
315
|
-
.argument('<topic>', 'Workspace folder or topic, as in regular-expressions')
|
|
316
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
317
|
-
.option('--slug <kebab>', "The lesson's own topic, as in capture-groups")
|
|
318
|
-
.option('--questions <n>', 'How many questions the quiz carries')
|
|
319
|
-
.option('--options <n>', 'How many options each question carries', '4')
|
|
320
|
-
.option('--json', 'Emit a machine-readable record on stdout')
|
|
321
|
-
.option('--root <path>', 'Teach root, defaulting to the main worktree')
|
|
322
|
-
.addHelpText(
|
|
323
|
-
'after',
|
|
324
|
-
[
|
|
325
|
-
'',
|
|
326
|
-
'Exit codes:',
|
|
327
|
-
' 0 the lesson was planned',
|
|
328
|
-
' 1 refused, with the reason on stderr or in the JSON record',
|
|
329
|
-
'',
|
|
330
|
-
'It writes nothing. It reports the numbered path the lesson takes, the',
|
|
331
|
-
'shared stylesheet with whether that file exists yet, the mission',
|
|
332
|
-
'success lines to report progress against, and one option order per',
|
|
333
|
-
'question.',
|
|
334
|
-
'',
|
|
335
|
-
'Write the correct option first and present the options in the order',
|
|
336
|
-
'reported. Position is drawn here rather than chosen, so an answer',
|
|
337
|
-
'cannot settle into the first slot.',
|
|
338
|
-
'',
|
|
339
|
-
'Examples:',
|
|
340
|
-
' canon teach lesson regular-expressions --slug capture-groups --questions 3 --json',
|
|
341
|
-
'',
|
|
342
|
-
].join('\n'),
|
|
343
|
-
)
|
|
344
|
-
.action(async (topic: string, opts: LessonCommandOptions) => {
|
|
345
|
-
process.exitCode = await runLesson(topic, opts)
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
teach
|
|
349
|
-
.command('stylesheet')
|
|
350
|
-
.description('Seed a workspace stylesheet from the design source')
|
|
351
|
-
.argument('<topic>', 'Workspace folder or topic, as in regular-expressions')
|
|
352
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
353
|
-
.option('--force', 'Rewrite a stylesheet the workspace already carries')
|
|
354
|
-
.option('--json', 'Emit a machine-readable record on stdout')
|
|
355
|
-
.option('--root <path>', 'Teach root, defaulting to the main worktree')
|
|
356
|
-
.addHelpText(
|
|
357
|
-
'after',
|
|
358
|
-
[
|
|
359
|
-
'',
|
|
360
|
-
'Exit codes:',
|
|
361
|
-
' 0 the workspace carries a stylesheet, written now or already there',
|
|
362
|
-
' 1 refused, with the reason on stderr or in the JSON record',
|
|
363
|
-
'',
|
|
364
|
-
'The file it writes carries the design tokens as custom properties and',
|
|
365
|
-
'the component rules built on them, so a workspace renders in the same',
|
|
366
|
-
'system every other surface does. Add lesson rules under the seed and',
|
|
367
|
-
'read a value through its property rather than restating the hex.',
|
|
368
|
-
'',
|
|
369
|
-
'An existing stylesheet is left alone, since a workspace adds to this',
|
|
370
|
-
'file as it goes. Pass --force to take the seed back over it.',
|
|
371
|
-
'',
|
|
372
|
-
'Examples:',
|
|
373
|
-
' canon teach stylesheet regular-expressions --json',
|
|
374
|
-
'',
|
|
375
|
-
].join('\n'),
|
|
376
|
-
)
|
|
377
|
-
.action(async (topic: string, opts: StylesheetCommandOptions) => {
|
|
378
|
-
process.exitCode = await runStylesheet(topic, opts)
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
teach
|
|
382
|
-
.command('nav')
|
|
383
|
-
.description('Rewrite the teach root, contents pages, and lesson chrome')
|
|
384
|
-
.argument('[topic]', 'Workspace folder or topic, scoping the run to one')
|
|
385
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
386
|
-
.option('--json', 'Emit a machine-readable record on stdout')
|
|
387
|
-
.option('--root <path>', 'Teach root, defaulting to the main worktree')
|
|
388
|
-
.addHelpText(
|
|
389
|
-
'after',
|
|
390
|
-
[
|
|
391
|
-
'',
|
|
392
|
-
'Exit codes:',
|
|
393
|
-
' 0 the root, the contents page(s), and every found lesson chrome were written',
|
|
394
|
-
' 1 refused, with the reason on stderr or in the JSON record',
|
|
395
|
-
'',
|
|
396
|
-
'Rewrites the teach-root listing, the contents page of every workspace',
|
|
397
|
-
"or of the one topic named, and each of its lessons' chrome: the",
|
|
398
|
-
'embedded stylesheet, the header with its breadcrumb and jump menus,',
|
|
399
|
-
'the prev/next footer nav, and the behavior scripts. The authored',
|
|
400
|
-
'<h1>, lede, body, and quiz are left untouched.',
|
|
401
|
-
'',
|
|
402
|
-
"A workspace's generated assets/base.css is rewritten on every run, so",
|
|
403
|
-
'a CLI upgrade reaches it. Its course.css is never rewritten, and a',
|
|
404
|
-
'workspace holding only a course.css is not given a base.css.',
|
|
405
|
-
'',
|
|
406
|
-
'A lesson missing one of the four chrome markers is refused rather',
|
|
407
|
-
"than rewritten, reported by name in the JSON record's skipped list",
|
|
408
|
-
'and on stderr, while every other lesson still rewrites.',
|
|
409
|
-
'',
|
|
410
|
-
'Examples:',
|
|
411
|
-
' canon teach nav',
|
|
412
|
-
' canon teach nav regular-expressions --json',
|
|
413
|
-
'',
|
|
414
|
-
].join('\n'),
|
|
415
|
-
)
|
|
416
|
-
.action(async (topic: string | undefined, opts: NavCommandOptions) => {
|
|
417
|
-
process.exitCode = await runNav(topic, opts)
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
teach
|
|
421
|
-
.command('up')
|
|
422
|
-
.description('Refresh the chrome with nav, then serve the teach folder')
|
|
423
|
-
.argument('[topic]', 'Workspace folder or topic the printed link opens')
|
|
424
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
425
|
-
.option('--port <number>', `Port to try first, default ${DEFAULT_PORT}`)
|
|
426
|
-
.option('--json', 'Emit a machine-readable record on stdout')
|
|
427
|
-
.option('--root <path>', 'Teach root, defaulting to the main worktree')
|
|
428
|
-
.addHelpText(
|
|
429
|
-
'after',
|
|
430
|
-
[
|
|
431
|
-
'',
|
|
432
|
-
'Exit codes:',
|
|
433
|
-
' 0 the server stopped after running',
|
|
434
|
-
' 1 refused, with the reason on stderr or in the JSON record',
|
|
435
|
-
'',
|
|
436
|
-
'Runs the nav pass for the topic, or for every workspace when none is',
|
|
437
|
-
'named, then serves the teach folder through the same server as',
|
|
438
|
-
'canon serve. The link opens the topic contents page, or the root',
|
|
439
|
-
'listing with no topic. A lesson nav skips is reported and the server',
|
|
440
|
-
'still starts, while a nav refusal starts no server.',
|
|
441
|
-
'',
|
|
442
|
-
'Viewing writes: every page and generated stylesheet nav owns is',
|
|
443
|
-
'rewritten first. canon teach list <topic> prints a canon serve line',
|
|
444
|
-
'that serves without rewriting.',
|
|
445
|
-
'',
|
|
446
|
-
'It runs until interrupted. A session wanting the link without waiting',
|
|
447
|
-
'starts it in the background and reads the record off stdout.',
|
|
448
|
-
'',
|
|
449
|
-
'Examples:',
|
|
450
|
-
' canon teach up regular-expressions',
|
|
451
|
-
' canon teach up --port 4000 --json',
|
|
452
|
-
'',
|
|
453
|
-
].join('\n'),
|
|
454
|
-
)
|
|
455
|
-
.action(async (topic: string | undefined, opts: UpCommandOptions) => {
|
|
456
|
-
process.exitCode = await runUp(topic, opts)
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
teach
|
|
460
|
-
.command('render')
|
|
461
|
-
.description('Render a lesson body block list to HTML')
|
|
462
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
463
|
-
.option('--json', 'Emit a machine-readable record on stdout')
|
|
464
|
-
.addHelpText(
|
|
465
|
-
'after',
|
|
466
|
-
[
|
|
467
|
-
'',
|
|
468
|
-
'Exit codes:',
|
|
469
|
-
' 0 the block list rendered',
|
|
470
|
-
' 1 refused, with the reason on stderr or in the JSON record',
|
|
471
|
-
'',
|
|
472
|
-
'Reads a JSON array of blocks from stdin, each a heading, paragraph,',
|
|
473
|
-
'list, refs, code, or raw block, and renders it through the same',
|
|
474
|
-
'components the fixture lesson is generated from. Content the',
|
|
475
|
-
'structured blocks cannot express takes type raw, carrying its own',
|
|
476
|
-
'html verbatim, unescaped.',
|
|
477
|
-
'',
|
|
478
|
-
'A code block takes text and an optional lang, a highlight.js name or',
|
|
479
|
-
'alias matching ^[a-z0-9+#-]+$, and renders highlighted at build time',
|
|
480
|
-
'with no script. An unregistered lang renders as plain escaped code.',
|
|
481
|
-
'',
|
|
482
|
-
'A paragraph takes an optional cites array of reference numbers,',
|
|
483
|
-
'rendered as footnote markers after its text. The one refs block',
|
|
484
|
-
'lists { title, url?, note? } items numbered from 1, and a cite that',
|
|
485
|
-
'resolves to no item, a cite on a lede, or a second refs block is',
|
|
486
|
-
'refused. A url must be http or https.',
|
|
487
|
-
'',
|
|
488
|
-
'Takes no topic and no --root: the verb is a stateless transform,',
|
|
489
|
-
'reading nothing off a workspace on disk.',
|
|
490
|
-
'',
|
|
491
|
-
'Examples:',
|
|
492
|
-
' echo \'[{"type":"heading","level":1,"text":"Compass bearings"}]\' | canon teach render --json',
|
|
493
|
-
'',
|
|
494
|
-
].join('\n'),
|
|
495
|
-
)
|
|
496
|
-
.action(async (opts: RenderCommandOptions) => {
|
|
497
|
-
process.exitCode = await runRender(opts)
|
|
498
|
-
})
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
interface StylesheetCommandOptions {
|
|
502
|
-
readonly force?: boolean
|
|
503
|
-
readonly json?: boolean
|
|
504
|
-
readonly root?: string
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
async function runStylesheet(
|
|
508
|
-
topic: string,
|
|
509
|
-
opts: StylesheetCommandOptions,
|
|
510
|
-
): Promise<number> {
|
|
511
|
-
const emitJson = opts.json ?? false
|
|
512
|
-
const root = await rootFor(opts.root)
|
|
513
|
-
const outcome = await writeStylesheet(root, topic, opts.force ?? false)
|
|
514
|
-
|
|
515
|
-
if (!outcome.ok) {
|
|
516
|
-
return reportRefusal('canon teach stylesheet', outcome, emitJson, root)
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (emitJson) {
|
|
520
|
-
process.stdout.write(
|
|
521
|
-
`${JSON.stringify({
|
|
522
|
-
ok: true,
|
|
523
|
-
root,
|
|
524
|
-
slug: outcome.slug,
|
|
525
|
-
path: outcome.path,
|
|
526
|
-
basePath: outcome.basePath,
|
|
527
|
-
written: outcome.written,
|
|
528
|
-
})}\n`,
|
|
529
|
-
)
|
|
530
|
-
return 0
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
intro('canon teach stylesheet')
|
|
534
|
-
logStep(outcome.written ? 'Written' : 'Already present, left alone')
|
|
535
|
-
logInfo(outcome.path)
|
|
536
|
-
logInfo(outcome.basePath)
|
|
537
|
-
outro()
|
|
538
|
-
return 0
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function collect(value: string, previous: string[]): string[] {
|
|
542
|
-
return [...previous, value]
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function readStdin(): Promise<string> {
|
|
546
|
-
return new Promise((resolveStream, rejectStream) => {
|
|
547
|
-
const chunks: Buffer[] = []
|
|
548
|
-
process.stdin.on('data', (chunk: Buffer) => chunks.push(chunk))
|
|
549
|
-
process.stdin.on('end', () =>
|
|
550
|
-
resolveStream(Buffer.concat(chunks).toString('utf8')),
|
|
551
|
-
)
|
|
552
|
-
process.stdin.on('error', rejectStream)
|
|
553
|
-
})
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Splits every pair or reports the ones that carry no separator. Both halves
|
|
558
|
-
* are reported together, so a caller passing four pairs learns about all the
|
|
559
|
-
* broken ones from one run rather than from four.
|
|
560
|
-
*/
|
|
561
|
-
function parsePairs(
|
|
562
|
-
raw: readonly string[],
|
|
563
|
-
): { pairs: Array<[string, string]> } | { invalid: string[] } {
|
|
564
|
-
const pairs: Array<[string, string]> = []
|
|
565
|
-
const invalid: string[] = []
|
|
566
|
-
|
|
567
|
-
for (const entry of raw) {
|
|
568
|
-
const match = PAIR.exec(entry)
|
|
569
|
-
const left = match?.[1].trim()
|
|
570
|
-
const right = match?.[2].trim()
|
|
571
|
-
|
|
572
|
-
if (!left || !right) {
|
|
573
|
-
invalid.push(entry)
|
|
574
|
-
continue
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
pairs.push([left, right])
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
return invalid.length > 0 ? { invalid } : { pairs }
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function badInput(
|
|
584
|
-
message: string,
|
|
585
|
-
detail: readonly string[] = [],
|
|
586
|
-
): TeachRefused {
|
|
587
|
-
return { ok: false, reason: 'bad-input', message, detail }
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
async function rootFor(given: string | undefined): Promise<string> {
|
|
591
|
-
return given ?? (await mainWorktreeRoot())
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
async function runList(
|
|
595
|
-
topic: string | undefined,
|
|
596
|
-
opts: ListCommandOptions,
|
|
597
|
-
): Promise<number> {
|
|
598
|
-
const emitJson = opts.json ?? false
|
|
599
|
-
const root = await rootFor(opts.root)
|
|
600
|
-
|
|
601
|
-
if (topic === undefined) {
|
|
602
|
-
return reportList(await listWorkspaces(root), emitJson, root)
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
return reportWorkspace(await readWorkspace(root, topic), emitJson, root)
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
async function runOpen(
|
|
609
|
-
topic: string,
|
|
610
|
-
opts: OpenCommandOptions,
|
|
611
|
-
): Promise<number> {
|
|
612
|
-
const emitJson = opts.json ?? false
|
|
613
|
-
|
|
614
|
-
if (!opts.subject) {
|
|
615
|
-
return reportRefusal(
|
|
616
|
-
'canon teach open',
|
|
617
|
-
badInput('No subject. Pass --subject <line>.'),
|
|
618
|
-
emitJson,
|
|
619
|
-
process.cwd(),
|
|
620
|
-
)
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
if (!opts.startingPoint) {
|
|
624
|
-
return reportRefusal(
|
|
625
|
-
'canon teach open',
|
|
626
|
-
badInput(
|
|
627
|
-
'No starting point. Pass --starting-point <text>, so difficulty has a floor.',
|
|
628
|
-
),
|
|
629
|
-
emitJson,
|
|
630
|
-
process.cwd(),
|
|
631
|
-
)
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const root = await rootFor(opts.root)
|
|
635
|
-
|
|
636
|
-
return reportOpen(
|
|
637
|
-
await openWorkspace(root, {
|
|
638
|
-
topic,
|
|
639
|
-
subject: opts.subject,
|
|
640
|
-
startingPoint: opts.startingPoint,
|
|
641
|
-
success: opts.success ?? [],
|
|
642
|
-
outOfScope: opts.outOfScope ?? [],
|
|
643
|
-
title: opts.title,
|
|
644
|
-
date: opts.date,
|
|
645
|
-
}),
|
|
646
|
-
emitJson,
|
|
647
|
-
root,
|
|
648
|
-
)
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
async function runResource(
|
|
652
|
-
topic: string,
|
|
653
|
-
opts: ResourceCommandOptions,
|
|
654
|
-
): Promise<number> {
|
|
655
|
-
const emitJson = opts.json ?? false
|
|
656
|
-
const raw = [...(opts.read ?? []), ...(opts.lead ?? [])]
|
|
657
|
-
|
|
658
|
-
if (raw.length === 0) {
|
|
659
|
-
return reportRefusal(
|
|
660
|
-
'canon teach resource',
|
|
661
|
-
badInput('No source given. Pass --read or --lead as <title>=<url>.'),
|
|
662
|
-
emitJson,
|
|
663
|
-
process.cwd(),
|
|
664
|
-
)
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
const parsed = parsePairs(raw)
|
|
668
|
-
|
|
669
|
-
if ('invalid' in parsed) {
|
|
670
|
-
return reportRefusal(
|
|
671
|
-
'canon teach resource',
|
|
672
|
-
badInput(
|
|
673
|
-
`Not a title and url: ${parsed.invalid.join(', ')}`,
|
|
674
|
-
parsed.invalid,
|
|
675
|
-
),
|
|
676
|
-
emitJson,
|
|
677
|
-
process.cwd(),
|
|
678
|
-
)
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
const readCount = (opts.read ?? []).length
|
|
682
|
-
const sources: Source[] = parsed.pairs.map(([title, url]) => ({ title, url }))
|
|
683
|
-
const root = await rootFor(opts.root)
|
|
684
|
-
|
|
685
|
-
return reportResource(
|
|
686
|
-
await recordSources(
|
|
687
|
-
root,
|
|
688
|
-
topic,
|
|
689
|
-
sources.slice(0, readCount),
|
|
690
|
-
sources.slice(readCount),
|
|
691
|
-
),
|
|
692
|
-
emitJson,
|
|
693
|
-
root,
|
|
694
|
-
)
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
async function runGlossary(
|
|
698
|
-
topic: string,
|
|
699
|
-
opts: GlossaryCommandOptions,
|
|
700
|
-
): Promise<number> {
|
|
701
|
-
const emitJson = opts.json ?? false
|
|
702
|
-
const raw = opts.term ?? []
|
|
703
|
-
|
|
704
|
-
if (raw.length === 0) {
|
|
705
|
-
return reportRefusal(
|
|
706
|
-
'canon teach glossary',
|
|
707
|
-
badInput('No term given. Pass --term <term>=<definition>.'),
|
|
708
|
-
emitJson,
|
|
709
|
-
process.cwd(),
|
|
710
|
-
)
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
const parsed = parsePairs(raw)
|
|
714
|
-
|
|
715
|
-
if ('invalid' in parsed) {
|
|
716
|
-
return reportRefusal(
|
|
717
|
-
'canon teach glossary',
|
|
718
|
-
badInput(
|
|
719
|
-
`Not a term and definition: ${parsed.invalid.join(', ')}`,
|
|
720
|
-
parsed.invalid,
|
|
721
|
-
),
|
|
722
|
-
emitJson,
|
|
723
|
-
process.cwd(),
|
|
724
|
-
)
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
const terms: Term[] = parsed.pairs.map(([term, definition]) => ({
|
|
728
|
-
term,
|
|
729
|
-
definition,
|
|
730
|
-
}))
|
|
731
|
-
|
|
732
|
-
const duplicated = terms
|
|
733
|
-
.map((term) => term.term.toLowerCase())
|
|
734
|
-
.filter((term, index, all) => all.indexOf(term) !== index)
|
|
735
|
-
|
|
736
|
-
if (duplicated.length > 0) {
|
|
737
|
-
return reportRefusal(
|
|
738
|
-
'canon teach glossary',
|
|
739
|
-
badInput(`Two definitions for ${[...new Set(duplicated)].join(', ')}.`),
|
|
740
|
-
emitJson,
|
|
741
|
-
process.cwd(),
|
|
742
|
-
)
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
const root = await rootFor(opts.root)
|
|
746
|
-
|
|
747
|
-
return reportGlossary(
|
|
748
|
-
await defineTerms(root, topic, terms, opts.firstSeen),
|
|
749
|
-
emitJson,
|
|
750
|
-
root,
|
|
751
|
-
)
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
async function runLesson(
|
|
755
|
-
topic: string,
|
|
756
|
-
opts: LessonCommandOptions,
|
|
757
|
-
): Promise<number> {
|
|
758
|
-
const emitJson = opts.json ?? false
|
|
759
|
-
|
|
760
|
-
if (!opts.slug) {
|
|
761
|
-
return reportRefusal(
|
|
762
|
-
'canon teach lesson',
|
|
763
|
-
badInput('No lesson slug. Pass --slug <kebab>.'),
|
|
764
|
-
emitJson,
|
|
765
|
-
process.cwd(),
|
|
766
|
-
)
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
if (!opts.questions) {
|
|
770
|
-
return reportRefusal(
|
|
771
|
-
'canon teach lesson',
|
|
772
|
-
badInput('No question count. Pass --questions <n>.'),
|
|
773
|
-
emitJson,
|
|
774
|
-
process.cwd(),
|
|
775
|
-
)
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
const root = await rootFor(opts.root)
|
|
779
|
-
|
|
780
|
-
return reportLesson(
|
|
781
|
-
await planLesson(root, topic, {
|
|
782
|
-
slug: opts.slug,
|
|
783
|
-
questions: Number(opts.questions),
|
|
784
|
-
options: Number(opts.options ?? '4'),
|
|
785
|
-
}),
|
|
786
|
-
emitJson,
|
|
787
|
-
root,
|
|
788
|
-
)
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
async function runNav(
|
|
792
|
-
topic: string | undefined,
|
|
793
|
-
opts: NavCommandOptions,
|
|
794
|
-
): Promise<number> {
|
|
795
|
-
const emitJson = opts.json ?? false
|
|
796
|
-
const root = await rootFor(opts.root)
|
|
797
|
-
|
|
798
|
-
return reportNav(await generateNav(root, topic), emitJson, root)
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
async function runRender(opts: RenderCommandOptions): Promise<number> {
|
|
802
|
-
const emitJson = opts.json ?? false
|
|
803
|
-
|
|
804
|
-
if (process.stdin.isTTY) {
|
|
805
|
-
return reportRenderRefusal(
|
|
806
|
-
badInput(
|
|
807
|
-
"No blocks on stdin. Pipe a JSON array: echo '[...]' | canon teach render",
|
|
808
|
-
),
|
|
809
|
-
emitJson,
|
|
810
|
-
)
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
const body = (await readStdin()).trim()
|
|
814
|
-
|
|
815
|
-
if (!body) {
|
|
816
|
-
return reportRenderRefusal(
|
|
817
|
-
badInput('Empty stdin. Pipe a JSON array of blocks.'),
|
|
818
|
-
emitJson,
|
|
819
|
-
)
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
let parsed: unknown
|
|
823
|
-
try {
|
|
824
|
-
parsed = JSON.parse(body)
|
|
825
|
-
} catch {
|
|
826
|
-
return reportRenderRefusal(badInput('Malformed JSON on stdin.'), emitJson)
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
if (!Array.isArray(parsed)) {
|
|
830
|
-
return reportRenderRefusal(
|
|
831
|
-
badInput('Stdin must be a JSON array of blocks.'),
|
|
832
|
-
emitJson,
|
|
833
|
-
)
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
// Loaded here rather than at the top, since the renderer pulls in the
|
|
837
|
-
// highlighter and every other verb would pay for its grammars at startup.
|
|
838
|
-
const { renderLessonBody } = await import('@/teach/render')
|
|
839
|
-
return reportRender(renderLessonBody(parsed), emitJson)
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function reportRenderRefusal(refused: TeachRefused, emitJson: boolean): number {
|
|
843
|
-
if (emitJson) {
|
|
844
|
-
process.stderr.write(`${refused.message}\n`)
|
|
845
|
-
process.stdout.write(
|
|
846
|
-
`${JSON.stringify({
|
|
847
|
-
ok: false,
|
|
848
|
-
reason: refused.reason,
|
|
849
|
-
message: refused.message,
|
|
850
|
-
})}\n`,
|
|
851
|
-
)
|
|
852
|
-
return 1
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
intro('canon teach render')
|
|
856
|
-
logStep('Refused')
|
|
857
|
-
logError(refused.message)
|
|
858
|
-
outro()
|
|
859
|
-
return 1
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
function reportRender(outcome: RenderOutcome, emitJson: boolean): number {
|
|
863
|
-
if (!outcome.ok) return reportRenderRefusal(outcome, emitJson)
|
|
864
|
-
|
|
865
|
-
if (emitJson) {
|
|
866
|
-
process.stdout.write(
|
|
867
|
-
`${JSON.stringify({ ok: true, html: outcome.html })}\n`,
|
|
868
|
-
)
|
|
869
|
-
return 0
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
process.stdout.write(`${outcome.html}\n`)
|
|
873
|
-
return 0
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
function reportNav(
|
|
877
|
-
outcome: NavOutcome,
|
|
878
|
-
emitJson: boolean,
|
|
879
|
-
root: string,
|
|
880
|
-
): number {
|
|
881
|
-
if (!outcome.ok)
|
|
882
|
-
return reportRefusal('canon teach nav', outcome, emitJson, root)
|
|
883
|
-
|
|
884
|
-
if (emitJson) {
|
|
885
|
-
process.stdout.write(`${JSON.stringify(navFields(outcome))}\n`)
|
|
886
|
-
return 0
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
intro('canon teach nav')
|
|
890
|
-
logNav(outcome)
|
|
891
|
-
outro()
|
|
892
|
-
return 0
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
function navFields(outcome: NavGenerated): Record<string, unknown> {
|
|
896
|
-
return {
|
|
897
|
-
ok: true,
|
|
898
|
-
root: outcome.root,
|
|
899
|
-
contents: outcome.contents,
|
|
900
|
-
lessons: outcome.lessons,
|
|
901
|
-
reference: outcome.reference,
|
|
902
|
-
skipped: outcome.skipped,
|
|
903
|
-
unresolved: outcome.unresolved,
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
function logNav(outcome: NavGenerated): void {
|
|
908
|
-
logStep('Root')
|
|
909
|
-
logInfo(outcome.root)
|
|
910
|
-
logStep('Contents')
|
|
911
|
-
for (const path of outcome.contents) logInfo(path)
|
|
912
|
-
logStep('Lessons rewritten')
|
|
913
|
-
logInfo(String(outcome.lessons))
|
|
914
|
-
logStep('Reference pages rendered')
|
|
915
|
-
logInfo(String(outcome.reference))
|
|
916
|
-
|
|
917
|
-
if (outcome.skipped.length > 0) {
|
|
918
|
-
logStep('Refused, missing a chrome marker or the markdown renderer')
|
|
919
|
-
for (const skip of outcome.skipped) {
|
|
920
|
-
logWarn(`${skip.file}: no ${skip.missing}`)
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (outcome.unresolved.length > 0) {
|
|
925
|
-
logStep('Lesson references naming no lesson')
|
|
926
|
-
for (const reference of outcome.unresolved) {
|
|
927
|
-
logWarn(`${reference.file}: lesson ${reference.lesson}`)
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
async function runUp(
|
|
933
|
-
topic: string | undefined,
|
|
934
|
-
opts: UpCommandOptions,
|
|
935
|
-
): Promise<number> {
|
|
936
|
-
const emitJson = opts.json ?? false
|
|
937
|
-
const root = await rootFor(opts.root)
|
|
938
|
-
|
|
939
|
-
const port = parsePort(opts.port)
|
|
940
|
-
if (port === undefined) {
|
|
941
|
-
return reportRefusal(
|
|
942
|
-
'canon teach up',
|
|
943
|
-
badInput(`${opts.port} is not a port`),
|
|
944
|
-
emitJson,
|
|
945
|
-
root,
|
|
946
|
-
)
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
const nav = await generateNav(root, topic)
|
|
950
|
-
if (!nav.ok) return reportRefusal('canon teach up', nav, emitJson, root)
|
|
951
|
-
|
|
952
|
-
// The server resolves its folder against the cwd, and the teach folder sits
|
|
953
|
-
// at the main worktree root, so it takes the absolute path a linked
|
|
954
|
-
// worktree can still reach.
|
|
955
|
-
const dir = resolve(teachDir(root))
|
|
956
|
-
const contents = nav.contents[0]
|
|
957
|
-
const entry =
|
|
958
|
-
topic === undefined || contents === undefined
|
|
959
|
-
? 'index.html'
|
|
960
|
-
: relative(dir, resolve(root, contents)).split(sep).join('/')
|
|
961
|
-
|
|
962
|
-
const stream = refreshOnChange(root, dir, topic, emitJson)
|
|
963
|
-
const served = startServer(dir, {
|
|
964
|
-
port,
|
|
965
|
-
entry,
|
|
966
|
-
live: {
|
|
967
|
-
stream,
|
|
968
|
-
script: compileScript(reload, [LIVE_EVENTS_PATH]),
|
|
969
|
-
},
|
|
970
|
-
})
|
|
971
|
-
if (!served.ok) {
|
|
972
|
-
stream.close()
|
|
973
|
-
// A serve refusal carries a reason the teach refusals do not list, so it
|
|
974
|
-
// is written here in the teach record's shape rather than through one.
|
|
975
|
-
if (emitJson) {
|
|
976
|
-
process.stderr.write(`${served.detail}\n`)
|
|
977
|
-
process.stdout.write(
|
|
978
|
-
`${JSON.stringify({ ok: false, root, reason: served.reason, message: served.detail, detail: [] })}\n`,
|
|
979
|
-
)
|
|
980
|
-
return 1
|
|
981
|
-
}
|
|
982
|
-
intro('canon teach up')
|
|
983
|
-
logStep('Refused')
|
|
984
|
-
logError(served.detail)
|
|
985
|
-
outro()
|
|
986
|
-
return 1
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
if (emitJson) {
|
|
990
|
-
process.stdout.write(
|
|
991
|
-
`${JSON.stringify({ ...navFields(nav), served: served.root, ...serveFields(served) })}\n`,
|
|
992
|
-
)
|
|
993
|
-
} else {
|
|
994
|
-
intro('canon teach up')
|
|
995
|
-
logNav(nav)
|
|
996
|
-
logServing(served)
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
await waitForInterrupt(async () => {
|
|
1000
|
-
stream.close()
|
|
1001
|
-
await served.stop()
|
|
1002
|
-
})
|
|
1003
|
-
return 0
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
/**
|
|
1007
|
-
* An editor's swap or backup file, or anything under a dot folder. It changes
|
|
1008
|
-
* nothing a page shows, so it neither starts a pass nor counts as served.
|
|
1009
|
-
*/
|
|
1010
|
-
function isScratch(path: string): boolean {
|
|
1011
|
-
return path
|
|
1012
|
-
.split(/[\\/]/)
|
|
1013
|
-
.some((segment) => segment.startsWith('.') || segment.endsWith('~'))
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
/**
|
|
1017
|
-
* What the served folder holds, as each file's path, size, and mtime. A pass
|
|
1018
|
-
* whose `nav` wrote nothing leaves it unchanged, which is how the echo of a
|
|
1019
|
-
* pass's own writes settles without a second reload.
|
|
1020
|
-
*/
|
|
1021
|
-
function servedState(dir: string): string {
|
|
1022
|
-
return readdirSync(dir, { recursive: true, withFileTypes: true })
|
|
1023
|
-
.filter((entry) => entry.isFile())
|
|
1024
|
-
.map((entry) => relative(dir, join(entry.parentPath, entry.name)))
|
|
1025
|
-
.filter((path) => !isScratch(path))
|
|
1026
|
-
.sort()
|
|
1027
|
-
.map((path) => {
|
|
1028
|
-
const stat = statSync(join(dir, path), { throwIfNoEntry: false })
|
|
1029
|
-
return `${path}:${stat?.size}:${stat?.mtimeMs}`
|
|
1030
|
-
})
|
|
1031
|
-
.join('\n')
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
/**
|
|
1035
|
-
* Re-runs `nav` on each settled change under the teach folder and tells every
|
|
1036
|
-
* open page to reload once a pass changed what is served. Passes run one at a
|
|
1037
|
-
* time, since `nav` is not reentrant over the files it rewrites, and a change
|
|
1038
|
-
* arriving mid-pass is coalesced into exactly one more. A refused or failed
|
|
1039
|
-
* pass sends no reload, so a page never reloads onto a half-written state.
|
|
1040
|
-
*/
|
|
1041
|
-
function refreshOnChange(
|
|
1042
|
-
root: string,
|
|
1043
|
-
dir: string,
|
|
1044
|
-
topic: string | undefined,
|
|
1045
|
-
emitJson: boolean,
|
|
1046
|
-
): LiveStream<string> {
|
|
1047
|
-
let isRunning = false
|
|
1048
|
-
let isDirty = false
|
|
1049
|
-
let state = servedState(dir)
|
|
1050
|
-
|
|
1051
|
-
const report = (message: string, isWarning: boolean) => {
|
|
1052
|
-
if (emitJson) process.stderr.write(`${message}\n`)
|
|
1053
|
-
else if (isWarning) logWarn(message)
|
|
1054
|
-
else logInfo(message)
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
const pass = async (): Promise<void> => {
|
|
1058
|
-
const nav = await generateNav(root, topic)
|
|
1059
|
-
if (!nav.ok) {
|
|
1060
|
-
report(`Refresh refused: ${nav.message}`, true)
|
|
1061
|
-
return
|
|
1062
|
-
}
|
|
1063
|
-
const next = servedState(dir)
|
|
1064
|
-
if (next === state) return
|
|
1065
|
-
state = next
|
|
1066
|
-
stream.send({ reload: true })
|
|
1067
|
-
report(`Refreshed ${nav.lessons} lessons, reloading open pages`, false)
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
const schedule = async (): Promise<void> => {
|
|
1071
|
-
if (isRunning) {
|
|
1072
|
-
isDirty = true
|
|
1073
|
-
return
|
|
1074
|
-
}
|
|
1075
|
-
isRunning = true
|
|
1076
|
-
do {
|
|
1077
|
-
isDirty = false
|
|
1078
|
-
// A throw, such as the teach folder removed mid-pass, is reported and
|
|
1079
|
-
// the loop carries on, since a rejection left here would end the
|
|
1080
|
-
// process and a stuck flag would end every later refresh.
|
|
1081
|
-
try {
|
|
1082
|
-
await pass()
|
|
1083
|
-
} catch (error) {
|
|
1084
|
-
const code = (error as NodeJS.ErrnoException).code ?? String(error)
|
|
1085
|
-
report(`Refresh failed: ${code}`, true)
|
|
1086
|
-
}
|
|
1087
|
-
} while (isDirty)
|
|
1088
|
-
isRunning = false
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
const stream = new LiveStream<string>(dir, {
|
|
1092
|
-
map: (path) => (isScratch(path) ? undefined : path),
|
|
1093
|
-
onChange: () => void schedule(),
|
|
1094
|
-
})
|
|
1095
|
-
return stream
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
function reportRefusal(
|
|
1099
|
-
title: string,
|
|
1100
|
-
refused: TeachRefused,
|
|
1101
|
-
emitJson: boolean,
|
|
1102
|
-
root: string,
|
|
1103
|
-
): number {
|
|
1104
|
-
// The framed branch reaches stderr through logError, so the bare write is
|
|
1105
|
-
// what keeps the JSON mode from reporting the reason on stdout alone.
|
|
1106
|
-
if (emitJson) {
|
|
1107
|
-
process.stderr.write(`${refused.message}\n`)
|
|
1108
|
-
process.stdout.write(
|
|
1109
|
-
`${JSON.stringify({
|
|
1110
|
-
ok: false,
|
|
1111
|
-
root,
|
|
1112
|
-
reason: refused.reason,
|
|
1113
|
-
message: refused.message,
|
|
1114
|
-
detail: refused.detail,
|
|
1115
|
-
})}\n`,
|
|
1116
|
-
)
|
|
1117
|
-
return 1
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
intro(title)
|
|
1121
|
-
logStep('Refused')
|
|
1122
|
-
logError(refused.message)
|
|
1123
|
-
if (refused.detail.length > 0) pipeOutput(refused.detail.join('\n'))
|
|
1124
|
-
outro()
|
|
1125
|
-
|
|
1126
|
-
return 1
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
/**
|
|
1130
|
-
* One scheduled revisit, carrying both ladder dates rather than the rung alone.
|
|
1131
|
-
* A session copies the date the outcome names instead of computing one from a
|
|
1132
|
-
* ladder stated in prose.
|
|
1133
|
-
*/
|
|
1134
|
-
function describeRevisit(item: RevisitItem): string {
|
|
1135
|
-
const when = item.overdue ? `due ${item.date}` : item.date
|
|
1136
|
-
|
|
1137
|
-
return `${item.item}: ${when}, rung ${item.rung} (hit ${item.hit}, miss ${item.miss}), from ${item.record}`
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
function describe(workspace: WorkspaceSummary): string {
|
|
1141
|
-
return `${workspace.slug}: ${workspace.lessons} lesson(s), ${workspace.records} record(s), ${workspace.reference} reference page(s), ${workspace.terms} term(s)`
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
/**
|
|
1145
|
-
* `canon serve` resolves its folder against the caller's cwd, while the list
|
|
1146
|
-
* verb reads the teach folder at the main worktree root, so the line carries
|
|
1147
|
-
* the folder actually read. A literal `.canon/teach` serves an absent folder
|
|
1148
|
-
* from a linked worktree.
|
|
1149
|
-
*/
|
|
1150
|
-
function serveLine(root: string, entry: string): string {
|
|
1151
|
-
const dir = resolve(teachDir(root))
|
|
1152
|
-
const fromCwd = relative(process.cwd(), dir)
|
|
1153
|
-
const isOutsideCwd =
|
|
1154
|
-
fromCwd === '..' || fromCwd.startsWith(`..${sep}`) || isAbsolute(fromCwd)
|
|
1155
|
-
const printed = fromCwd === '' ? '.' : isOutsideCwd ? dir : fromCwd
|
|
1156
|
-
|
|
1157
|
-
return `canon serve ${printed} --entry ${entry}`
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
function reportList(
|
|
1161
|
-
outcome: ListOutcome,
|
|
1162
|
-
emitJson: boolean,
|
|
1163
|
-
root: string,
|
|
1164
|
-
): number {
|
|
1165
|
-
if (!outcome.ok)
|
|
1166
|
-
return reportRefusal('canon teach list', outcome, emitJson, root)
|
|
1167
|
-
|
|
1168
|
-
if (emitJson) {
|
|
1169
|
-
process.stdout.write(
|
|
1170
|
-
`${JSON.stringify({
|
|
1171
|
-
ok: true,
|
|
1172
|
-
root,
|
|
1173
|
-
workspaces: outcome.workspaces,
|
|
1174
|
-
next: outcome.next,
|
|
1175
|
-
})}\n`,
|
|
1176
|
-
)
|
|
1177
|
-
return 0
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
intro('canon teach list')
|
|
1181
|
-
logStep(outcome.workspaces.length > 0 ? 'Workspaces' : 'No workspaces')
|
|
1182
|
-
|
|
1183
|
-
for (const workspace of outcome.workspaces) logInfo(describe(workspace))
|
|
1184
|
-
|
|
1185
|
-
const incomplete = outcome.workspaces.filter(
|
|
1186
|
-
(workspace) => workspace.missing.length > 0,
|
|
1187
|
-
)
|
|
1188
|
-
|
|
1189
|
-
if (incomplete.length > 0) {
|
|
1190
|
-
logStep('Missing a required file')
|
|
1191
|
-
for (const workspace of incomplete) {
|
|
1192
|
-
logWarn(`${workspace.slug}: no ${workspace.missing.join(' and no ')}`)
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
logStep('Next ordinal')
|
|
1197
|
-
logInfo(outcome.next)
|
|
1198
|
-
|
|
1199
|
-
if (outcome.workspaces.length > 0) {
|
|
1200
|
-
logStep('Open')
|
|
1201
|
-
logInfo(serveLine(root, '<nn>-<topic>/index.html'))
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
outro()
|
|
1205
|
-
|
|
1206
|
-
return 0
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
function reportWorkspace(
|
|
1210
|
-
outcome: ReadOutcome,
|
|
1211
|
-
emitJson: boolean,
|
|
1212
|
-
root: string,
|
|
1213
|
-
): number {
|
|
1214
|
-
if (!outcome.ok)
|
|
1215
|
-
return reportRefusal('canon teach list', outcome, emitJson, root)
|
|
1216
|
-
|
|
1217
|
-
const workspace = outcome.workspace
|
|
1218
|
-
|
|
1219
|
-
if (emitJson) {
|
|
1220
|
-
process.stdout.write(`${JSON.stringify({ ok: true, root, workspace })}\n`)
|
|
1221
|
-
return 0
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
intro('canon teach list')
|
|
1225
|
-
logStep(workspace.slug)
|
|
1226
|
-
logInfo(`${workspace.title ?? 'no title'}, opened ${workspace.opened ?? '?'}`)
|
|
1227
|
-
logInfo(workspace.path)
|
|
1228
|
-
|
|
1229
|
-
for (const [label, files] of [
|
|
1230
|
-
['Lessons', workspace.lessonFiles],
|
|
1231
|
-
['Learning records', workspace.recordFiles],
|
|
1232
|
-
['Reference', workspace.referenceFiles],
|
|
1233
|
-
] as const) {
|
|
1234
|
-
logStep(files.length > 0 ? label : `${label} (none)`)
|
|
1235
|
-
for (const file of files) logInfo(file)
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
logStep(workspace.glossary.length > 0 ? 'Glossary' : 'Glossary (empty)')
|
|
1239
|
-
for (const entry of workspace.glossary) logInfo(entry)
|
|
1240
|
-
|
|
1241
|
-
logStep(
|
|
1242
|
-
workspace.success.length > 0 ? 'Exit criteria' : 'Exit criteria (none)',
|
|
1243
|
-
)
|
|
1244
|
-
for (const line of workspace.success) logInfo(line)
|
|
1245
|
-
|
|
1246
|
-
logStep(workspace.due.length > 0 ? 'Revisit' : 'Revisit (nothing scheduled)')
|
|
1247
|
-
for (const item of workspace.due) logInfo(describeRevisit(item))
|
|
1248
|
-
|
|
1249
|
-
if (workspace.missing.length > 0) {
|
|
1250
|
-
logStep('Missing a required file')
|
|
1251
|
-
logWarn(`no ${workspace.missing.join(' and no ')}`)
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
logStep('Open')
|
|
1255
|
-
logInfo(serveLine(root, `${workspace.slug}/index.html`))
|
|
1256
|
-
outro()
|
|
1257
|
-
|
|
1258
|
-
return 0
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
function reportOpen(
|
|
1262
|
-
outcome: OpenOutcome,
|
|
1263
|
-
emitJson: boolean,
|
|
1264
|
-
root: string,
|
|
1265
|
-
): number {
|
|
1266
|
-
if (!outcome.ok)
|
|
1267
|
-
return reportRefusal('canon teach open', outcome, emitJson, root)
|
|
1268
|
-
|
|
1269
|
-
if (emitJson) {
|
|
1270
|
-
process.stdout.write(
|
|
1271
|
-
`${JSON.stringify({
|
|
1272
|
-
ok: true,
|
|
1273
|
-
root,
|
|
1274
|
-
slug: outcome.slug,
|
|
1275
|
-
path: outcome.path,
|
|
1276
|
-
created: outcome.created,
|
|
1277
|
-
})}\n`,
|
|
1278
|
-
)
|
|
1279
|
-
return 0
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
intro('canon teach open')
|
|
1283
|
-
logStep('Opened')
|
|
1284
|
-
logInfo(outcome.slug)
|
|
1285
|
-
for (const file of outcome.created) logAdd(file)
|
|
1286
|
-
outro()
|
|
1287
|
-
|
|
1288
|
-
return 0
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
function reportResource(
|
|
1292
|
-
outcome: SourceOutcome,
|
|
1293
|
-
emitJson: boolean,
|
|
1294
|
-
root: string,
|
|
1295
|
-
): number {
|
|
1296
|
-
if (!outcome.ok) {
|
|
1297
|
-
return reportRefusal('canon teach resource', outcome, emitJson, root)
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
if (emitJson) {
|
|
1301
|
-
process.stdout.write(
|
|
1302
|
-
`${JSON.stringify({
|
|
1303
|
-
ok: true,
|
|
1304
|
-
root,
|
|
1305
|
-
slug: outcome.slug,
|
|
1306
|
-
path: relative(root, outcome.path),
|
|
1307
|
-
read: outcome.read,
|
|
1308
|
-
leads: outcome.leads,
|
|
1309
|
-
})}\n`,
|
|
1310
|
-
)
|
|
1311
|
-
return 0
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
intro('canon teach resource')
|
|
1315
|
-
logStep('Recorded')
|
|
1316
|
-
for (const source of outcome.read) logInfo(`read: ${source.title}`)
|
|
1317
|
-
for (const source of outcome.leads) logInfo(`lead: ${source.title}`)
|
|
1318
|
-
logAdd(relative(root, outcome.path))
|
|
1319
|
-
outro()
|
|
1320
|
-
|
|
1321
|
-
return 0
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
function reportGlossary(
|
|
1325
|
-
outcome: TermOutcome,
|
|
1326
|
-
emitJson: boolean,
|
|
1327
|
-
root: string,
|
|
1328
|
-
): number {
|
|
1329
|
-
if (!outcome.ok) {
|
|
1330
|
-
return reportRefusal('canon teach glossary', outcome, emitJson, root)
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
if (emitJson) {
|
|
1334
|
-
process.stdout.write(
|
|
1335
|
-
`${JSON.stringify({
|
|
1336
|
-
ok: true,
|
|
1337
|
-
root,
|
|
1338
|
-
slug: outcome.slug,
|
|
1339
|
-
path: relative(root, outcome.path),
|
|
1340
|
-
defined: outcome.defined,
|
|
1341
|
-
})}\n`,
|
|
1342
|
-
)
|
|
1343
|
-
return 0
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
intro('canon teach glossary')
|
|
1347
|
-
logStep('Defined')
|
|
1348
|
-
for (const term of outcome.defined) logInfo(term.term)
|
|
1349
|
-
logAdd(relative(root, outcome.path))
|
|
1350
|
-
outro()
|
|
1351
|
-
|
|
1352
|
-
return 0
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
function reportLesson(
|
|
1356
|
-
outcome: LessonOutcome,
|
|
1357
|
-
emitJson: boolean,
|
|
1358
|
-
root: string,
|
|
1359
|
-
): number {
|
|
1360
|
-
if (!outcome.ok) {
|
|
1361
|
-
return reportRefusal('canon teach lesson', outcome, emitJson, root)
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
if (emitJson) {
|
|
1365
|
-
process.stdout.write(
|
|
1366
|
-
`${JSON.stringify({
|
|
1367
|
-
ok: true,
|
|
1368
|
-
root,
|
|
1369
|
-
slug: outcome.slug,
|
|
1370
|
-
path: outcome.path,
|
|
1371
|
-
lesson: outcome.lesson,
|
|
1372
|
-
stylesheet: outcome.stylesheet,
|
|
1373
|
-
stylesheetExists: outcome.stylesheetExists,
|
|
1374
|
-
success: outcome.success,
|
|
1375
|
-
quiz: outcome.quiz,
|
|
1376
|
-
})}\n`,
|
|
1377
|
-
)
|
|
1378
|
-
return 0
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
intro('canon teach lesson')
|
|
1382
|
-
logStep('Lesson')
|
|
1383
|
-
logInfo(outcome.lesson)
|
|
1384
|
-
|
|
1385
|
-
logStep(outcome.stylesheetExists ? 'Stylesheet' : 'Stylesheet (to write)')
|
|
1386
|
-
logInfo(`${outcome.stylesheet} resolved for embedding`)
|
|
1387
|
-
|
|
1388
|
-
logStep(outcome.success.length > 0 ? 'Exit criteria' : 'Exit criteria (none)')
|
|
1389
|
-
for (const line of outcome.success) logInfo(line)
|
|
1390
|
-
|
|
1391
|
-
logStep('Option order')
|
|
1392
|
-
for (const question of outcome.quiz) {
|
|
1393
|
-
logInfo(
|
|
1394
|
-
`question ${question.question}: present ${question.order.join(', ')}, correct answer in position ${question.answer}`,
|
|
1395
|
-
)
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
outro()
|
|
1399
|
-
|
|
1400
|
-
return 0
|
|
1401
|
-
}
|