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,269 @@
1
+ /**
2
+ * Project Utilities
3
+ *
4
+ * Shared helpers used across multiple CLI commands and lib/ modules.
5
+ * Single source of truth for project validation and common utilities.
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+
11
+ // =============================================================================
12
+ // PROJECT VALIDATION
13
+ // =============================================================================
14
+
15
+ /**
16
+ * Validate that the current working directory is a valid CourseCode project.
17
+ * Consolidates 6 former copies into one with options.
18
+ *
19
+ * @param {Object} [options={}]
20
+ * @param {boolean} [options.frameworkDev=false] - Expect framework repo layout
21
+ * @param {boolean} [options.warnMissingRc=false] - Warn if .coursecoderc.json is missing
22
+ * @returns {{ coursePath: string, viteConfig: string|null }} Resolved paths
23
+ */
24
+ export function validateProject(options = {}) {
25
+ const { frameworkDev = false, warnMissingRc = false } = options;
26
+ const cwd = process.cwd();
27
+
28
+ if (frameworkDev) {
29
+ const hasTemplateCourse = fs.existsSync(path.join(cwd, 'template', 'course'));
30
+ const hasFrameworkDir = fs.existsSync(path.join(cwd, 'framework'));
31
+ const hasViteDevConfig = fs.existsSync(path.join(cwd, 'vite.framework-dev.config.js'));
32
+
33
+ if (!hasTemplateCourse || !hasFrameworkDir || !hasViteDevConfig) {
34
+ console.error(`
35
+ ❌ Not a valid CourseCode framework repository.
36
+
37
+ Missing required paths:
38
+ ${!hasTemplateCourse ? ' - template/course/' : ''}
39
+ ${!hasFrameworkDir ? ' - framework/' : ''}
40
+ ${!hasViteDevConfig ? ' - vite.framework-dev.config.js' : ''}
41
+
42
+ Run this command from the framework source repository root.
43
+ `);
44
+ process.exit(1);
45
+ }
46
+
47
+ return {
48
+ coursePath: path.join(cwd, 'template', 'course'),
49
+ viteConfig: 'vite.framework-dev.config.js'
50
+ };
51
+ }
52
+
53
+ const hasCourseDir = fs.existsSync(path.join(cwd, 'course'));
54
+ const hasFrameworkDir = fs.existsSync(path.join(cwd, 'framework'));
55
+
56
+ if (!hasCourseDir || !hasFrameworkDir) {
57
+ // Detect framework repo for helpful error message
58
+ const isFrameworkRepo = fs.existsSync(path.join(cwd, 'lib', 'preview-server.js')) &&
59
+ fs.existsSync(path.join(cwd, 'template', 'course'));
60
+
61
+ if (isFrameworkRepo) {
62
+ console.error(`
63
+ ❌ Detected framework source repository.
64
+
65
+ Use --framework-dev flag to run in framework development mode:
66
+
67
+ coursecode preview --framework-dev
68
+ `);
69
+ } else {
70
+ console.error(`
71
+ ❌ Not a valid CourseCode project directory.
72
+
73
+ Missing required directories:
74
+ ${!hasCourseDir ? ' - course/' : ''}
75
+ ${!hasFrameworkDir ? ' - framework/' : ''}
76
+
77
+ Run this command from a CourseCode project root, or create a new project:
78
+
79
+ coursecode create my-course
80
+ `);
81
+ }
82
+ process.exit(1);
83
+ }
84
+
85
+ if (warnMissingRc && !fs.existsSync(path.join(cwd, '.coursecoderc.json'))) {
86
+ console.warn(`
87
+ ⚠️ No .coursecoderc.json found. This project may not have been created with the CourseCode CLI.
88
+ Framework upgrades may not work correctly.
89
+ `);
90
+ }
91
+
92
+ return {
93
+ coursePath: path.join(cwd, 'course'),
94
+ viteConfig: null
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Validate that a specific course path contains a course-config.js.
100
+ * Used by export-content which takes an explicit path rather than using cwd.
101
+ *
102
+ * @param {string} coursePath - Path to course directory (absolute or relative)
103
+ * @param {Object} [options={}]
104
+ * @param {boolean} [options.silent=false] - Return null instead of exiting on error
105
+ * @returns {string|null} Resolved absolute path, or null if silent and invalid
106
+ */
107
+ export function validateCoursePath(coursePath, options = {}) {
108
+ const { silent = false } = options;
109
+ const cwd = process.cwd();
110
+ const fullCoursePath = path.isAbsolute(coursePath) ? coursePath : path.join(cwd, coursePath);
111
+
112
+ if (!fs.existsSync(path.join(fullCoursePath, 'course-config.js'))) {
113
+ if (silent) return null;
114
+ console.error(`
115
+ ❌ Could not find course-config.js in ${fullCoursePath}
116
+
117
+ Make sure you're running this command from a CourseCode project root,
118
+ or specify the correct path with --course-path.
119
+ `);
120
+ process.exit(1);
121
+ }
122
+
123
+ return fullCoursePath;
124
+ }
125
+
126
+ // =============================================================================
127
+ // HTML UTILITIES
128
+ // =============================================================================
129
+
130
+ /**
131
+ * Escape HTML special characters in a string.
132
+ * @param {string} str - Input string
133
+ * @returns {string} Escaped string
134
+ */
135
+ export function escapeHtml(str) {
136
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
137
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
138
+ }
139
+
140
+ // =============================================================================
141
+ // MIME TYPES
142
+ // =============================================================================
143
+
144
+ const mimeTypes = {
145
+ '.html': 'text/html',
146
+ '.css': 'text/css',
147
+ '.js': 'application/javascript',
148
+ '.mjs': 'application/javascript',
149
+ '.json': 'application/json',
150
+ '.png': 'image/png',
151
+ '.jpg': 'image/jpeg',
152
+ '.jpeg': 'image/jpeg',
153
+ '.gif': 'image/gif',
154
+ '.svg': 'image/svg+xml',
155
+ '.webp': 'image/webp',
156
+ '.ico': 'image/x-icon',
157
+ '.woff': 'font/woff',
158
+ '.woff2': 'font/woff2',
159
+ '.ttf': 'font/ttf',
160
+ '.eot': 'application/vnd.ms-fontobject',
161
+ '.mp3': 'audio/mpeg',
162
+ '.mp4': 'video/mp4',
163
+ '.webm': 'video/webm',
164
+ '.ogg': 'audio/ogg',
165
+ '.wav': 'audio/wav',
166
+ '.xml': 'application/xml',
167
+ '.xsd': 'application/xml',
168
+ '.pdf': 'application/pdf',
169
+ '.zip': 'application/zip'
170
+ };
171
+
172
+ /**
173
+ * Get MIME type for a file path based on extension.
174
+ * @param {string} filePath
175
+ * @returns {string}
176
+ */
177
+ export function getMimeType(filePath) {
178
+ return mimeTypes[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
179
+ }
180
+
181
+ // =============================================================================
182
+ // FILE SERVING
183
+ // =============================================================================
184
+
185
+ /**
186
+ * Serve a static file over HTTP with proper Content-Type and Content-Length.
187
+ * @param {string} filePath - Absolute path to file
188
+ * @param {import('http').ServerResponse} res
189
+ */
190
+ export function serveFile(filePath, res) {
191
+ fs.stat(filePath, (statErr, stats) => {
192
+ if (statErr) {
193
+ res.writeHead(404);
194
+ res.end('Not found');
195
+ return;
196
+ }
197
+
198
+ fs.readFile(filePath, (err, data) => {
199
+ if (err) {
200
+ res.writeHead(404);
201
+ res.end('Not found');
202
+ return;
203
+ }
204
+
205
+ res.writeHead(200, {
206
+ 'Content-Type': getMimeType(filePath),
207
+ 'Content-Length': stats.size,
208
+ 'Accept-Ranges': 'bytes'
209
+ });
210
+ res.end(data);
211
+ });
212
+ });
213
+ }
214
+
215
+ // =============================================================================
216
+ // COURSE STRUCTURE HELPERS
217
+ // =============================================================================
218
+
219
+ /**
220
+ * Count total slides in a course structure (including nested sections).
221
+ * @param {Array} items - Structure array
222
+ * @returns {number}
223
+ */
224
+ export function countSlides(items) {
225
+ let count = 0;
226
+ for (const item of items) {
227
+ if (item.type === 'slide' || item.type === 'assessment') {
228
+ count++;
229
+ } else if (item.children) {
230
+ count += countSlides(item.children);
231
+ }
232
+ }
233
+ return count;
234
+ }
235
+
236
+ /**
237
+ * Find a slide by ID in a nested structure.
238
+ * @param {Array} items - Structure array
239
+ * @param {string} id - Slide ID to find
240
+ * @returns {object|null}
241
+ */
242
+ export function findSlideById(items, id) {
243
+ for (const item of items) {
244
+ if (item.id === id) return item;
245
+ if (item.children) {
246
+ const found = findSlideById(item.children, id);
247
+ if (found) return found;
248
+ }
249
+ }
250
+ return null;
251
+ }
252
+
253
+ /**
254
+ * Collect all slide IDs from a nested structure into a flat array.
255
+ * @param {Array} items - Structure array
256
+ * @returns {Array<{id: string, title: string, type: string}>}
257
+ */
258
+ export function collectSlideIds(items) {
259
+ const slides = [];
260
+ for (const item of items) {
261
+ if (item.type === 'slide' || item.type === 'assessment') {
262
+ slides.push({ id: item.id, title: item.title || item.id, type: item.type });
263
+ }
264
+ if (item.children) {
265
+ slides.push(...collectSlideIds(item.children));
266
+ }
267
+ }
268
+ return slides;
269
+ }
@@ -0,0 +1,68 @@
1
+ <!DOCTYPE html>
2
+ <!--
3
+ SCORM Proxy - Loads course from external CDN
4
+ Generated by CourseCode - Do not edit manually
5
+
6
+ This page:
7
+ 1. Embeds the course in an iframe from the external URL
8
+ 2. Bridges postMessage calls to the LMS SCORM API via pipwerks
9
+ -->
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="UTF-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
14
+ <title>Loading Course...</title>
15
+ <style>
16
+ * { margin: 0; padding: 0; box-sizing: border-box; }
17
+ html, body { width: 100%; height: 100%; overflow: hidden; }
18
+ iframe { width: 100%; height: 100%; border: none; }
19
+ .loading {
20
+ position: absolute;
21
+ top: 50%;
22
+ left: 50%;
23
+ transform: translate(-50%, -50%);
24
+ font-family: system-ui, sans-serif;
25
+ text-align: center;
26
+ color: #666;
27
+ }
28
+ .loading .spinner {
29
+ width: 40px;
30
+ height: 40px;
31
+ margin: 0 auto 16px;
32
+ border: 3px solid #eee;
33
+ border-top-color: #333;
34
+ border-radius: 50%;
35
+ animation: spin 1s linear infinite;
36
+ }
37
+ @keyframes spin { to { transform: rotate(360deg); } }
38
+ </style>
39
+ </head>
40
+ <body>
41
+ <div class="loading" id="loading">
42
+ <div class="spinner"></div>
43
+ <div>Loading course...</div>
44
+ </div>
45
+
46
+ <iframe id="course" style="display:none"></iframe>
47
+
48
+ <script src="scorm-bridge.js"></script>
49
+ <script>
50
+ // Course URL is injected at build time
51
+ const COURSE_URL = '{{EXTERNAL_URL}}';
52
+
53
+ const iframe = document.getElementById('course');
54
+ const loading = document.getElementById('loading');
55
+
56
+ iframe.onload = function() {
57
+ loading.style.display = 'none';
58
+ iframe.style.display = 'block';
59
+ };
60
+
61
+ iframe.onerror = function() {
62
+ loading.innerHTML = '<div style="color:#c00">Failed to load course</div>';
63
+ };
64
+
65
+ iframe.src = COURSE_URL;
66
+ </script>
67
+ </body>
68
+ </html>
@@ -0,0 +1,112 @@
1
+ /**
2
+ * SCORM Bridge - Connects postMessage from iframe to LMS API
3
+ * Generated by CourseCode - Do not edit manually
4
+ *
5
+ * Uses pipwerks wrapper which auto-detects SCORM 1.2 or 2004 API.
6
+ */
7
+
8
+ // Import pipwerks (bundled with proxy package)
9
+ // Note: pipwerks is loaded as a global in the proxy package
10
+ // We check for it and initialize on load
11
+
12
+ (function() {
13
+ 'use strict';
14
+
15
+ // Wait for pipwerks to be available
16
+ if (typeof pipwerks === 'undefined') {
17
+ console.error('SCORM Bridge: pipwerks not loaded');
18
+ return;
19
+ }
20
+
21
+ const scorm = pipwerks.SCORM;
22
+ const iframe = document.getElementById('course');
23
+ let initialized = false;
24
+
25
+ // Derive expected course origin from build-time URL for postMessage validation
26
+ // COURSE_URL is injected by the build system (e.g., 'https://cdn.example.com/my-course')
27
+ let courseOrigin = '*'; // Fallback if URL parsing fails
28
+ try {
29
+ if (typeof COURSE_URL === 'string' && COURSE_URL && !COURSE_URL.startsWith('{{')) {
30
+ courseOrigin = new URL(COURSE_URL).origin;
31
+ console.log('SCORM Bridge: Expected course origin:', courseOrigin);
32
+ }
33
+ } catch (e) {
34
+ console.warn('SCORM Bridge: Could not parse COURSE_URL for origin validation:', e.message);
35
+ }
36
+
37
+ // Disable pipwerks auto-handling - we manage status ourselves
38
+ scorm.handleCompletionStatus = false;
39
+ scorm.handleExitMode = false;
40
+
41
+ /**
42
+ * Handle messages from course iframe
43
+ */
44
+ window.addEventListener('message', function(event) {
45
+ const { data } = event;
46
+
47
+ // Ignore non-proxy messages
48
+ if (!data || data.type !== 'scorm-proxy-request') {
49
+ return;
50
+ }
51
+
52
+ // Validate origin when we have a known course origin
53
+ if (courseOrigin !== '*' && event.origin !== courseOrigin) {
54
+ console.warn('SCORM Bridge: Rejected message from unexpected origin:', event.origin);
55
+ return;
56
+ }
57
+
58
+ const { id, method, args = [] } = data;
59
+ let result;
60
+ let error = null;
61
+
62
+ try {
63
+ switch (method) {
64
+ case 'Initialize':
65
+ if (!initialized) {
66
+ result = scorm.init();
67
+ initialized = result;
68
+ } else {
69
+ result = true;
70
+ }
71
+ break;
72
+
73
+ case 'GetValue':
74
+ result = scorm.get(args[0]);
75
+ break;
76
+
77
+ case 'SetValue':
78
+ result = scorm.set(args[0], args[1]);
79
+ break;
80
+
81
+ case 'Commit':
82
+ result = scorm.save();
83
+ break;
84
+
85
+ case 'Terminate':
86
+ result = scorm.quit();
87
+ initialized = false;
88
+ break;
89
+
90
+ default:
91
+ error = `Unknown method: ${method}`;
92
+ }
93
+ } catch (e) {
94
+ error = e.message || String(e);
95
+ console.error('SCORM Bridge error:', method, e);
96
+ }
97
+
98
+ // Send response back to iframe (targeted to known origin)
99
+ if (iframe && iframe.contentWindow) {
100
+ iframe.contentWindow.postMessage({
101
+ type: 'scorm-proxy-response',
102
+ id,
103
+ result,
104
+ error
105
+ }, courseOrigin);
106
+ }
107
+ });
108
+
109
+ // Log bridge ready
110
+ console.log('SCORM Bridge: Ready (pipwerks v' + (pipwerks.version || '1.x') + ')');
111
+
112
+ })();
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Scaffold commands — create, clean, and generate course files
3
+ */
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+
8
+ /**
9
+ * Minimal course-config.js content for blank projects
10
+ */
11
+ const MINIMAL_CONFIG = `export const courseConfig = {
12
+ metadata: {
13
+ title: 'Course Title',
14
+ description: 'Course description',
15
+ version: '1.0.0',
16
+ language: 'en'
17
+ },
18
+ layout: 'article',
19
+ structure: [
20
+ {
21
+ type: 'slide',
22
+ id: 'intro',
23
+ component: '@slides/intro.js',
24
+ title: 'Introduction',
25
+ engagement: { required: false }
26
+ }
27
+ ],
28
+ navigation: {
29
+ sidebar: { enabled: true }
30
+ },
31
+ environment: {
32
+ automation: { enabled: true, exposeCorrectAnswers: true }
33
+ }
34
+ };
35
+ `;
36
+
37
+ /**
38
+ * Minimal slide template
39
+ */
40
+ function slideTemplate(id) {
41
+ const title = id.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
42
+ return `export const slide = {
43
+ render(root, context) {
44
+ const container = document.createElement('div');
45
+ container.innerHTML = \`
46
+ <h1>${title}</h1>
47
+ <p>Content goes here.</p>
48
+ \`;
49
+ return container;
50
+ }
51
+ };
52
+ `;
53
+ }
54
+
55
+ /**
56
+ * Minimal assessment template
57
+ */
58
+ function assessmentTemplate(id) {
59
+ return `const { AssessmentManager, createMultipleChoiceQuestion } = CourseCode;
60
+
61
+ export const config = {
62
+ id: '${id}',
63
+ assessmentObjective: null,
64
+ settings: {
65
+ passingScore: 80,
66
+ randomizeQuestions: false,
67
+ allowUnansweredSubmission: true
68
+ }
69
+ };
70
+
71
+ const questions = [
72
+ createMultipleChoiceQuestion({
73
+ id: '${id}-q1',
74
+ prompt: 'Question text?',
75
+ choices: [
76
+ { value: 'a', text: 'Option A' },
77
+ { value: 'b', text: 'Option B' },
78
+ { value: 'c', text: 'Option C' }
79
+ ],
80
+ correctAnswer: 'a',
81
+ controlled: true
82
+ })
83
+ ];
84
+
85
+ export const slide = {
86
+ render(root) {
87
+ const assessment = AssessmentManager.createAssessment(config, questions);
88
+ return assessment.render(root);
89
+ }
90
+ };
91
+ `;
92
+ }
93
+
94
+ /**
95
+ * Remove all example-* files and reset config to minimal starter.
96
+ * Looks for course/ directory relative to cwd or a provided base path.
97
+ */
98
+ export function clean(options = {}) {
99
+ const basePath = options.basePath || process.cwd();
100
+ const coursePath = path.join(basePath, 'course');
101
+
102
+ if (!fs.existsSync(coursePath)) {
103
+ console.error('\n❌ No course/ directory found. Are you in a CourseCode project?\n');
104
+ process.exit(1);
105
+ }
106
+
107
+ let removed = 0;
108
+
109
+ // Remove example slides
110
+ const slidesDir = path.join(coursePath, 'slides');
111
+ if (fs.existsSync(slidesDir)) {
112
+ for (const file of fs.readdirSync(slidesDir)) {
113
+ if (file.startsWith('example-')) {
114
+ fs.unlinkSync(path.join(slidesDir, file));
115
+ removed++;
116
+ }
117
+ }
118
+ }
119
+
120
+ // Remove example audio
121
+ const audioDir = path.join(coursePath, 'assets', 'audio');
122
+ if (fs.existsSync(audioDir)) {
123
+ for (const file of fs.readdirSync(audioDir)) {
124
+ if (file.startsWith('example-')) {
125
+ fs.unlinkSync(path.join(audioDir, file));
126
+ removed++;
127
+ }
128
+ }
129
+ }
130
+
131
+ // Rewrite course-config.js to minimal starter
132
+ const configPath = path.join(coursePath, 'course-config.js');
133
+ if (fs.existsSync(configPath)) {
134
+ fs.writeFileSync(configPath, MINIMAL_CONFIG);
135
+ }
136
+
137
+ // Create the intro slide if it doesn't exist
138
+ const introPath = path.join(slidesDir, 'intro.js');
139
+ if (!fs.existsSync(introPath)) {
140
+ fs.mkdirSync(slidesDir, { recursive: true });
141
+ fs.writeFileSync(introPath, slideTemplate('intro'));
142
+ }
143
+
144
+ console.log(`\n✅ Cleaned project — removed ${removed} example file${removed !== 1 ? 's' : ''}, reset course-config.js\n`);
145
+ }
146
+
147
+ /**
148
+ * Create a new slide file.
149
+ */
150
+ export function newSlide(id) {
151
+ const slidePath = path.join(process.cwd(), 'course', 'slides', `${id}.js`);
152
+
153
+ if (fs.existsSync(slidePath)) {
154
+ console.error(`\n❌ Slide file already exists: course/slides/${id}.js\n`);
155
+ process.exit(1);
156
+ }
157
+
158
+ fs.mkdirSync(path.dirname(slidePath), { recursive: true });
159
+ fs.writeFileSync(slidePath, slideTemplate(id));
160
+ console.log(`\n✅ Created slide: course/slides/${id}.js\n`);
161
+ }
162
+
163
+ /**
164
+ * Create a new assessment file.
165
+ */
166
+ export function newAssessment(id) {
167
+ const assessmentPath = path.join(process.cwd(), 'course', 'slides', `${id}.js`);
168
+
169
+ if (fs.existsSync(assessmentPath)) {
170
+ console.error(`\n❌ File already exists: course/slides/${id}.js\n`);
171
+ process.exit(1);
172
+ }
173
+
174
+ fs.mkdirSync(path.dirname(assessmentPath), { recursive: true });
175
+ fs.writeFileSync(assessmentPath, assessmentTemplate(id));
176
+ console.log(`\n✅ Created assessment: course/slides/${id}.js\n`);
177
+ }
178
+
179
+ /**
180
+ * Create a new course-config.js file.
181
+ */
182
+ export function newConfig() {
183
+ const configPath = path.join(process.cwd(), 'course', 'course-config.js');
184
+
185
+ if (fs.existsSync(configPath)) {
186
+ console.error('\n❌ course/course-config.js already exists. Use `coursecode clean` to reset it.\n');
187
+ process.exit(1);
188
+ }
189
+
190
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
191
+ fs.writeFileSync(configPath, MINIMAL_CONFIG);
192
+ console.log('\n✅ Created course/course-config.js\n');
193
+ }