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,258 @@
1
+ /**
2
+ * Course Writer - Unified writing utility for CourseCode
3
+ *
4
+ * Single write() function handles config-object edit operations:
5
+ * - config: courseConfig properties
6
+ * - slide: slide config in structure array
7
+ * - objective: objective properties
8
+ * - gating: gating conditions
9
+ *
10
+ * For source-text edits (interactions, assessments, templates),
11
+ * see slide-source-editor.js.
12
+ */
13
+
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import { pathToFileURL } from 'url';
17
+
18
+ // =============================================================================
19
+ // PUBLIC API
20
+ // =============================================================================
21
+
22
+ // In-memory write mutex to prevent concurrent writes from clobbering each other.
23
+ // Each write() call chains onto the previous one, ensuring serial execution.
24
+ let _writeQueue = Promise.resolve();
25
+
26
+ /**
27
+ * Unified write dispatcher
28
+ * @param {string} coursePath - Path to course directory
29
+ * @param {string} target - What to write: 'config' | 'slide' | 'objective' | 'gating' | 'rename-objective'
30
+ * @param {string} id - Identifier (path for config, slideId for slides, objectiveId, etc.)
31
+ * @param {*} value - New value to set
32
+ * @returns {Promise<{success: boolean, error?: string}>}
33
+ */
34
+ export function write(coursePath, target, id, value) {
35
+ const result = _writeQueue.then(() => _doWrite(coursePath, target, id, value));
36
+ // Keep the queue moving even if this write fails
37
+ _writeQueue = result.catch(() => {});
38
+ return result;
39
+ }
40
+
41
+ async function _doWrite(coursePath, target, id, value) {
42
+ try {
43
+ switch (target) {
44
+ case 'config':
45
+ await writeConfig(coursePath, id, value);
46
+ break;
47
+ case 'slide':
48
+ await writeSlideConfig(coursePath, id, value);
49
+ break;
50
+ case 'objective':
51
+ await writeObjective(coursePath, id, value);
52
+ break;
53
+ case 'gating':
54
+ await writeGating(coursePath, id, value);
55
+ break;
56
+ case 'rename-objective':
57
+ await renameObjective(coursePath, id, value);
58
+ break;
59
+ default:
60
+ return { success: false, error: `Unknown target: ${target}` };
61
+ }
62
+ return { success: true };
63
+ } catch (err) {
64
+ return { success: false, error: err.message };
65
+ }
66
+ }
67
+
68
+
69
+ // =============================================================================
70
+ // TARGET HANDLERS
71
+ // =============================================================================
72
+
73
+ /**
74
+ * Write config property
75
+ * @param {string} coursePath
76
+ * @param {string} propPath - Dot-notation path (e.g., 'navigation.sidebar.enabled')
77
+ * @param {*} value
78
+ */
79
+ async function writeConfig(coursePath, propPath, value) {
80
+ const config = await loadConfig(coursePath);
81
+ setNestedProperty(config, propPath, value);
82
+ await saveConfig(coursePath, config);
83
+ }
84
+
85
+ /**
86
+ * Write slide config in structure array
87
+ * @param {string} coursePath
88
+ * @param {string} slideId
89
+ * @param {object} updates - Object with property paths and values, e.g. { 'engagement.required': true }
90
+ */
91
+ async function writeSlideConfig(coursePath, slideId, updates) {
92
+ const config = await loadConfig(coursePath);
93
+ const slide = findStructureItem(config.structure, slideId);
94
+ if (!slide) throw new Error(`Slide not found: ${slideId}`);
95
+
96
+ for (const [propPath, value] of Object.entries(updates)) {
97
+ setNestedProperty(slide, propPath, value);
98
+ }
99
+ await saveConfig(coursePath, config);
100
+ }
101
+
102
+ /**
103
+ * Write objective property
104
+ * @param {string} coursePath
105
+ * @param {string} objectiveId
106
+ * @param {object} updates - Object with property paths and values
107
+ */
108
+ async function writeObjective(coursePath, objectiveId, updates) {
109
+ const config = await loadConfig(coursePath);
110
+ const objective = config.objectives?.find(o => o.id === objectiveId);
111
+ if (!objective) throw new Error(`Objective not found: ${objectiveId}`);
112
+
113
+ for (const [propPath, value] of Object.entries(updates)) {
114
+ setNestedProperty(objective, propPath, value);
115
+ }
116
+ await saveConfig(coursePath, config);
117
+ }
118
+
119
+ /**
120
+ * Write gating conditions for a slide
121
+ * @param {string} coursePath
122
+ * @param {string} slideId
123
+ * @param {object} gatingConfig - Full gating object { mode, message, conditions }
124
+ */
125
+ async function writeGating(coursePath, slideId, gatingConfig) {
126
+ const config = await loadConfig(coursePath);
127
+ const slide = findStructureItem(config.structure, slideId);
128
+ if (!slide) throw new Error(`Slide not found: ${slideId}`);
129
+
130
+ if (!slide.navigation) slide.navigation = {};
131
+ slide.navigation.gating = gatingConfig;
132
+ await saveConfig(coursePath, config);
133
+ }
134
+
135
+ /**
136
+ * Rename an objective ID and cascade to all references
137
+ * @param {string} coursePath
138
+ * @param {string} oldId
139
+ * @param {string} newId
140
+ */
141
+ async function renameObjective(coursePath, oldId, newId) {
142
+ if (oldId === newId) return;
143
+
144
+ const config = await loadConfig(coursePath);
145
+ const objective = config.objectives?.find(o => o.id === oldId);
146
+ if (!objective) throw new Error(`Objective not found: ${oldId}`);
147
+
148
+ const conflict = config.objectives.find(o => o.id === newId);
149
+ if (conflict) throw new Error(`ID already exists: ${newId}`);
150
+
151
+ // Rename the objective itself
152
+ objective.id = newId;
153
+
154
+ // Cascade to all objectiveId references in gating conditions
155
+ function cascadeRename(items) {
156
+ for (const item of items) {
157
+ const conditions = item.navigation?.gating?.conditions;
158
+ if (conditions) {
159
+ for (const cond of conditions) {
160
+ if (cond.objectiveId === oldId) cond.objectiveId = newId;
161
+ }
162
+ }
163
+ if (item.children) cascadeRename(item.children);
164
+ }
165
+ }
166
+ cascadeRename(config.structure || []);
167
+
168
+ await saveConfig(coursePath, config);
169
+ }
170
+
171
+
172
+ // =============================================================================
173
+ // CONFIG HELPERS
174
+ // =============================================================================
175
+
176
+ async function loadConfig(coursePath) {
177
+ const configFilePath = path.join(coursePath, 'course-config.js');
178
+ const configUrl = pathToFileURL(configFilePath).href + `?t=${Date.now()}`;
179
+ const configModule = await import(configUrl);
180
+ return configModule.courseConfig || configModule.default;
181
+ }
182
+
183
+ async function saveConfig(coursePath, config) {
184
+ const configFilePath = path.join(coursePath, 'course-config.js');
185
+ const originalSource = fs.readFileSync(configFilePath, 'utf-8');
186
+ const headerComments = extractHeaderComments(originalSource);
187
+ const serialized = 'export const courseConfig = ' + serializeObject(config) + ';\n';
188
+ fs.writeFileSync(configFilePath, headerComments + '\n\n' + serialized, 'utf-8');
189
+ }
190
+
191
+ function setNestedProperty(obj, propPath, value) {
192
+ const parts = propPath.split('.');
193
+ let current = obj;
194
+ for (let i = 0; i < parts.length - 1; i++) {
195
+ if (current[parts[i]] === undefined) {
196
+ current[parts[i]] = {};
197
+ }
198
+ current = current[parts[i]];
199
+ }
200
+ current[parts[parts.length - 1]] = value;
201
+ }
202
+
203
+ function findStructureItem(structure, id) {
204
+ for (const item of structure) {
205
+ if (item.id === id) return item;
206
+ if (item.children) {
207
+ const found = findStructureItem(item.children, id);
208
+ if (found) return found;
209
+ }
210
+ }
211
+ return null;
212
+ }
213
+
214
+ function extractHeaderComments(source) {
215
+ const lines = source.split('\n');
216
+ const commentLines = [];
217
+ for (const line of lines) {
218
+ const trimmed = line.trim();
219
+ if (trimmed.startsWith('/**') || trimmed.startsWith('*') || trimmed.startsWith('*/') || trimmed === '') {
220
+ commentLines.push(line);
221
+ } else {
222
+ break;
223
+ }
224
+ }
225
+ while (commentLines.length > 0 && commentLines[commentLines.length - 1].trim() === '') {
226
+ commentLines.pop();
227
+ }
228
+ return commentLines.join('\n');
229
+ }
230
+
231
+ function serializeObject(obj, indent = 0) {
232
+ const spaces = ' '.repeat(indent);
233
+ const innerSpaces = ' '.repeat(indent + 1);
234
+
235
+ if (obj === null) return 'null';
236
+ if (obj === undefined) return 'undefined';
237
+ if (typeof obj === 'boolean') return String(obj);
238
+ if (typeof obj === 'number') return String(obj);
239
+ if (typeof obj === 'string') return `'${obj.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
240
+ if (typeof obj === 'function') return obj.toString();
241
+
242
+ if (Array.isArray(obj)) {
243
+ if (obj.length === 0) return '[]';
244
+ const items = obj.map(item => innerSpaces + serializeObject(item, indent + 1));
245
+ return '[\n' + items.join(',\n') + '\n' + spaces + ']';
246
+ }
247
+
248
+ const entries = Object.entries(obj);
249
+ if (entries.length === 0) return '{}';
250
+
251
+ const props = entries.map(([key, val]) => {
252
+ const keyStr = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `'${key}'`;
253
+ return innerSpaces + keyStr + ': ' + serializeObject(val, indent + 1);
254
+ });
255
+
256
+ return '{\n' + props.join(',\n') + '\n' + spaces + '}';
257
+ }
258
+
package/lib/create.js ADDED
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Create command - scaffold a new CourseCode project
3
+ */
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ import { spawn } from 'child_process';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const PACKAGE_ROOT = path.join(__dirname, '..');
12
+
13
+ /**
14
+ * Copy directory recursively
15
+ */
16
+ function copyDir(src, dest, options = {}) {
17
+ const { exclude = [] } = options;
18
+
19
+ if (!fs.existsSync(dest)) {
20
+ fs.mkdirSync(dest, { recursive: true });
21
+ }
22
+
23
+ const entries = fs.readdirSync(src, { withFileTypes: true });
24
+
25
+ for (const entry of entries) {
26
+ const srcPath = path.join(src, entry.name);
27
+ const destPath = path.join(dest, entry.name);
28
+
29
+ // Check exclusions
30
+ if (exclude.some(pattern => entry.name.match(pattern))) {
31
+ continue;
32
+ }
33
+
34
+ if (entry.isDirectory()) {
35
+ copyDir(srcPath, destPath, options);
36
+ } else {
37
+ fs.copyFileSync(srcPath, destPath);
38
+ }
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Run npm install in directory
44
+ */
45
+ function npmInstall(cwd) {
46
+ return new Promise((resolve, reject) => {
47
+ const child = spawn('npm', ['install'], {
48
+ cwd,
49
+ stdio: 'inherit',
50
+ shell: true
51
+ });
52
+
53
+ child.on('close', (code) => {
54
+ if (code === 0) {
55
+ resolve();
56
+ } else {
57
+ reject(new Error(`npm install failed with code ${code}`));
58
+ }
59
+ });
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Initialize git repository
65
+ */
66
+ function gitInit(cwd) {
67
+ return new Promise((resolve, reject) => {
68
+ const child = spawn('git', ['init'], {
69
+ cwd,
70
+ stdio: 'pipe',
71
+ shell: true
72
+ });
73
+
74
+ child.on('close', (code) => {
75
+ if (code === 0) {
76
+ resolve();
77
+ } else {
78
+ reject(new Error(`git init failed with code ${code}`));
79
+ }
80
+ });
81
+ });
82
+ }
83
+
84
+ export async function create(name, options = {}) {
85
+ const targetDir = path.resolve(process.cwd(), name);
86
+
87
+ // Check if directory already exists
88
+ if (fs.existsSync(targetDir)) {
89
+ console.error(`\n❌ Directory "${name}" already exists.\n`);
90
+ process.exit(1);
91
+ }
92
+
93
+ console.log(`\n🚀 Creating CourseCode project: ${name}\n`);
94
+
95
+ // Create project directory
96
+ fs.mkdirSync(targetDir, { recursive: true });
97
+
98
+ // Copy template files (course directory and vite config)
99
+ const templateDir = path.join(PACKAGE_ROOT, 'template');
100
+ console.log(' Copying template files...');
101
+ copyDir(templateDir, targetDir, {
102
+ exclude: [/^\.DS_Store$/, /^node_modules$/]
103
+ });
104
+
105
+ // Copy framework
106
+ const frameworkSrc = path.join(PACKAGE_ROOT, 'framework');
107
+ const frameworkDest = path.join(targetDir, 'framework');
108
+ console.log(' Copying framework...');
109
+ copyDir(frameworkSrc, frameworkDest, {
110
+ exclude: [/^\.DS_Store$/, /^dist$/]
111
+ });
112
+
113
+ // Copy SCORM schemas
114
+ const schemasSrc = path.join(PACKAGE_ROOT, 'schemas');
115
+ const schemasDest = path.join(targetDir, 'schemas');
116
+ console.log(' Copying LMS schemas...');
117
+ copyDir(schemasSrc, schemasDest);
118
+
119
+ // Copy manifest generation lib (used by vite.config.js)
120
+ const manifestSrc = path.join(PACKAGE_ROOT, 'lib', 'manifest');
121
+ const manifestDest = path.join(targetDir, 'lib', 'manifest');
122
+ console.log(' Copying manifest generators...');
123
+ copyDir(manifestSrc, manifestDest);
124
+
125
+ // Copy shared packaging utilities + proxy templates (used by vite.config.js)
126
+ const packagingSrc = path.join(PACKAGE_ROOT, 'lib', 'build-packaging.js');
127
+ const packagingDest = path.join(targetDir, 'lib', 'build-packaging.js');
128
+ fs.copyFileSync(packagingSrc, packagingDest);
129
+
130
+ const proxyTemplatesSrc = path.join(PACKAGE_ROOT, 'lib', 'proxy-templates');
131
+ const proxyTemplatesDest = path.join(targetDir, 'lib', 'proxy-templates');
132
+ console.log(' Copying proxy packaging templates...');
133
+ copyDir(proxyTemplatesSrc, proxyTemplatesDest);
134
+
135
+ // Read and customize package.json
136
+ const pkgPath = path.join(targetDir, 'package.json');
137
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
138
+ pkg.name = name;
139
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
140
+
141
+ // Create .coursecoderc.json to track framework version
142
+ const frameworkPkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
143
+ const coursecoderc = {
144
+ frameworkVersion: frameworkPkg.version,
145
+ createdAt: new Date().toISOString(),
146
+ createdWith: `coursecode@${frameworkPkg.version}`
147
+ };
148
+ fs.writeFileSync(
149
+ path.join(targetDir, '.coursecoderc.json'),
150
+ JSON.stringify(coursecoderc, null, 2)
151
+ );
152
+
153
+ console.log(' ✅ Project files created');
154
+
155
+ // If --blank, remove example files and reset config
156
+ if (options.blank) {
157
+ const { clean } = await import('./scaffold.js');
158
+ clean({ basePath: targetDir });
159
+ }
160
+
161
+ // Install dependencies
162
+ if (options.install !== false) {
163
+ console.log('\n Installing dependencies...\n');
164
+ try {
165
+ await npmInstall(targetDir);
166
+ console.log('\n ✅ Dependencies installed');
167
+ } catch (_error) {
168
+ console.warn('\n ⚠️ npm install failed. Run it manually.');
169
+ }
170
+ }
171
+
172
+ // Initialize git repository
173
+ console.log('\n Initializing git repository...');
174
+ try {
175
+ await gitInit(targetDir);
176
+ console.log(' ✅ Git repository initialized');
177
+ } catch (_error) {
178
+ console.warn(' ⚠️ Git init failed. You can run "git init" manually.');
179
+ }
180
+
181
+ // Print success message
182
+ if (options.blank) {
183
+ console.log(`
184
+ ✅ CourseCode project "${name}" created (blank starter)!
185
+
186
+ Course files:
187
+ - course/course-config.js - Course metadata & structure (minimal)
188
+ - course/slides/intro.js - Starter slide
189
+ - course/theme.css - Custom styling
190
+ - course/assets/ - Images, audio, etc.
191
+
192
+ Next steps:
193
+ - Edit course-config.js with your course metadata
194
+ - Create slides with: coursecode new slide <id>
195
+ - Create assessments with: coursecode new assessment <id>
196
+ `);
197
+ } else {
198
+ console.log(`
199
+ ✅ CourseCode project "${name}" created successfully!
200
+
201
+ Course files:
202
+ - course/course-config.js - Course metadata & structure
203
+ - course/slides/ - Slide content (example- files for reference)
204
+ - course/theme.css - Custom styling
205
+ - course/assets/ - Images, audio, etc.
206
+
207
+ The project includes example slides (prefixed with "example-") that
208
+ demonstrate framework features. When ready to build your own course:
209
+ - Run: coursecode clean
210
+ - Or create a blank project: coursecode create <name> --blank
211
+ `);
212
+ }
213
+
214
+ // Prompt to start dev server
215
+ const shouldStart = await promptYesNo(' Start the development server? (Y/n) ');
216
+
217
+ if (shouldStart) {
218
+ console.log('\n Starting development server...\n');
219
+ const child = spawn('npx', ['coursecode', 'dev'], {
220
+ cwd: targetDir,
221
+ stdio: 'inherit',
222
+ shell: true
223
+ });
224
+
225
+ child.on('error', () => {
226
+ console.warn(' ⚠️ Failed to start dev server. Run manually:');
227
+ console.log(` cd ${name} && coursecode dev\n`);
228
+ });
229
+ } else {
230
+ console.log(`
231
+ To start developing:
232
+
233
+ cd ${name}
234
+ coursecode dev
235
+ `);
236
+ }
237
+ }
238
+
239
+ function promptYesNo(question) {
240
+ return new Promise((resolve) => {
241
+ process.stdout.write(question);
242
+ process.stdin.setEncoding('utf8');
243
+ process.stdin.once('data', (data) => {
244
+ const answer = data.trim().toLowerCase();
245
+ resolve(answer === '' || answer === 'y' || answer === 'yes');
246
+ });
247
+ });
248
+ }