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