coursecode 0.1.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.
Files changed (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/bin/cli.js +331 -0
  5. package/framework/assets/logo-coursecode-black.svg +14 -0
  6. package/framework/assets/logo-coursecode-white.svg +14 -0
  7. package/framework/assets/logo-coursecode.svg +14 -0
  8. package/framework/css/01-base.css +160 -0
  9. package/framework/css/02-layout.css +499 -0
  10. package/framework/css/accessibility.css +834 -0
  11. package/framework/css/components/accordions.css +710 -0
  12. package/framework/css/components/assessments.css +520 -0
  13. package/framework/css/components/audio-player.css +570 -0
  14. package/framework/css/components/badges.css +80 -0
  15. package/framework/css/components/breadcrumbs.css +87 -0
  16. package/framework/css/components/buttons.css +707 -0
  17. package/framework/css/components/callouts.css +1280 -0
  18. package/framework/css/components/cards.css +475 -0
  19. package/framework/css/components/carousel.css +193 -0
  20. package/framework/css/components/checkbox-group.css +123 -0
  21. package/framework/css/components/checklist.css +203 -0
  22. package/framework/css/components/collapse.css +96 -0
  23. package/framework/css/components/comparison.css +33 -0
  24. package/framework/css/components/content-image.css +36 -0
  25. package/framework/css/components/document-gallery.css +425 -0
  26. package/framework/css/components/dropdown.css +115 -0
  27. package/framework/css/components/embed-frame.css +142 -0
  28. package/framework/css/components/engagement.css +412 -0
  29. package/framework/css/components/features.css +35 -0
  30. package/framework/css/components/flip-cards.css +253 -0
  31. package/framework/css/components/footer.css +353 -0
  32. package/framework/css/components/forms.css +294 -0
  33. package/framework/css/components/hero.css +216 -0
  34. package/framework/css/components/images.css +528 -0
  35. package/framework/css/components/interactive-timeline.css +274 -0
  36. package/framework/css/components/intro-cards.css +30 -0
  37. package/framework/css/components/lightbox.css +666 -0
  38. package/framework/css/components/loading.css +65 -0
  39. package/framework/css/components/modals.css +235 -0
  40. package/framework/css/components/notifications.css +107 -0
  41. package/framework/css/components/quote.css +150 -0
  42. package/framework/css/components/sidebar.css +684 -0
  43. package/framework/css/components/slide-header.css +52 -0
  44. package/framework/css/components/spinner.css +62 -0
  45. package/framework/css/components/stats.css +44 -0
  46. package/framework/css/components/steps.css +232 -0
  47. package/framework/css/components/tables.css +90 -0
  48. package/framework/css/components/tabs.css +347 -0
  49. package/framework/css/components/timeline.css +154 -0
  50. package/framework/css/components/toggle.css +95 -0
  51. package/framework/css/components/tooltip.css +226 -0
  52. package/framework/css/components/video-player.css +438 -0
  53. package/framework/css/design-tokens.css +707 -0
  54. package/framework/css/framework.css +86 -0
  55. package/framework/css/interactions/accessibility.css +75 -0
  56. package/framework/css/interactions/base.css +92 -0
  57. package/framework/css/interactions/drag-drop.css +295 -0
  58. package/framework/css/interactions/fill-in-the-blank.css +236 -0
  59. package/framework/css/interactions/hotspots.css +69 -0
  60. package/framework/css/interactions/index.css +45 -0
  61. package/framework/css/interactions/interactive-image.css +359 -0
  62. package/framework/css/interactions/likert.css +126 -0
  63. package/framework/css/interactions/matching.css +354 -0
  64. package/framework/css/interactions/numeric-input.css +78 -0
  65. package/framework/css/interactions/sequencing.css +378 -0
  66. package/framework/css/interactions/true-false.css +177 -0
  67. package/framework/css/layouts/article.css +258 -0
  68. package/framework/css/layouts/base.css +30 -0
  69. package/framework/css/layouts/canvas.css +38 -0
  70. package/framework/css/layouts/focused.css +236 -0
  71. package/framework/css/layouts/index.css +29 -0
  72. package/framework/css/layouts/presentation.css +191 -0
  73. package/framework/css/layouts/traditional.css +52 -0
  74. package/framework/css/responsive.css +439 -0
  75. package/framework/css/utilities/accessibility-utils.css +59 -0
  76. package/framework/css/utilities/animations.css +419 -0
  77. package/framework/css/utilities/borders.css +72 -0
  78. package/framework/css/utilities/colors.css +76 -0
  79. package/framework/css/utilities/container.css +46 -0
  80. package/framework/css/utilities/decorative.css +442 -0
  81. package/framework/css/utilities/display.css +257 -0
  82. package/framework/css/utilities/flexbox.css +80 -0
  83. package/framework/css/utilities/grid.css +69 -0
  84. package/framework/css/utilities/icons.css +534 -0
  85. package/framework/css/utilities/lists.css +190 -0
  86. package/framework/css/utilities/spacing.css +167 -0
  87. package/framework/css/utilities/tables.css +81 -0
  88. package/framework/css/utilities/typography.css +159 -0
  89. package/framework/css/utilities/visibility.css +117 -0
  90. package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
  91. package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
  92. package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
  93. package/framework/docs/DATA_MODEL.md +409 -0
  94. package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
  95. package/framework/docs/USER_GUIDE.md +583 -0
  96. package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
  97. package/framework/docs/examples/cloudflare-data-worker.js +102 -0
  98. package/framework/docs/examples/cloudflare-error-worker.js +228 -0
  99. package/framework/index.html +175 -0
  100. package/framework/js/app/AppActions.js +410 -0
  101. package/framework/js/app/AppState.js +225 -0
  102. package/framework/js/app/AppUI.js +616 -0
  103. package/framework/js/assessment/AssessmentActions.js +615 -0
  104. package/framework/js/assessment/AssessmentFactory.js +471 -0
  105. package/framework/js/assessment/AssessmentState.js +322 -0
  106. package/framework/js/assessment/AssessmentUI.js +451 -0
  107. package/framework/js/automation/api-engagement.js +196 -0
  108. package/framework/js/automation/api-interactions.js +167 -0
  109. package/framework/js/automation/api.js +242 -0
  110. package/framework/js/automation/index.js +41 -0
  111. package/framework/js/components/interactions/drag-drop.js +884 -0
  112. package/framework/js/components/interactions/fill-in.js +535 -0
  113. package/framework/js/components/interactions/hotspot.js +702 -0
  114. package/framework/js/components/interactions/interaction-base.js +511 -0
  115. package/framework/js/components/interactions/likert.js +301 -0
  116. package/framework/js/components/interactions/matching.js +699 -0
  117. package/framework/js/components/interactions/multiple-choice.js +377 -0
  118. package/framework/js/components/interactions/numeric.js +271 -0
  119. package/framework/js/components/interactions/sequencing.js +423 -0
  120. package/framework/js/components/interactions/true-false.js +241 -0
  121. package/framework/js/components/ui-components/accordion.js +442 -0
  122. package/framework/js/components/ui-components/alert.js +88 -0
  123. package/framework/js/components/ui-components/audio-player.js +1193 -0
  124. package/framework/js/components/ui-components/callout.js +121 -0
  125. package/framework/js/components/ui-components/carousel.js +145 -0
  126. package/framework/js/components/ui-components/checkbox-group.js +87 -0
  127. package/framework/js/components/ui-components/checklist.js +40 -0
  128. package/framework/js/components/ui-components/collapse.js +114 -0
  129. package/framework/js/components/ui-components/comparison.js +30 -0
  130. package/framework/js/components/ui-components/conditional-display.js +150 -0
  131. package/framework/js/components/ui-components/content-image.js +41 -0
  132. package/framework/js/components/ui-components/dropdown.js +262 -0
  133. package/framework/js/components/ui-components/embed-frame.js +274 -0
  134. package/framework/js/components/ui-components/features.js +33 -0
  135. package/framework/js/components/ui-components/flip-card.js +230 -0
  136. package/framework/js/components/ui-components/form-validator.js +76 -0
  137. package/framework/js/components/ui-components/hero.js +49 -0
  138. package/framework/js/components/ui-components/index.js +12 -0
  139. package/framework/js/components/ui-components/interactive-image.js +235 -0
  140. package/framework/js/components/ui-components/interactive-timeline.js +285 -0
  141. package/framework/js/components/ui-components/intro-cards.js +35 -0
  142. package/framework/js/components/ui-components/lightbox.js +652 -0
  143. package/framework/js/components/ui-components/modal.js +386 -0
  144. package/framework/js/components/ui-components/notifications.js +145 -0
  145. package/framework/js/components/ui-components/progress.js +88 -0
  146. package/framework/js/components/ui-components/quote.js +41 -0
  147. package/framework/js/components/ui-components/stats.js +33 -0
  148. package/framework/js/components/ui-components/steps.js +41 -0
  149. package/framework/js/components/ui-components/tabs.js +255 -0
  150. package/framework/js/components/ui-components/timeline.js +42 -0
  151. package/framework/js/components/ui-components/toggle-group.js +73 -0
  152. package/framework/js/components/ui-components/tooltip.js +458 -0
  153. package/framework/js/components/ui-components/value-display.js +133 -0
  154. package/framework/js/components/ui-components/video-player.js +686 -0
  155. package/framework/js/core/component-catalog.js +121 -0
  156. package/framework/js/core/event-bus.js +178 -0
  157. package/framework/js/core/interaction-catalog.js +149 -0
  158. package/framework/js/dev/runtime-linter.js +1725 -0
  159. package/framework/js/drivers/cmi5-driver.js +768 -0
  160. package/framework/js/drivers/driver-factory.js +77 -0
  161. package/framework/js/drivers/driver-interface.js +110 -0
  162. package/framework/js/drivers/http-driver-base.js +241 -0
  163. package/framework/js/drivers/lti-driver.js +508 -0
  164. package/framework/js/drivers/proxy-driver.js +444 -0
  165. package/framework/js/drivers/scorm-12-driver.js +560 -0
  166. package/framework/js/drivers/scorm-2004-driver.js +775 -0
  167. package/framework/js/drivers/scorm-driver-base.js +112 -0
  168. package/framework/js/engagement/engagement-manager.js +404 -0
  169. package/framework/js/engagement/engagement-progress.js +191 -0
  170. package/framework/js/engagement/engagement-trackers.js +215 -0
  171. package/framework/js/engagement/requirement-strategies.js +268 -0
  172. package/framework/js/main.js +727 -0
  173. package/framework/js/managers/accessibility-manager.js +499 -0
  174. package/framework/js/managers/assessment-manager.js +230 -0
  175. package/framework/js/managers/audio-manager.js +944 -0
  176. package/framework/js/managers/comment-manager.js +88 -0
  177. package/framework/js/managers/flag-manager.js +86 -0
  178. package/framework/js/managers/interaction-manager.js +254 -0
  179. package/framework/js/managers/interaction-registry.js +96 -0
  180. package/framework/js/managers/objective-manager.js +423 -0
  181. package/framework/js/managers/score-manager.js +441 -0
  182. package/framework/js/managers/video-manager.js +536 -0
  183. package/framework/js/navigation/Breadcrumbs.js +234 -0
  184. package/framework/js/navigation/NavigationActions.js +1132 -0
  185. package/framework/js/navigation/NavigationState.js +276 -0
  186. package/framework/js/navigation/NavigationUI.js +574 -0
  187. package/framework/js/navigation/document-gallery.js +357 -0
  188. package/framework/js/navigation/navigation-helpers.js +175 -0
  189. package/framework/js/navigation/navigation-validators.js +174 -0
  190. package/framework/js/state/index.js +8 -0
  191. package/framework/js/state/lms-connection.js +482 -0
  192. package/framework/js/state/lms-error-utils.js +58 -0
  193. package/framework/js/state/state-commits.js +200 -0
  194. package/framework/js/state/state-domains.js +86 -0
  195. package/framework/js/state/state-manager.js +502 -0
  196. package/framework/js/state/state-validation.js +311 -0
  197. package/framework/js/state/transaction-log.js +41 -0
  198. package/framework/js/state/xapi-statement-service.js +325 -0
  199. package/framework/js/utilities/access-control.js +99 -0
  200. package/framework/js/utilities/breakpoint-manager.js +315 -0
  201. package/framework/js/utilities/canvas-slide.js +35 -0
  202. package/framework/js/utilities/conditional-display.js +388 -0
  203. package/framework/js/utilities/course-channel.js +214 -0
  204. package/framework/js/utilities/course-helpers.js +420 -0
  205. package/framework/js/utilities/data-reporter.js +273 -0
  206. package/framework/js/utilities/error-reporter.js +313 -0
  207. package/framework/js/utilities/hotspot-helper.js +341 -0
  208. package/framework/js/utilities/icons.js +348 -0
  209. package/framework/js/utilities/logger.js +92 -0
  210. package/framework/js/utilities/markdown-renderer.js +45 -0
  211. package/framework/js/utilities/scroll-tracker.js +68 -0
  212. package/framework/js/utilities/ui-initializer.js +146 -0
  213. package/framework/js/utilities/utilities.js +293 -0
  214. package/framework/js/utilities/view-manager.js +227 -0
  215. package/framework/js/validation/html-validators.js +422 -0
  216. package/framework/js/validation/scorm-validators.js +438 -0
  217. package/framework/js/vendor/pipwerks.js +931 -0
  218. package/framework/scripts/generate-narration.js +629 -0
  219. package/framework/scripts/tts-providers/azure-provider.js +178 -0
  220. package/framework/scripts/tts-providers/base-provider.js +81 -0
  221. package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
  222. package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
  223. package/framework/scripts/tts-providers/google-provider.js +272 -0
  224. package/framework/scripts/tts-providers/index.js +158 -0
  225. package/framework/scripts/tts-providers/openai-provider.js +143 -0
  226. package/framework/version.json +63 -0
  227. package/lib/authoring-api.js +919 -0
  228. package/lib/build-linter.js +450 -0
  229. package/lib/build-packaging.js +186 -0
  230. package/lib/build.js +88 -0
  231. package/lib/cloud.js +691 -0
  232. package/lib/convert.js +341 -0
  233. package/lib/course-parser.js +936 -0
  234. package/lib/course-writer.js +258 -0
  235. package/lib/create.js +248 -0
  236. package/lib/css-index.js +237 -0
  237. package/lib/dev.js +51 -0
  238. package/lib/export-content.js +1246 -0
  239. package/lib/headless-browser.js +413 -0
  240. package/lib/import.js +377 -0
  241. package/lib/index.js +80 -0
  242. package/lib/info.js +79 -0
  243. package/lib/interaction-formatters.js +568 -0
  244. package/lib/manifest/cmi5-manifest.js +63 -0
  245. package/lib/manifest/lti-tool-config.js +53 -0
  246. package/lib/manifest/manifest-factory.js +99 -0
  247. package/lib/manifest/scorm-12-manifest.js +61 -0
  248. package/lib/manifest/scorm-2004-manifest.js +94 -0
  249. package/lib/manifest/scorm-proxy-manifest.js +104 -0
  250. package/lib/manifest-parser.js +96 -0
  251. package/lib/mcp-prompts.js +753 -0
  252. package/lib/mcp-server.js +316 -0
  253. package/lib/narration.js +53 -0
  254. package/lib/pdf-structure.js +142 -0
  255. package/lib/preview-export.js +231 -0
  256. package/lib/preview-routes-api.js +662 -0
  257. package/lib/preview-routes-editing.js +159 -0
  258. package/lib/preview-routes-lms.js +230 -0
  259. package/lib/preview-server.js +564 -0
  260. package/lib/project-utils.js +269 -0
  261. package/lib/proxy-templates/proxy.html +68 -0
  262. package/lib/proxy-templates/scorm-bridge.js +112 -0
  263. package/lib/scaffold.js +193 -0
  264. package/lib/schema-extractor.js +361 -0
  265. package/lib/slide-source-editor.js +586 -0
  266. package/lib/stub-player/app-viewer.js +195 -0
  267. package/lib/stub-player/app.js +370 -0
  268. package/lib/stub-player/catalog-panel.js +312 -0
  269. package/lib/stub-player/config-panel.js +1303 -0
  270. package/lib/stub-player/content-generator.js +586 -0
  271. package/lib/stub-player/content-viewer.js +173 -0
  272. package/lib/stub-player/debug-panel.js +420 -0
  273. package/lib/stub-player/edit-mode.js +922 -0
  274. package/lib/stub-player/edit-utils.js +400 -0
  275. package/lib/stub-player/header-bar.js +354 -0
  276. package/lib/stub-player/interaction-editor.js +210 -0
  277. package/lib/stub-player/interactions-panel.js +565 -0
  278. package/lib/stub-player/lms-api.js +1094 -0
  279. package/lib/stub-player/login-screen.js +74 -0
  280. package/lib/stub-player/outline-mode.js +689 -0
  281. package/lib/stub-player/styles/_assessments-panel.css +245 -0
  282. package/lib/stub-player/styles/_base.css +89 -0
  283. package/lib/stub-player/styles/_catalog-icons.css +96 -0
  284. package/lib/stub-player/styles/_catalog-panel.css +291 -0
  285. package/lib/stub-player/styles/_config-panel.css +636 -0
  286. package/lib/stub-player/styles/_content-viewer.css +834 -0
  287. package/lib/stub-player/styles/_debug-panel.css +576 -0
  288. package/lib/stub-player/styles/_edit-mode.css +128 -0
  289. package/lib/stub-player/styles/_header-bar.css +343 -0
  290. package/lib/stub-player/styles/_interaction-editor.css +140 -0
  291. package/lib/stub-player/styles/_interactions-panel.css +1038 -0
  292. package/lib/stub-player/styles/_login-screen.css +102 -0
  293. package/lib/stub-player/styles/_outline-mode.css +752 -0
  294. package/lib/stub-player/styles.css +15 -0
  295. package/lib/stub-player.js +160 -0
  296. package/lib/test-data-reporting.js +176 -0
  297. package/lib/test-error-reporting.js +146 -0
  298. package/lib/token.js +86 -0
  299. package/lib/upgrade.js +257 -0
  300. package/lib/validation-rules.js +517 -0
  301. package/lib/vite-plugin-content-discovery.js +296 -0
  302. package/package.json +108 -0
  303. package/schemas/XMLSchema.dtd +402 -0
  304. package/schemas/adlcp_v1p3.xsd +111 -0
  305. package/schemas/adlnav_v1p3.xsd +61 -0
  306. package/schemas/adlseq_v1p3.xsd +93 -0
  307. package/schemas/common/anyElement.xsd +27 -0
  308. package/schemas/common/dataTypes.xsd +138 -0
  309. package/schemas/common/elementNames.xsd +767 -0
  310. package/schemas/common/elementTypes.xsd +786 -0
  311. package/schemas/common/rootElement.xsd +31 -0
  312. package/schemas/common/vocabTypes.xsd +345 -0
  313. package/schemas/common/vocabValues.xsd +257 -0
  314. package/schemas/datatypes.dtd +203 -0
  315. package/schemas/ims_xml.xsd +35 -0
  316. package/schemas/imscp_v1p1.xsd +368 -0
  317. package/schemas/imsss_v1p0.xsd +67 -0
  318. package/schemas/imsss_v1p0auxresource.xsd +19 -0
  319. package/schemas/imsss_v1p0control.xsd +20 -0
  320. package/schemas/imsss_v1p0delivery.xsd +17 -0
  321. package/schemas/imsss_v1p0limit.xsd +47 -0
  322. package/schemas/imsss_v1p0objective.xsd +67 -0
  323. package/schemas/imsss_v1p0random.xsd +16 -0
  324. package/schemas/imsss_v1p0rollup.xsd +46 -0
  325. package/schemas/imsss_v1p0seqrule.xsd +108 -0
  326. package/schemas/imsss_v1p0util.xsd +94 -0
  327. package/schemas/license.txt +17 -0
  328. package/schemas/lom.xsd +102 -0
  329. package/schemas/lomCustom.xsd +62 -0
  330. package/schemas/lomLoose.xsd +62 -0
  331. package/schemas/lomStrict.xsd +62 -0
  332. package/schemas/xml.xsd +81 -0
  333. package/template/.env.example +92 -0
  334. package/template/course/assets/audio/example-intro.mp3 +0 -0
  335. package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
  336. package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
  337. package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
  338. package/template/course/assets/docs/example_md_1.md +39 -0
  339. package/template/course/assets/docs/example_md_2.md +41 -0
  340. package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
  341. package/template/course/assets/docs/example_pdf_2.pdf +0 -0
  342. package/template/course/assets/images/course-architecture.svg +36 -0
  343. package/template/course/assets/images/logo.svg +14 -0
  344. package/template/course/assets/widgets/counter-demo.html +190 -0
  345. package/template/course/assets/widgets/gravity-painter.html +384 -0
  346. package/template/course/course-config.js +539 -0
  347. package/template/course/icons.js +19 -0
  348. package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
  349. package/template/course/slides/example-course-structure.js +138 -0
  350. package/template/course/slides/example-final-exam.js +144 -0
  351. package/template/course/slides/example-finishing.js +127 -0
  352. package/template/course/slides/example-interactions-showcase.js +615 -0
  353. package/template/course/slides/example-preview-tour.js +129 -0
  354. package/template/course/slides/example-remedial.js +143 -0
  355. package/template/course/slides/example-summary.js +103 -0
  356. package/template/course/slides/example-ui-showcase.js +1805 -0
  357. package/template/course/slides/example-welcome.js +123 -0
  358. package/template/course/slides/example-workflow.js +140 -0
  359. package/template/course/theme.css +165 -0
  360. package/template/eslint.config.js +47 -0
  361. package/template/package.json +28 -0
  362. package/template/vite.config.js +339 -0
@@ -0,0 +1,919 @@
1
+ /**
2
+ * Authoring API for CourseCode
3
+ *
4
+ * Provides file-system based utilities for AI-assisted course authoring.
5
+ * These methods work without a running preview server.
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { pathToFileURL } from 'url';
11
+ import { spawn } from 'child_process';
12
+ import postcss from 'postcss';
13
+ import {
14
+ getAllComponentSchemas,
15
+ getAllComponentMetadata,
16
+ getRegisteredComponentTypes,
17
+ getAllSchemas,
18
+ getAllMetadata,
19
+ getRegisteredTypes,
20
+ getAllIcons
21
+ } from './schema-extractor.js';
22
+ import { fileURLToPath } from 'url';
23
+
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+ const __packageRoot = path.dirname(__dirname); // lib/ -> repo root
26
+
27
+ /**
28
+ * Get the course root directory (where course/ folder is).
29
+ * Tries process.cwd() first (normal for course projects),
30
+ * then falls back to package root (framework repo / global install).
31
+ */
32
+ function getCourseRoot() {
33
+ // Check cwd first (course projects run from their own root)
34
+ if (fs.existsSync(path.join(process.cwd(), 'course'))) {
35
+ return process.cwd();
36
+ }
37
+ if (fs.existsSync(path.join(process.cwd(), 'template', 'course'))) {
38
+ return path.join(process.cwd(), 'template');
39
+ }
40
+ // Fallback: resolve from package root (framework repo launched by IDE)
41
+ if (fs.existsSync(path.join(__packageRoot, 'course'))) {
42
+ return __packageRoot;
43
+ }
44
+ if (fs.existsSync(path.join(__packageRoot, 'template', 'course'))) {
45
+ return path.join(__packageRoot, 'template');
46
+ }
47
+ throw new Error('No course directory found. Run from a CourseCode project root.');
48
+ }
49
+
50
+ /**
51
+ * Get the framework root directory.
52
+ * Tries process.cwd() first, then falls back to package root.
53
+ */
54
+ function getFrameworkRoot() {
55
+ const cwd = process.cwd();
56
+ if (fs.existsSync(path.join(cwd, 'framework'))) {
57
+ return cwd;
58
+ }
59
+ const parent = path.dirname(cwd);
60
+ if (fs.existsSync(path.join(parent, 'framework'))) {
61
+ return parent;
62
+ }
63
+ // Fallback: package root
64
+ if (fs.existsSync(path.join(__packageRoot, 'framework'))) {
65
+ return __packageRoot;
66
+ }
67
+ throw new Error('Framework directory not found.');
68
+ }
69
+
70
+ /**
71
+ * List files in a directory (non-recursive)
72
+ */
73
+ function listFiles(dir) {
74
+ if (!fs.existsSync(dir)) return [];
75
+ return fs.readdirSync(dir).filter(f => {
76
+ const stat = fs.statSync(path.join(dir, f));
77
+ return stat.isFile();
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Get status of reference files and their conversions
83
+ */
84
+ export function getRefsStatus() {
85
+ const courseRoot = getCourseRoot();
86
+ const refsDir = path.join(courseRoot, 'course', 'references');
87
+ const mdDir = path.join(refsDir, 'converted');
88
+
89
+ const rawFiles = listFiles(refsDir).filter(f =>
90
+ !f.startsWith('.') &&
91
+ ['.pdf', '.docx', '.doc', '.pptx', '.ppt', '.md'].some(ext => f.toLowerCase().endsWith(ext))
92
+ );
93
+
94
+ const convertedFiles = listFiles(mdDir).filter(f => f.endsWith('.md'));
95
+
96
+ // Find files that need conversion (have raw but no corresponding md)
97
+ const convertedBases = new Set(convertedFiles.map(f => path.parse(f).name.toLowerCase()));
98
+ const needsConversion = rawFiles.filter(f => {
99
+ const base = path.parse(f).name.toLowerCase();
100
+ return !convertedBases.has(base);
101
+ });
102
+
103
+ return {
104
+ refsDirectory: refsDir,
105
+ convertedDirectory: mdDir,
106
+ raw: rawFiles,
107
+ converted: convertedFiles,
108
+ needsConversion,
109
+ convertCommand: 'coursecode convert',
110
+ isEmpty: rawFiles.length === 0 && convertedFiles.length === 0,
111
+ message: rawFiles.length === 0
112
+ ? 'No reference files found. Add PDFs, Word docs, PowerPoints, or Markdown files to course/references/'
113
+ : needsConversion.length > 0
114
+ ? `${needsConversion.length} file(s) need conversion. Run: coursecode convert`
115
+ : `All ${rawFiles.length} reference file(s) converted.`
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Get context for outline creation stage
121
+ */
122
+ export function getOutlineContext() {
123
+ const courseRoot = getCourseRoot();
124
+ const frameworkRoot = getFrameworkRoot();
125
+ const mdDir = path.join(courseRoot, 'course', 'references', 'converted');
126
+
127
+ const referenceMds = listFiles(mdDir).filter(f => f.endsWith('.md'));
128
+ const outlinePath = path.join(courseRoot, 'course', 'COURSE_OUTLINE.md');
129
+
130
+ return {
131
+ outlineGuide: path.join(frameworkRoot, 'framework', 'docs', 'COURSE_OUTLINE_GUIDE.md'),
132
+ outlineTemplate: path.join(frameworkRoot, 'framework', 'docs', 'COURSE_OUTLINE_TEMPLATE.md'),
133
+ referenceMds: referenceMds.map(f => path.join(mdDir, f)),
134
+ existingOutline: fs.existsSync(outlinePath) ? outlinePath : null,
135
+ outlineLocation: outlinePath,
136
+ message: fs.existsSync(outlinePath)
137
+ ? 'Existing outline found. Review and iterate, or start fresh.'
138
+ : 'No outline yet. Use the template and guide to create one at course/COURSE_OUTLINE.md'
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Get context for course building stage
144
+ */
145
+ export function getAuthoringContext() {
146
+ const courseRoot = getCourseRoot();
147
+ const frameworkRoot = getFrameworkRoot();
148
+ const mdDir = path.join(courseRoot, 'course', 'references', 'converted');
149
+ const slidesDir = path.join(courseRoot, 'course', 'slides');
150
+
151
+ const referenceMds = listFiles(mdDir).filter(f => f.endsWith('.md'));
152
+ const existingSlides = listFiles(slidesDir).filter(f => f.endsWith('.js'));
153
+ const outlinePath = path.join(courseRoot, 'course', 'COURSE_OUTLINE.md');
154
+
155
+ return {
156
+ authoringGuide: path.join(frameworkRoot, 'framework', 'docs', 'COURSE_AUTHORING_GUIDE.md'),
157
+ courseOutline: fs.existsSync(outlinePath) ? outlinePath : null,
158
+ referenceMds: referenceMds.map(f => path.join(mdDir, f)),
159
+ existingSlides: existingSlides.map(f => path.join(slidesDir, f)),
160
+ courseConfig: path.join(courseRoot, 'course', 'course-config.js'),
161
+ slidesDirectory: slidesDir,
162
+ message: !fs.existsSync(outlinePath)
163
+ ? 'Warning: No outline found. Create one first with getOutlineContext().'
164
+ : `Ready to build. ${existingSlides.length} existing slide(s) in course/slides/`
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Dynamic CSS catalog — extracts structured class data from real CSS files via PostCSS.
170
+ *
171
+ * Without filterCategory: returns compact categorized index (class name → short description).
172
+ * With filterCategory: returns full detail for that category (all declarations).
173
+ *
174
+ * Category names are derived from file paths relative to framework/css/:
175
+ * utilities/borders.css → "utilities/borders"
176
+ * 02-layout.css → "layout"
177
+ * components/hero.css → "components/hero"
178
+ */
179
+
180
+ // Module-level cache — parsed once per process
181
+ let _cssCatalogCache = null;
182
+
183
+ function buildCssCatalog() {
184
+ if (_cssCatalogCache) return _cssCatalogCache;
185
+
186
+ const frameworkRoot = getFrameworkRoot();
187
+ const cssDir = path.join(frameworkRoot, 'framework', 'css');
188
+ const cssFiles = [];
189
+
190
+ if (fs.existsSync(cssDir)) {
191
+ collectCssFiles(cssDir, cssFiles);
192
+ }
193
+
194
+ // Also include course CSS
195
+ try {
196
+ const courseRoot = getCourseRoot();
197
+ const courseDir = path.join(courseRoot, 'course');
198
+ const themeFile = path.join(courseDir, 'theme.css');
199
+ if (fs.existsSync(themeFile)) cssFiles.push(themeFile);
200
+ const customDir = path.join(courseDir, 'components');
201
+ if (fs.existsSync(customDir)) collectCssFiles(customDir, cssFiles);
202
+ } catch {
203
+ // No course directory — framework-only mode
204
+ }
205
+
206
+ const categories = {};
207
+ let totalClasses = 0;
208
+
209
+ for (const file of cssFiles) {
210
+ const relPath = path.relative(cssDir, file);
211
+ // Derive category: "utilities/borders.css" → "utilities/borders", "02-layout.css" → "layout"
212
+ const category = relPath
213
+ .replace(/\.css$/, '')
214
+ .replace(/^\d+-/, ''); // Strip leading number prefixes like "01-", "02-"
215
+
216
+ try {
217
+ const source = fs.readFileSync(file, 'utf-8');
218
+ const root = postcss.parse(source, { from: file });
219
+ const classes = {};
220
+
221
+ extractClassCatalog(root, classes);
222
+
223
+ if (Object.keys(classes).length > 0) {
224
+ categories[category] = {
225
+ file: relPath,
226
+ classes
227
+ };
228
+ totalClasses += Object.keys(classes).length;
229
+ }
230
+ } catch {
231
+ // Skip unparseable CSS
232
+ }
233
+ }
234
+
235
+ _cssCatalogCache = { categories, totalClasses };
236
+ return _cssCatalogCache;
237
+ }
238
+
239
+ /**
240
+ * Extract class names with abbreviated declarations from PostCSS nodes.
241
+ * Walks the AST and builds { className: "shortDescription" } entries.
242
+ */
243
+ function extractClassCatalog(node, classes) {
244
+ if (node.type === 'rule' && node.selector) {
245
+ // Only process simple class selectors (e.g., .foo, .foo-bar)
246
+ // Skip compound selectors, pseudo-classes, nested selectors
247
+ const selectorParts = node.selector.split(',').map(s => s.trim());
248
+
249
+ for (const part of selectorParts) {
250
+ // Match standalone class selectors like ".foo" or ".foo-bar"
251
+ // Skip selectors with spaces, combinators, pseudo-classes, attribute selectors
252
+ const simpleClassMatch = part.match(/^\.([a-zA-Z][\w-]*)$/);
253
+ if (!simpleClassMatch) continue;
254
+
255
+ const className = simpleClassMatch[1];
256
+ if (classes[className]) continue; // Already captured
257
+
258
+ // Build short description from declarations
259
+ const decls = [];
260
+ node.walk(child => {
261
+ if (child.type === 'decl') {
262
+ decls.push(`${child.prop}: ${child.value}`);
263
+ }
264
+ });
265
+
266
+ // Abbreviate: show first 2 declarations, truncate long values
267
+ const shortDecls = decls.slice(0, 2).map(d =>
268
+ d.length > 60 ? d.slice(0, 57) + '...' : d
269
+ );
270
+ if (decls.length > 2) shortDecls.push(`+${decls.length - 2} more`);
271
+
272
+ classes[className] = shortDecls.join('; ');
273
+ }
274
+ }
275
+
276
+ // Recurse into @media, @supports, etc.
277
+ if (node.nodes) {
278
+ for (const child of node.nodes) {
279
+ extractClassCatalog(child, classes);
280
+ }
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Get CSS catalog — compact list or full detail for one category.
286
+ * @param {string} [filterCategory] - If provided, return detail for this category only
287
+ */
288
+ export function getCssCatalog(filterCategory) {
289
+ const catalog = buildCssCatalog();
290
+
291
+ if (filterCategory) {
292
+ const cat = catalog.categories[filterCategory];
293
+ if (!cat) {
294
+ return {
295
+ error: `Unknown category: '${filterCategory}'`,
296
+ available: Object.keys(catalog.categories).sort()
297
+ };
298
+ }
299
+ return { category: filterCategory, ...cat };
300
+ }
301
+
302
+ return {
303
+ categories: catalog.categories,
304
+ totalClasses: catalog.totalClasses,
305
+ categoryCount: Object.keys(catalog.categories).length,
306
+ message: `${catalog.totalClasses} CSS classes across ${Object.keys(catalog.categories).length} categories. Pass 'category' for full detail.`
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Get export options and commands
312
+ */
313
+ export function getExportOptions() {
314
+ return {
315
+ formats: ['cmi5', 'scorm2004', 'scorm1.2', 'lti'],
316
+ defaultFormat: 'cmi5',
317
+ commands: {
318
+ cmi5: 'coursecode build --format cmi5',
319
+ scorm2004: 'coursecode build --format scorm2004',
320
+ 'scorm1.2': 'coursecode build --format scorm1.2',
321
+ lti: 'coursecode build --format lti',
322
+ preview: 'coursecode build --preview',
323
+ previewWithPassword: 'coursecode build --preview --password "your-password"'
324
+ },
325
+ outputDir: 'dist/',
326
+ message: 'Use cmi5 (default) for modern LMS, scorm1.2 for legacy, lti for LTI 1.3 platforms.'
327
+ };
328
+ }
329
+
330
+ /**
331
+ * Get preview server status (checks if running)
332
+ */
333
+ export async function getPreviewStatus(port = 4173) {
334
+ const url = `http://localhost:${port}`;
335
+
336
+ try {
337
+ const controller = new AbortController();
338
+ const timeout = setTimeout(() => controller.abort(), 2000);
339
+
340
+ const response = await fetch(url, {
341
+ signal: controller.signal,
342
+ method: 'HEAD'
343
+ });
344
+
345
+ clearTimeout(timeout);
346
+
347
+ return {
348
+ running: response.ok,
349
+ url,
350
+ port,
351
+ startCommand: 'coursecode preview',
352
+ message: response.ok
353
+ ? `Preview running at ${url}`
354
+ : 'Preview server not responding.'
355
+ };
356
+ } catch (_error) {
357
+ return {
358
+ running: false,
359
+ url,
360
+ port,
361
+ startCommand: 'coursecode preview',
362
+ message: 'Preview server not running. Start with: coursecode preview'
363
+ };
364
+ }
365
+ }
366
+
367
+
368
+ // =============================================================================
369
+ // CATALOG & VALIDATION TOOLS (MCP-facing)
370
+ // =============================================================================
371
+
372
+ /**
373
+ * Get UI components — compact list or full detail for one type.
374
+ * Uses schema-extractor.js — works at build time, no preview needed.
375
+ * @param {string} [filterType] - If provided, return full detail for this type only
376
+ */
377
+ export function getComponentCatalog(filterType) {
378
+ const schemas = getAllComponentSchemas();
379
+ const metadata = getAllComponentMetadata();
380
+ const registeredTypes = getRegisteredComponentTypes();
381
+
382
+ // Full detail for a specific type
383
+ if (filterType) {
384
+ const type = filterType;
385
+ if (!registeredTypes.includes(type)) {
386
+ return { error: `Unknown component type: '${type}'`, available: registeredTypes };
387
+ }
388
+ const schema = schemas[type] || {};
389
+ const meta = metadata[type] || {};
390
+
391
+ let usage = `<div data-component="${type}">...</div>`;
392
+ if (schema.structure?.children) {
393
+ const childExamples = Object.entries(schema.structure.children)
394
+ .map(([name, def]) => ` ${def.selector ? `<div class="${name}">...</div>` : `<!-- ${name} -->`}`)
395
+ .join('\n');
396
+ usage = `<div data-component="${type}">\n${childExamples}\n</div>`;
397
+ }
398
+
399
+ return { type, schema, metadata: meta, usage, example: schema.example || null, engagementTracking: meta.engagementTracking || null };
400
+ }
401
+
402
+ // Compact list — names, descriptions, and engagement tracking only
403
+ const components = {};
404
+ for (const type of registeredTypes) {
405
+ const meta = metadata[type] || {};
406
+ const sch = schemas[type] || {};
407
+ components[type] = {
408
+ description: sch.description || null,
409
+ engagementTracking: meta.engagementTracking || null
410
+ };
411
+ }
412
+
413
+ return {
414
+ components,
415
+ count: Object.keys(components).length,
416
+ message: `${Object.keys(components).length} registered UI components. Pass 'type' for full schema and usage.`
417
+ };
418
+ }
419
+
420
+ /**
421
+ * Get interaction types — compact list or full detail for one type.
422
+ * Uses schema-extractor.js — works at build time, no preview needed.
423
+ * @param {string} [filterType] - If provided, return full detail for this type only
424
+ */
425
+ export function getInteractionCatalog(filterType) {
426
+ const schemas = getAllSchemas();
427
+ const metadata = getAllMetadata();
428
+ const registeredTypes = getRegisteredTypes();
429
+
430
+ // Full detail for a specific type
431
+ if (filterType) {
432
+ const type = filterType;
433
+ if (!registeredTypes.includes(type)) {
434
+ return { error: `Unknown interaction type: '${type}'`, available: registeredTypes };
435
+ }
436
+ const schema = schemas[type] || {};
437
+ return {
438
+ type,
439
+ schema,
440
+ metadata: metadata[type] || null,
441
+ example: schema.example || null
442
+ };
443
+ }
444
+
445
+ // Compact list — names and descriptions only
446
+ const interactions = {};
447
+ for (const type of registeredTypes) {
448
+ const sch = schemas[type] || {};
449
+ interactions[type] = {
450
+ description: sch.description || null
451
+ };
452
+ }
453
+
454
+ return {
455
+ interactions,
456
+ count: Object.keys(interactions).length,
457
+ message: `${Object.keys(interactions).length} registered interaction types. Pass 'type' for full schema.`
458
+ };
459
+ }
460
+
461
+ /**
462
+ * Get icon catalog — compact list or detail for one icon.
463
+ * Uses schema-extractor.js — works at build time, no preview needed.
464
+ * @param {string} [filterName] - If provided, return detail for this icon name
465
+ */
466
+ export function getIconCatalog(filterName) {
467
+ const allIcons = getAllIcons();
468
+ const names = Object.keys(allIcons);
469
+
470
+ // Detail for a specific icon
471
+ if (filterName) {
472
+ const icon = allIcons[filterName];
473
+ if (!icon) {
474
+ return { error: `Unknown icon: '${filterName}'`, available: names };
475
+ }
476
+ return {
477
+ name: filterName,
478
+ category: icon.category,
479
+ source: icon.source,
480
+ svg: icon.svg,
481
+ usage: {
482
+ js: `iconManager.getIcon('${filterName}', { size: 'md' })`,
483
+ config: `icon: '${filterName}'`,
484
+ html: `<span class="icon-text">\n \${iconManager.getIcon('${filterName}', { size: 'md', class: 'icon-primary' })}\n <span>Label</span>\n</span>`
485
+ },
486
+ sizes: 'xs (12px) | sm (16px) | md (20px) | lg (24px) | xl (32px) | 2xl (48px) | 3xl (64px)'
487
+ };
488
+ }
489
+
490
+ // Compact list grouped by category
491
+ const byCategory = {};
492
+ for (const [name, info] of Object.entries(allIcons)) {
493
+ const cat = info.category;
494
+ if (!byCategory[cat]) byCategory[cat] = [];
495
+ byCategory[cat].push(name);
496
+ }
497
+
498
+ return {
499
+ icons: byCategory,
500
+ count: names.length,
501
+ usage: {
502
+ js: "iconManager.getIcon('icon-name', { size: 'md' })",
503
+ config: "icon: 'icon-name'",
504
+ sizes: 'xs | sm | md | lg | xl | 2xl | 3xl'
505
+ },
506
+ message: `${names.length} icons across ${Object.keys(byCategory).length} categories. Pass 'name' for SVG content and usage.`
507
+ };
508
+ }
509
+
510
+ /**
511
+ * CSS index — imported from standalone module (avoids circular dependency with build-linter.js).
512
+ */
513
+ import { getValidCssClasses, collectCssFiles } from './css-index.js';
514
+ export { getValidCssClasses };
515
+
516
+
517
+ /**
518
+ * Run the build-time linter and return structured results.
519
+ *
520
+ * Spawns a fresh Node process to avoid ESM module caching — ensures the linter
521
+ * always uses the latest code from disk (build-linter.js, course-parser.js,
522
+ * schema-extractor.js, validation-rules.js, etc.).
523
+ *
524
+ * Post-processing (structured parsing, CSS suggestions) runs in-process since
525
+ * it only depends on CSS files which are read fresh from disk by PostCSS.
526
+ */
527
+ export async function lintCourse() {
528
+ try {
529
+ const courseRoot = getCourseRoot();
530
+ const coursePath = path.join(courseRoot, 'course');
531
+ const configPath = path.join(coursePath, 'course-config.js');
532
+
533
+ if (!fs.existsSync(configPath)) {
534
+ return { error: 'No course-config.js found', errors: [], warnings: [], passed: false };
535
+ }
536
+
537
+ // Resolve paths for the child process
538
+ const linterPath = pathToFileURL(path.resolve(path.join(__dirname, 'build-linter.js'))).href;
539
+ const absConfigPath = pathToFileURL(path.resolve(configPath)).href;
540
+ const absCoursePath = path.resolve(coursePath);
541
+
542
+ // Inline script for the child process — loads everything fresh
543
+ const script = `
544
+ const configModule = await import('${absConfigPath}');
545
+ const config = configModule.default || configModule.courseConfig;
546
+ if (!config) { console.log(JSON.stringify({ error: 'no-config' })); process.exit(0); }
547
+ const { lintCourse } = await import('${linterPath}');
548
+ const result = await lintCourse(config, '${absCoursePath.replace(/\\/g, '\\\\')}');
549
+ console.log(JSON.stringify(result));
550
+ `;
551
+
552
+ // Spawn fresh Node process — zero ESM cache
553
+ const { errors, warnings } = await new Promise((resolve, reject) => {
554
+ const child = spawn('node', ['--input-type=module', '-e', script], {
555
+ cwd: courseRoot,
556
+ stdio: ['ignore', 'pipe', 'pipe'],
557
+ shell: false
558
+ });
559
+
560
+ // Kill child if it hangs (10s timeout — lint typically completes in ~1-2s)
561
+ const timeout = setTimeout(() => {
562
+ child.kill('SIGKILL');
563
+ reject(new Error('Lint process timed out after 10s'));
564
+ }, 10000);
565
+
566
+ let stdout = '';
567
+ let stderr = '';
568
+ child.stdout.on('data', d => { stdout += d; });
569
+ child.stderr.on('data', d => { stderr += d; });
570
+
571
+ child.on('close', (code) => {
572
+ clearTimeout(timeout);
573
+ if (code !== 0 && !stdout.trim()) {
574
+ reject(new Error(stderr.trim() || `Lint process exited with code ${code}`));
575
+ return;
576
+ }
577
+ try {
578
+ const result = JSON.parse(stdout.trim());
579
+ if (result.error === 'no-config') {
580
+ reject(new Error('course-config.js does not export courseConfig'));
581
+ return;
582
+ }
583
+ resolve(result);
584
+ } catch {
585
+ reject(new Error(`Failed to parse lint output: ${stdout.slice(0, 200)}`));
586
+ }
587
+ });
588
+
589
+ child.on('error', (err) => {
590
+ clearTimeout(timeout);
591
+ reject(err);
592
+ });
593
+ });
594
+
595
+ // Parse string results into structured objects
596
+ const structuredErrors = errors.map(msg => parseLintMessage(msg, 'error'));
597
+ const structuredWarnings = warnings.map(msg => parseLintMessage(msg, 'warning'));
598
+
599
+ // Add CSS class suggestions to relevant warnings
600
+ const validCss = getValidCssClasses();
601
+ for (const warning of structuredWarnings) {
602
+ if (warning.rule === 'undefined-css-class' && warning.class) {
603
+ warning.suggestion = suggestCssFix(warning.class, validCss);
604
+ }
605
+ }
606
+
607
+ return {
608
+ errors: structuredErrors,
609
+ warnings: structuredWarnings,
610
+ errorCount: structuredErrors.length,
611
+ warningCount: structuredWarnings.length,
612
+ passed: structuredErrors.length === 0,
613
+ message: structuredErrors.length === 0
614
+ ? (structuredWarnings.length > 0 ? `Passed with ${structuredWarnings.length} warning(s).` : 'All checks passed.')
615
+ : `${structuredErrors.length} error(s) found.`
616
+ };
617
+ } catch (error) {
618
+ return {
619
+ error: error.message,
620
+ errors: [{ rule: 'lint-failure', message: error.message, severity: 'error' }],
621
+ warnings: [],
622
+ passed: false
623
+ };
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Parse a lint message string into a structured object.
629
+ * Input format: 'Slide "slide-id": message text'
630
+ */
631
+ function parseLintMessage(msg, severity) {
632
+ const slideMatch = msg.match(/^Slide "([^"]+)": (.+)$/);
633
+ const result = {
634
+ severity,
635
+ message: msg,
636
+ slideId: slideMatch ? slideMatch[1] : null,
637
+ detail: slideMatch ? slideMatch[2] : msg,
638
+ rule: classifyLintRule(msg)
639
+ };
640
+
641
+ // Extract class name if it's a CSS class warning
642
+ const classMatch = msg.match(/CSS class "([^"]+)"/);
643
+ if (classMatch) result.class = classMatch[1];
644
+
645
+ return result;
646
+ }
647
+
648
+ /**
649
+ * Classify a lint message into a rule category.
650
+ */
651
+ function classifyLintRule(msg) {
652
+ if (msg.includes('CSS class')) return 'undefined-css-class';
653
+ if (msg.includes('unknown component type')) return 'unknown-component';
654
+ if (msg.includes('requirement but no')) return 'requirement-missing-component';
655
+ if (msg.includes('should match filename')) return 'slide-id-filename-mismatch';
656
+ if (msg.includes('non-existent file')) return 'missing-slide-file';
657
+ if (msg.includes('Assessment ID mismatch')) return 'assessment-id-mismatch';
658
+ if (msg.includes('gating')) return 'invalid-gating';
659
+ if (msg.includes('interaction')) return 'interaction-config';
660
+ return 'general';
661
+ }
662
+
663
+ /**
664
+ * Suggest a fix for an undefined CSS class.
665
+ * Checks if a matching data-component value exists.
666
+ */
667
+ function suggestCssFix(className, validCss) {
668
+ // Check if removing "pattern-" prefix yields a valid data-component
669
+ if (className.startsWith('pattern-')) {
670
+ const componentName = className.replace('pattern-', '');
671
+ if (validCss.dataComponents.includes(componentName)) {
672
+ return `Replace class="${className}" with data-component="${componentName}"`;
673
+ }
674
+ }
675
+
676
+ // Check for close matches (simple Levenshtein-like)
677
+ const closeMatches = validCss.classes.filter(cls => {
678
+ if (Math.abs(cls.length - className.length) > 2) return false;
679
+ let diff = 0;
680
+ for (let i = 0; i < Math.max(cls.length, className.length); i++) {
681
+ if (cls[i] !== className[i]) diff++;
682
+ if (diff > 2) return false;
683
+ }
684
+ return diff > 0 && diff <= 2;
685
+ });
686
+
687
+ if (closeMatches.length > 0) {
688
+ return `Did you mean: ${closeMatches.slice(0, 3).join(', ')}?`;
689
+ }
690
+
691
+ return null;
692
+ }
693
+
694
+ // =============================================================================
695
+ // WORKFLOW STATUS & BUILD TOOLS
696
+ // =============================================================================
697
+
698
+ /**
699
+ * Detect the current authoring stage by inspecting the filesystem.
700
+ * Returns the inferred stage, a checklist of what exists, and recommended next action.
701
+ *
702
+ * Stages:
703
+ * 1. Source Ingestion - convert reference docs to markdown
704
+ * 2. Outline Creation - create COURSE_OUTLINE.md from references
705
+ * 3. Course Building - build slides and course config
706
+ * 4. Preview & Polish - iterate on visual quality
707
+ * 5. Export Ready - course passes lint, ready to deploy
708
+ */
709
+ export async function getWorkflowStatus(port = 4173) {
710
+ let courseRoot;
711
+ try {
712
+ courseRoot = getCourseRoot();
713
+ } catch {
714
+ return {
715
+ stage: 'not-initialized',
716
+ stageNumber: 0,
717
+ checklist: {},
718
+ nextAction: 'Create a CourseCode project: coursecode create my-course',
719
+ recommendedTool: null,
720
+ message: 'No course directory found. Create a project first.'
721
+ };
722
+ }
723
+
724
+ const courseDir = path.join(courseRoot, 'course');
725
+ const refsDir = path.join(courseDir, 'references');
726
+ const mdDir = path.join(refsDir, 'converted');
727
+ const slidesDir = path.join(courseDir, 'slides');
728
+ const outlinePath = path.join(courseDir, 'COURSE_OUTLINE.md');
729
+ const configPath = path.join(courseDir, 'course-config.js');
730
+
731
+ // Filesystem checks
732
+ const rawRefs = listFiles(refsDir).filter(f =>
733
+ ['.pdf', '.docx', '.doc', '.pptx', '.ppt', '.md'].some(ext => f.toLowerCase().endsWith(ext))
734
+ );
735
+ const convertedRefs = listFiles(mdDir).filter(f => f.endsWith('.md'));
736
+ const slides = listFiles(slidesDir).filter(f => f.endsWith('.js') && !f.startsWith('example-'));
737
+
738
+ // Load config object to check runtime settings
739
+ let courseConfigObj = null;
740
+ if (fs.existsSync(configPath)) {
741
+ try {
742
+ const configUrl = pathToFileURL(configPath).href + `?t=${Date.now()}`;
743
+ const configModule = await import(configUrl);
744
+ courseConfigObj = configModule.courseConfig || configModule.default;
745
+ } catch {
746
+ // Config parse error — leave as null
747
+ }
748
+ }
749
+
750
+ const checklist = {
751
+ hasRawRefs: rawRefs.length > 0,
752
+ hasConvertedRefs: convertedRefs.length > 0,
753
+ rawRefCount: rawRefs.length,
754
+ convertedRefCount: convertedRefs.length,
755
+ hasOutline: fs.existsSync(outlinePath),
756
+ hasSlides: slides.length > 0,
757
+ slideCount: slides.length,
758
+ hasCourseConfig: fs.existsSync(configPath),
759
+ hasAutomationEnabled: courseConfigObj?.environment?.automation?.enabled === true,
760
+ source: courseConfigObj?.source || null,
761
+ previewRunning: false
762
+ };
763
+
764
+ // Check preview status
765
+ try {
766
+ const previewStatus = await getPreviewStatus(port);
767
+ checklist.previewRunning = previewStatus.running;
768
+ } catch {
769
+ // Preview check failed, leave as false
770
+ }
771
+
772
+ // Infer stage
773
+ let stage, stageNumber, nextAction, recommendedTool;
774
+
775
+ if (checklist.hasSlides && checklist.hasCourseConfig) {
776
+ // Course is built — run lint to decide polish vs export
777
+ let lintPassed = false;
778
+ try {
779
+ const lintResult = await lintCourse();
780
+ lintPassed = lintResult.passed === true;
781
+ checklist.lintPassed = lintPassed;
782
+ checklist.lintErrorCount = lintResult.errorCount || 0;
783
+ checklist.lintWarningCount = lintResult.warningCount || 0;
784
+ } catch {
785
+ checklist.lintPassed = false;
786
+ }
787
+
788
+ if (lintPassed) {
789
+ stage = 'export-ready';
790
+ stageNumber = 5;
791
+ nextAction = 'Lint passes. Run coursecode_build to export (format: cmi5, scorm2004, scorm1.2, or lti).';
792
+ recommendedTool = 'coursecode_build';
793
+ } else {
794
+ stage = 'preview-polish';
795
+ stageNumber = 4;
796
+ if (checklist.source === 'powerpoint-import') {
797
+ nextAction = 'Imported from PowerPoint. Enhance with AI: add engagement tracking, assessments, group slides into sections, customize theme. Use coursecode_screenshot to review slides.';
798
+ } else {
799
+ nextAction = 'Use coursecode_lint to find issues, coursecode_screenshot to check visual quality, iterate until lint passes.';
800
+ }
801
+ recommendedTool = 'coursecode_lint';
802
+ }
803
+ } else if (!checklist.hasRawRefs && !checklist.hasConvertedRefs) {
804
+ stage = 'source-ingestion';
805
+ stageNumber = 1;
806
+ nextAction = 'Add reference files (PDF, DOCX, PPTX, MD) to course/references/ and run coursecode convert';
807
+ recommendedTool = 'coursecode_workflow_status';
808
+ } else if (checklist.hasRawRefs && !checklist.hasConvertedRefs) {
809
+ stage = 'source-ingestion';
810
+ stageNumber = 1;
811
+ nextAction = 'Convert reference files to markdown: coursecode convert';
812
+ recommendedTool = 'coursecode_workflow_status';
813
+ } else if (!checklist.hasOutline) {
814
+ stage = 'outline-creation';
815
+ stageNumber = 2;
816
+ nextAction = 'Create course outline from reference materials. Stage instructions have all file paths.';
817
+ recommendedTool = 'coursecode_workflow_status';
818
+ } else {
819
+ stage = 'course-building';
820
+ stageNumber = 3;
821
+ nextAction = 'Build slide files and course-config.js based on the outline. Stage instructions have all file paths.';
822
+ recommendedTool = 'coursecode_workflow_status';
823
+ }
824
+
825
+ return {
826
+ stage,
827
+ stageNumber,
828
+ checklist,
829
+ nextAction,
830
+ recommendedTool,
831
+ message: `Stage ${stageNumber}/5: ${stage}`
832
+ };
833
+ }
834
+
835
+ /**
836
+ * Build the course for deployment.
837
+ * Spawns Vite production build with the appropriate LMS format config.
838
+ */
839
+ export async function buildCourse(options = {}) {
840
+ const format = options.format || 'cmi5';
841
+ const startTime = Date.now();
842
+
843
+ let courseRoot;
844
+ try {
845
+ courseRoot = getCourseRoot();
846
+ } catch (error) {
847
+ return { success: false, error: error.message, errors: [error.message], warnings: [], duration: '0s' };
848
+ }
849
+
850
+ const configPath = path.join(courseRoot, 'course', 'course-config.js');
851
+ if (!fs.existsSync(configPath)) {
852
+ return { success: false, error: 'No course-config.js found', errors: ['No course-config.js found'], warnings: [], duration: '0s' };
853
+ }
854
+
855
+ return new Promise((resolve) => {
856
+ const env = { ...process.env, LMS_FORMAT: format };
857
+ const child = spawn('npx', ['vite', 'build'], {
858
+ cwd: courseRoot,
859
+ env,
860
+ shell: true,
861
+ stdio: ['ignore', 'pipe', 'pipe']
862
+ });
863
+
864
+ let _stdout = '';
865
+ let stderr = '';
866
+
867
+ child.stdout.on('data', (data) => { _stdout += data.toString(); });
868
+ child.stderr.on('data', (data) => { stderr += data.toString(); });
869
+
870
+ child.on('close', (code) => {
871
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
872
+ const outputDir = path.join(courseRoot, 'dist');
873
+ const errors = [];
874
+ const warnings = [];
875
+
876
+ // Parse stderr for errors/warnings
877
+ for (const line of stderr.split('\n')) {
878
+ const trimmed = line.trim();
879
+ if (!trimmed) continue;
880
+ if (trimmed.toLowerCase().includes('warning')) {
881
+ warnings.push(trimmed);
882
+ } else if (trimmed.toLowerCase().includes('error')) {
883
+ errors.push(trimmed);
884
+ }
885
+ }
886
+
887
+ if (code !== 0) {
888
+ errors.push(`Build exited with code ${code}`);
889
+ if (stderr.trim()) errors.push(stderr.trim().slice(0, 500));
890
+ }
891
+
892
+ resolve({
893
+ success: code === 0,
894
+ format,
895
+ outputDir: fs.existsSync(outputDir) ? outputDir : null,
896
+ errors,
897
+ warnings,
898
+ duration,
899
+ message: code === 0
900
+ ? `Build succeeded (${format}) in ${duration}. Output: ${outputDir}`
901
+ : `Build failed. ${errors.length} error(s).`
902
+ });
903
+ });
904
+
905
+ child.on('error', (error) => {
906
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
907
+ resolve({
908
+ success: false,
909
+ format,
910
+ outputDir: null,
911
+ errors: [error.message],
912
+ warnings: [],
913
+ duration,
914
+ message: `Build failed: ${error.message}`
915
+ });
916
+ });
917
+ });
918
+ }
919
+