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.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/bin/cli.js +331 -0
- package/framework/assets/logo-coursecode-black.svg +14 -0
- package/framework/assets/logo-coursecode-white.svg +14 -0
- package/framework/assets/logo-coursecode.svg +14 -0
- package/framework/css/01-base.css +160 -0
- package/framework/css/02-layout.css +499 -0
- package/framework/css/accessibility.css +834 -0
- package/framework/css/components/accordions.css +710 -0
- package/framework/css/components/assessments.css +520 -0
- package/framework/css/components/audio-player.css +570 -0
- package/framework/css/components/badges.css +80 -0
- package/framework/css/components/breadcrumbs.css +87 -0
- package/framework/css/components/buttons.css +707 -0
- package/framework/css/components/callouts.css +1280 -0
- package/framework/css/components/cards.css +475 -0
- package/framework/css/components/carousel.css +193 -0
- package/framework/css/components/checkbox-group.css +123 -0
- package/framework/css/components/checklist.css +203 -0
- package/framework/css/components/collapse.css +96 -0
- package/framework/css/components/comparison.css +33 -0
- package/framework/css/components/content-image.css +36 -0
- package/framework/css/components/document-gallery.css +425 -0
- package/framework/css/components/dropdown.css +115 -0
- package/framework/css/components/embed-frame.css +142 -0
- package/framework/css/components/engagement.css +412 -0
- package/framework/css/components/features.css +35 -0
- package/framework/css/components/flip-cards.css +253 -0
- package/framework/css/components/footer.css +353 -0
- package/framework/css/components/forms.css +294 -0
- package/framework/css/components/hero.css +216 -0
- package/framework/css/components/images.css +528 -0
- package/framework/css/components/interactive-timeline.css +274 -0
- package/framework/css/components/intro-cards.css +30 -0
- package/framework/css/components/lightbox.css +666 -0
- package/framework/css/components/loading.css +65 -0
- package/framework/css/components/modals.css +235 -0
- package/framework/css/components/notifications.css +107 -0
- package/framework/css/components/quote.css +150 -0
- package/framework/css/components/sidebar.css +684 -0
- package/framework/css/components/slide-header.css +52 -0
- package/framework/css/components/spinner.css +62 -0
- package/framework/css/components/stats.css +44 -0
- package/framework/css/components/steps.css +232 -0
- package/framework/css/components/tables.css +90 -0
- package/framework/css/components/tabs.css +347 -0
- package/framework/css/components/timeline.css +154 -0
- package/framework/css/components/toggle.css +95 -0
- package/framework/css/components/tooltip.css +226 -0
- package/framework/css/components/video-player.css +438 -0
- package/framework/css/design-tokens.css +707 -0
- package/framework/css/framework.css +86 -0
- package/framework/css/interactions/accessibility.css +75 -0
- package/framework/css/interactions/base.css +92 -0
- package/framework/css/interactions/drag-drop.css +295 -0
- package/framework/css/interactions/fill-in-the-blank.css +236 -0
- package/framework/css/interactions/hotspots.css +69 -0
- package/framework/css/interactions/index.css +45 -0
- package/framework/css/interactions/interactive-image.css +359 -0
- package/framework/css/interactions/likert.css +126 -0
- package/framework/css/interactions/matching.css +354 -0
- package/framework/css/interactions/numeric-input.css +78 -0
- package/framework/css/interactions/sequencing.css +378 -0
- package/framework/css/interactions/true-false.css +177 -0
- package/framework/css/layouts/article.css +258 -0
- package/framework/css/layouts/base.css +30 -0
- package/framework/css/layouts/canvas.css +38 -0
- package/framework/css/layouts/focused.css +236 -0
- package/framework/css/layouts/index.css +29 -0
- package/framework/css/layouts/presentation.css +191 -0
- package/framework/css/layouts/traditional.css +52 -0
- package/framework/css/responsive.css +439 -0
- package/framework/css/utilities/accessibility-utils.css +59 -0
- package/framework/css/utilities/animations.css +419 -0
- package/framework/css/utilities/borders.css +72 -0
- package/framework/css/utilities/colors.css +76 -0
- package/framework/css/utilities/container.css +46 -0
- package/framework/css/utilities/decorative.css +442 -0
- package/framework/css/utilities/display.css +257 -0
- package/framework/css/utilities/flexbox.css +80 -0
- package/framework/css/utilities/grid.css +69 -0
- package/framework/css/utilities/icons.css +534 -0
- package/framework/css/utilities/lists.css +190 -0
- package/framework/css/utilities/spacing.css +167 -0
- package/framework/css/utilities/tables.css +81 -0
- package/framework/css/utilities/typography.css +159 -0
- package/framework/css/utilities/visibility.css +117 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
- package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
- package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
- package/framework/docs/DATA_MODEL.md +409 -0
- package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
- package/framework/docs/USER_GUIDE.md +583 -0
- package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
- package/framework/docs/examples/cloudflare-data-worker.js +102 -0
- package/framework/docs/examples/cloudflare-error-worker.js +228 -0
- package/framework/index.html +175 -0
- package/framework/js/app/AppActions.js +410 -0
- package/framework/js/app/AppState.js +225 -0
- package/framework/js/app/AppUI.js +616 -0
- package/framework/js/assessment/AssessmentActions.js +615 -0
- package/framework/js/assessment/AssessmentFactory.js +471 -0
- package/framework/js/assessment/AssessmentState.js +322 -0
- package/framework/js/assessment/AssessmentUI.js +451 -0
- package/framework/js/automation/api-engagement.js +196 -0
- package/framework/js/automation/api-interactions.js +167 -0
- package/framework/js/automation/api.js +242 -0
- package/framework/js/automation/index.js +41 -0
- package/framework/js/components/interactions/drag-drop.js +884 -0
- package/framework/js/components/interactions/fill-in.js +535 -0
- package/framework/js/components/interactions/hotspot.js +702 -0
- package/framework/js/components/interactions/interaction-base.js +511 -0
- package/framework/js/components/interactions/likert.js +301 -0
- package/framework/js/components/interactions/matching.js +699 -0
- package/framework/js/components/interactions/multiple-choice.js +377 -0
- package/framework/js/components/interactions/numeric.js +271 -0
- package/framework/js/components/interactions/sequencing.js +423 -0
- package/framework/js/components/interactions/true-false.js +241 -0
- package/framework/js/components/ui-components/accordion.js +442 -0
- package/framework/js/components/ui-components/alert.js +88 -0
- package/framework/js/components/ui-components/audio-player.js +1193 -0
- package/framework/js/components/ui-components/callout.js +121 -0
- package/framework/js/components/ui-components/carousel.js +145 -0
- package/framework/js/components/ui-components/checkbox-group.js +87 -0
- package/framework/js/components/ui-components/checklist.js +40 -0
- package/framework/js/components/ui-components/collapse.js +114 -0
- package/framework/js/components/ui-components/comparison.js +30 -0
- package/framework/js/components/ui-components/conditional-display.js +150 -0
- package/framework/js/components/ui-components/content-image.js +41 -0
- package/framework/js/components/ui-components/dropdown.js +262 -0
- package/framework/js/components/ui-components/embed-frame.js +274 -0
- package/framework/js/components/ui-components/features.js +33 -0
- package/framework/js/components/ui-components/flip-card.js +230 -0
- package/framework/js/components/ui-components/form-validator.js +76 -0
- package/framework/js/components/ui-components/hero.js +49 -0
- package/framework/js/components/ui-components/index.js +12 -0
- package/framework/js/components/ui-components/interactive-image.js +235 -0
- package/framework/js/components/ui-components/interactive-timeline.js +285 -0
- package/framework/js/components/ui-components/intro-cards.js +35 -0
- package/framework/js/components/ui-components/lightbox.js +652 -0
- package/framework/js/components/ui-components/modal.js +386 -0
- package/framework/js/components/ui-components/notifications.js +145 -0
- package/framework/js/components/ui-components/progress.js +88 -0
- package/framework/js/components/ui-components/quote.js +41 -0
- package/framework/js/components/ui-components/stats.js +33 -0
- package/framework/js/components/ui-components/steps.js +41 -0
- package/framework/js/components/ui-components/tabs.js +255 -0
- package/framework/js/components/ui-components/timeline.js +42 -0
- package/framework/js/components/ui-components/toggle-group.js +73 -0
- package/framework/js/components/ui-components/tooltip.js +458 -0
- package/framework/js/components/ui-components/value-display.js +133 -0
- package/framework/js/components/ui-components/video-player.js +686 -0
- package/framework/js/core/component-catalog.js +121 -0
- package/framework/js/core/event-bus.js +178 -0
- package/framework/js/core/interaction-catalog.js +149 -0
- package/framework/js/dev/runtime-linter.js +1725 -0
- package/framework/js/drivers/cmi5-driver.js +768 -0
- package/framework/js/drivers/driver-factory.js +77 -0
- package/framework/js/drivers/driver-interface.js +110 -0
- package/framework/js/drivers/http-driver-base.js +241 -0
- package/framework/js/drivers/lti-driver.js +508 -0
- package/framework/js/drivers/proxy-driver.js +444 -0
- package/framework/js/drivers/scorm-12-driver.js +560 -0
- package/framework/js/drivers/scorm-2004-driver.js +775 -0
- package/framework/js/drivers/scorm-driver-base.js +112 -0
- package/framework/js/engagement/engagement-manager.js +404 -0
- package/framework/js/engagement/engagement-progress.js +191 -0
- package/framework/js/engagement/engagement-trackers.js +215 -0
- package/framework/js/engagement/requirement-strategies.js +268 -0
- package/framework/js/main.js +727 -0
- package/framework/js/managers/accessibility-manager.js +499 -0
- package/framework/js/managers/assessment-manager.js +230 -0
- package/framework/js/managers/audio-manager.js +944 -0
- package/framework/js/managers/comment-manager.js +88 -0
- package/framework/js/managers/flag-manager.js +86 -0
- package/framework/js/managers/interaction-manager.js +254 -0
- package/framework/js/managers/interaction-registry.js +96 -0
- package/framework/js/managers/objective-manager.js +423 -0
- package/framework/js/managers/score-manager.js +441 -0
- package/framework/js/managers/video-manager.js +536 -0
- package/framework/js/navigation/Breadcrumbs.js +234 -0
- package/framework/js/navigation/NavigationActions.js +1132 -0
- package/framework/js/navigation/NavigationState.js +276 -0
- package/framework/js/navigation/NavigationUI.js +574 -0
- package/framework/js/navigation/document-gallery.js +357 -0
- package/framework/js/navigation/navigation-helpers.js +175 -0
- package/framework/js/navigation/navigation-validators.js +174 -0
- package/framework/js/state/index.js +8 -0
- package/framework/js/state/lms-connection.js +482 -0
- package/framework/js/state/lms-error-utils.js +58 -0
- package/framework/js/state/state-commits.js +200 -0
- package/framework/js/state/state-domains.js +86 -0
- package/framework/js/state/state-manager.js +502 -0
- package/framework/js/state/state-validation.js +311 -0
- package/framework/js/state/transaction-log.js +41 -0
- package/framework/js/state/xapi-statement-service.js +325 -0
- package/framework/js/utilities/access-control.js +99 -0
- package/framework/js/utilities/breakpoint-manager.js +315 -0
- package/framework/js/utilities/canvas-slide.js +35 -0
- package/framework/js/utilities/conditional-display.js +388 -0
- package/framework/js/utilities/course-channel.js +214 -0
- package/framework/js/utilities/course-helpers.js +420 -0
- package/framework/js/utilities/data-reporter.js +273 -0
- package/framework/js/utilities/error-reporter.js +313 -0
- package/framework/js/utilities/hotspot-helper.js +341 -0
- package/framework/js/utilities/icons.js +348 -0
- package/framework/js/utilities/logger.js +92 -0
- package/framework/js/utilities/markdown-renderer.js +45 -0
- package/framework/js/utilities/scroll-tracker.js +68 -0
- package/framework/js/utilities/ui-initializer.js +146 -0
- package/framework/js/utilities/utilities.js +293 -0
- package/framework/js/utilities/view-manager.js +227 -0
- package/framework/js/validation/html-validators.js +422 -0
- package/framework/js/validation/scorm-validators.js +438 -0
- package/framework/js/vendor/pipwerks.js +931 -0
- package/framework/scripts/generate-narration.js +629 -0
- package/framework/scripts/tts-providers/azure-provider.js +178 -0
- package/framework/scripts/tts-providers/base-provider.js +81 -0
- package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
- package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
- package/framework/scripts/tts-providers/google-provider.js +272 -0
- package/framework/scripts/tts-providers/index.js +158 -0
- package/framework/scripts/tts-providers/openai-provider.js +143 -0
- package/framework/version.json +63 -0
- package/lib/authoring-api.js +919 -0
- package/lib/build-linter.js +450 -0
- package/lib/build-packaging.js +186 -0
- package/lib/build.js +88 -0
- package/lib/cloud.js +691 -0
- package/lib/convert.js +341 -0
- package/lib/course-parser.js +936 -0
- package/lib/course-writer.js +258 -0
- package/lib/create.js +248 -0
- package/lib/css-index.js +237 -0
- package/lib/dev.js +51 -0
- package/lib/export-content.js +1246 -0
- package/lib/headless-browser.js +413 -0
- package/lib/import.js +377 -0
- package/lib/index.js +80 -0
- package/lib/info.js +79 -0
- package/lib/interaction-formatters.js +568 -0
- package/lib/manifest/cmi5-manifest.js +63 -0
- package/lib/manifest/lti-tool-config.js +53 -0
- package/lib/manifest/manifest-factory.js +99 -0
- package/lib/manifest/scorm-12-manifest.js +61 -0
- package/lib/manifest/scorm-2004-manifest.js +94 -0
- package/lib/manifest/scorm-proxy-manifest.js +104 -0
- package/lib/manifest-parser.js +96 -0
- package/lib/mcp-prompts.js +753 -0
- package/lib/mcp-server.js +316 -0
- package/lib/narration.js +53 -0
- package/lib/pdf-structure.js +142 -0
- package/lib/preview-export.js +231 -0
- package/lib/preview-routes-api.js +662 -0
- package/lib/preview-routes-editing.js +159 -0
- package/lib/preview-routes-lms.js +230 -0
- package/lib/preview-server.js +564 -0
- package/lib/project-utils.js +269 -0
- package/lib/proxy-templates/proxy.html +68 -0
- package/lib/proxy-templates/scorm-bridge.js +112 -0
- package/lib/scaffold.js +193 -0
- package/lib/schema-extractor.js +361 -0
- package/lib/slide-source-editor.js +586 -0
- package/lib/stub-player/app-viewer.js +195 -0
- package/lib/stub-player/app.js +370 -0
- package/lib/stub-player/catalog-panel.js +312 -0
- package/lib/stub-player/config-panel.js +1303 -0
- package/lib/stub-player/content-generator.js +586 -0
- package/lib/stub-player/content-viewer.js +173 -0
- package/lib/stub-player/debug-panel.js +420 -0
- package/lib/stub-player/edit-mode.js +922 -0
- package/lib/stub-player/edit-utils.js +400 -0
- package/lib/stub-player/header-bar.js +354 -0
- package/lib/stub-player/interaction-editor.js +210 -0
- package/lib/stub-player/interactions-panel.js +565 -0
- package/lib/stub-player/lms-api.js +1094 -0
- package/lib/stub-player/login-screen.js +74 -0
- package/lib/stub-player/outline-mode.js +689 -0
- package/lib/stub-player/styles/_assessments-panel.css +245 -0
- package/lib/stub-player/styles/_base.css +89 -0
- package/lib/stub-player/styles/_catalog-icons.css +96 -0
- package/lib/stub-player/styles/_catalog-panel.css +291 -0
- package/lib/stub-player/styles/_config-panel.css +636 -0
- package/lib/stub-player/styles/_content-viewer.css +834 -0
- package/lib/stub-player/styles/_debug-panel.css +576 -0
- package/lib/stub-player/styles/_edit-mode.css +128 -0
- package/lib/stub-player/styles/_header-bar.css +343 -0
- package/lib/stub-player/styles/_interaction-editor.css +140 -0
- package/lib/stub-player/styles/_interactions-panel.css +1038 -0
- package/lib/stub-player/styles/_login-screen.css +102 -0
- package/lib/stub-player/styles/_outline-mode.css +752 -0
- package/lib/stub-player/styles.css +15 -0
- package/lib/stub-player.js +160 -0
- package/lib/test-data-reporting.js +176 -0
- package/lib/test-error-reporting.js +146 -0
- package/lib/token.js +86 -0
- package/lib/upgrade.js +257 -0
- package/lib/validation-rules.js +517 -0
- package/lib/vite-plugin-content-discovery.js +296 -0
- package/package.json +108 -0
- package/schemas/XMLSchema.dtd +402 -0
- package/schemas/adlcp_v1p3.xsd +111 -0
- package/schemas/adlnav_v1p3.xsd +61 -0
- package/schemas/adlseq_v1p3.xsd +93 -0
- package/schemas/common/anyElement.xsd +27 -0
- package/schemas/common/dataTypes.xsd +138 -0
- package/schemas/common/elementNames.xsd +767 -0
- package/schemas/common/elementTypes.xsd +786 -0
- package/schemas/common/rootElement.xsd +31 -0
- package/schemas/common/vocabTypes.xsd +345 -0
- package/schemas/common/vocabValues.xsd +257 -0
- package/schemas/datatypes.dtd +203 -0
- package/schemas/ims_xml.xsd +35 -0
- package/schemas/imscp_v1p1.xsd +368 -0
- package/schemas/imsss_v1p0.xsd +67 -0
- package/schemas/imsss_v1p0auxresource.xsd +19 -0
- package/schemas/imsss_v1p0control.xsd +20 -0
- package/schemas/imsss_v1p0delivery.xsd +17 -0
- package/schemas/imsss_v1p0limit.xsd +47 -0
- package/schemas/imsss_v1p0objective.xsd +67 -0
- package/schemas/imsss_v1p0random.xsd +16 -0
- package/schemas/imsss_v1p0rollup.xsd +46 -0
- package/schemas/imsss_v1p0seqrule.xsd +108 -0
- package/schemas/imsss_v1p0util.xsd +94 -0
- package/schemas/license.txt +17 -0
- package/schemas/lom.xsd +102 -0
- package/schemas/lomCustom.xsd +62 -0
- package/schemas/lomLoose.xsd +62 -0
- package/schemas/lomStrict.xsd +62 -0
- package/schemas/xml.xsd +81 -0
- package/template/.env.example +92 -0
- package/template/course/assets/audio/example-intro.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
- package/template/course/assets/docs/example_md_1.md +39 -0
- package/template/course/assets/docs/example_md_2.md +41 -0
- package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
- package/template/course/assets/docs/example_pdf_2.pdf +0 -0
- package/template/course/assets/images/course-architecture.svg +36 -0
- package/template/course/assets/images/logo.svg +14 -0
- package/template/course/assets/widgets/counter-demo.html +190 -0
- package/template/course/assets/widgets/gravity-painter.html +384 -0
- package/template/course/course-config.js +539 -0
- package/template/course/icons.js +19 -0
- package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
- package/template/course/slides/example-course-structure.js +138 -0
- package/template/course/slides/example-final-exam.js +144 -0
- package/template/course/slides/example-finishing.js +127 -0
- package/template/course/slides/example-interactions-showcase.js +615 -0
- package/template/course/slides/example-preview-tour.js +129 -0
- package/template/course/slides/example-remedial.js +143 -0
- package/template/course/slides/example-summary.js +103 -0
- package/template/course/slides/example-ui-showcase.js +1805 -0
- package/template/course/slides/example-welcome.js +123 -0
- package/template/course/slides/example-workflow.js +140 -0
- package/template/course/theme.css +165 -0
- package/template/eslint.config.js +47 -0
- package/template/package.json +28 -0
- 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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
137
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
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
|
+
})();
|
package/lib/scaffold.js
ADDED
|
@@ -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
|
+
}
|