coursecode 0.1.57 → 0.1.58
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/README.md +8 -1
- package/bin/cli.js +16 -0
- package/framework/docs/FRAMEWORK_GUIDE.md +25 -7
- package/framework/docs/USER_GUIDE.md +14 -0
- package/framework/js/app/AppActions.js +17 -23
- package/framework/js/app/AppUI.js +7 -4
- package/framework/js/assessment/AssessmentActions.js +16 -15
- package/framework/js/assessment/AssessmentFactory.js +44 -15
- package/framework/js/automation/api-engagement.js +6 -3
- package/framework/js/components/ui-components/audio-player.js +30 -8
- package/framework/js/components/ui-components/collapse.js +4 -2
- package/framework/js/components/ui-components/dropdown.js +7 -3
- package/framework/js/components/ui-components/embed-frame.js +5 -1
- package/framework/js/components/ui-components/flip-card.js +12 -6
- package/framework/js/components/ui-components/interactive-image.js +28 -52
- package/framework/js/components/ui-components/lightbox.js +19 -5
- package/framework/js/components/ui-components/modal.js +20 -7
- package/framework/js/components/ui-components/notifications.js +8 -3
- package/framework/js/components/ui-components/video-player.js +46 -20
- package/framework/js/core/event-bus.js +31 -28
- package/framework/js/drivers/cmi5-driver.js +186 -83
- package/framework/js/drivers/driver-factory.js +7 -1
- package/framework/js/drivers/driver-interface.js +1 -1
- package/framework/js/drivers/http-driver-base.js +6 -0
- package/framework/js/drivers/lti-driver.js +112 -47
- package/framework/js/drivers/proxy-driver.js +347 -49
- package/framework/js/drivers/scorm-12-driver.js +213 -130
- package/framework/js/drivers/scorm-2004-driver.js +32 -19
- package/framework/js/drivers/scorm-driver-base.js +12 -0
- package/framework/js/drivers/standalone-driver.js +139 -0
- package/framework/js/engagement/engagement-trackers.js +38 -13
- package/framework/js/engagement/requirement-strategies.js +5 -1
- package/framework/js/main.js +7 -0
- package/framework/js/managers/assessment-manager.js +16 -1
- package/framework/js/managers/audio-manager.js +50 -16
- package/framework/js/managers/flag-manager.js +5 -1
- package/framework/js/managers/objective-manager.js +15 -7
- package/framework/js/managers/score-manager.js +27 -6
- package/framework/js/managers/video-manager.js +149 -70
- package/framework/js/navigation/NavigationActions.js +18 -4
- package/framework/js/navigation/document-gallery.js +7 -2
- package/framework/js/navigation/navigation-validators.js +3 -2
- package/framework/js/state/lms-connection.js +41 -16
- package/framework/js/state/state-commits.js +10 -5
- package/framework/js/state/state-manager.js +95 -9
- package/framework/js/state/state-validation.js +44 -11
- package/framework/js/utilities/course-helpers.js +3 -2
- package/framework/js/utilities/media-utils.js +28 -0
- package/framework/js/utilities/portable-assets.js +131 -0
- package/framework/js/utilities/ui-initializer.js +79 -5
- package/framework/js/utilities/utilities.js +32 -20
- package/framework/js/utilities/view-manager.js +16 -1
- package/framework/js/validation/scorm-validators.js +69 -0
- package/framework/version.json +2 -2
- package/lib/authoring-api.js +37 -18
- package/lib/build-packaging.js +47 -3
- package/lib/build.js +1 -1
- package/lib/cloud.js +40 -11
- package/lib/convert.js +30 -2
- package/lib/create.js +5 -3
- package/lib/dev.js +1 -1
- package/lib/headless-browser.js +66 -14
- package/lib/manifest/cmi5-manifest.js +26 -4
- package/lib/manifest/lti-tool-config.js +7 -2
- package/lib/manifest/manifest-factory.d.ts +1 -1
- package/lib/manifest/manifest-factory.js +3 -2
- package/lib/manifest/scorm-12-manifest.js +2 -2
- package/lib/manifest/scorm-2004-manifest.js +3 -28
- package/lib/manifest/scorm-proxy-manifest.js +37 -32
- package/lib/manifest/xml-utils.js +10 -0
- package/lib/narration.js +2 -2
- package/lib/portable-html.js +196 -0
- package/lib/preview-export.js +1 -1
- package/lib/preview-server.js +27 -6
- package/lib/project-utils.js +9 -1
- package/lib/proxy-templates/proxy.html +4 -1
- package/lib/proxy-templates/scorm-bridge.js +82 -10
- package/lib/scaffold.js +9 -4
- package/lib/stub-player/lms-api.js +63 -8
- package/package.json +5 -1
- package/schemas/adlcp_rootv1p2.xsd +110 -0
- package/schemas/coursecode_scorm12_package.xsd +5 -0
- package/schemas/coursecode_scorm2004_package.xsd +8 -0
- package/schemas/imscp_rootv1p1p2.xsd +304 -0
- package/template/vite.config.js +50 -22
|
@@ -44,10 +44,15 @@ export function deepClone(obj) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const clonedObj = {};
|
|
47
|
-
for (const key
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
for (const key of Object.keys(obj)) {
|
|
48
|
+
// defineProperty avoids invoking Object.prototype.__proto__ when state
|
|
49
|
+
// originated from JSON containing that literal key.
|
|
50
|
+
Object.defineProperty(clonedObj, key, {
|
|
51
|
+
value: deepClone(obj[key]),
|
|
52
|
+
enumerable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
writable: true
|
|
55
|
+
});
|
|
51
56
|
}
|
|
52
57
|
return clonedObj;
|
|
53
58
|
}
|
|
@@ -241,7 +246,7 @@ export function waitFor(condition, timeout = 5000, interval = 100) {
|
|
|
241
246
|
* @returns {Object} The merged target object
|
|
242
247
|
*/
|
|
243
248
|
export function deepMerge(target, ...sources) {
|
|
244
|
-
if (!
|
|
249
|
+
if (!isPlainObject(target)) {
|
|
245
250
|
throw new Error('deepMerge: target must be a plain object');
|
|
246
251
|
}
|
|
247
252
|
|
|
@@ -251,22 +256,23 @@ export function deepMerge(target, ...sources) {
|
|
|
251
256
|
|
|
252
257
|
const source = sources.shift();
|
|
253
258
|
|
|
254
|
-
if (
|
|
255
|
-
for (const key
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
} else {
|
|
267
|
-
// Direct assignment for primitives, arrays, and other types
|
|
268
|
-
target[key] = sourceValue;
|
|
259
|
+
if (isPlainObject(source)) {
|
|
260
|
+
for (const key of Object.keys(source)) {
|
|
261
|
+
if (key === '__proto__' || key === 'prototype' || key === 'constructor') {
|
|
262
|
+
throw new Error(`deepMerge: unsafe key "${key}" is not allowed`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const sourceValue = source[key];
|
|
266
|
+
const targetValue = target[key];
|
|
267
|
+
|
|
268
|
+
if (isPlainObject(sourceValue)) {
|
|
269
|
+
if (!isPlainObject(targetValue)) {
|
|
270
|
+
target[key] = {};
|
|
269
271
|
}
|
|
272
|
+
deepMerge(target[key], sourceValue);
|
|
273
|
+
} else {
|
|
274
|
+
// Direct assignment for primitives, arrays, and non-plain objects
|
|
275
|
+
target[key] = sourceValue;
|
|
270
276
|
}
|
|
271
277
|
}
|
|
272
278
|
}
|
|
@@ -274,6 +280,12 @@ export function deepMerge(target, ...sources) {
|
|
|
274
280
|
return deepMerge(target, ...sources);
|
|
275
281
|
}
|
|
276
282
|
|
|
283
|
+
function isPlainObject(value) {
|
|
284
|
+
if (value === null || typeof value !== 'object') return false;
|
|
285
|
+
const prototype = Object.getPrototypeOf(value);
|
|
286
|
+
return prototype === Object.prototype || prototype === null;
|
|
287
|
+
}
|
|
288
|
+
|
|
277
289
|
/**
|
|
278
290
|
* Shuffle an array using Fisher-Yates algorithm (creates new array).
|
|
279
291
|
* @param {Array} array - Array to shuffle
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { eventBus } from '../core/event-bus.js';
|
|
2
2
|
import { logger } from './logger.js';
|
|
3
3
|
import { validateRenderedHTML } from '../validation/html-validators.js';
|
|
4
|
-
import { initializeDeclarativeComponents } from './ui-initializer.js';
|
|
4
|
+
import { cleanupDeclarativeComponents, initializeDeclarativeComponents } from './ui-initializer.js';
|
|
5
5
|
import { courseConfig } from '../../../course/course-config.js';
|
|
6
|
+
import { rewritePortableAssetAttributes } from './portable-assets.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Creates a view manager for a given container element.
|
|
@@ -78,9 +79,20 @@ export function createViewManager(container, scope = 'local') {
|
|
|
78
79
|
// Auto-wrap content with content-width class if configured and not already wrapped
|
|
79
80
|
newElement = autoWrapContentIfNeeded(newElement, name);
|
|
80
81
|
|
|
82
|
+
// Convert copied course assets to embedded data URLs before components
|
|
83
|
+
// initialize and begin loading media in a portable HTML export.
|
|
84
|
+
rewritePortableAssetAttributes(newElement);
|
|
85
|
+
|
|
81
86
|
// Validate rendered HTML for common issues BEFORE adding to DOM
|
|
82
87
|
validateRenderedContent(newElement, name);
|
|
83
88
|
|
|
89
|
+
// Release component resources before removing the old view. Some
|
|
90
|
+
// components subscribe to document-level events or own media managers,
|
|
91
|
+
// so removing their DOM alone is not sufficient cleanup.
|
|
92
|
+
if (oldElement) {
|
|
93
|
+
cleanupDeclarativeComponents(oldElement);
|
|
94
|
+
}
|
|
95
|
+
|
|
84
96
|
// Clear container and add new view
|
|
85
97
|
container.innerHTML = '';
|
|
86
98
|
container.appendChild(newElement);
|
|
@@ -243,6 +255,9 @@ export function createViewManager(container, scope = 'local') {
|
|
|
243
255
|
if (currentElement && typeof currentView.onHide === 'function') {
|
|
244
256
|
currentView.onHide(currentElement);
|
|
245
257
|
}
|
|
258
|
+
if (currentElement) {
|
|
259
|
+
cleanupDeclarativeComponents(currentElement);
|
|
260
|
+
}
|
|
246
261
|
}
|
|
247
262
|
container.innerHTML = '';
|
|
248
263
|
currentViewName = null;
|
|
@@ -229,6 +229,75 @@ export function formatLearnerResponseForScorm(interactionType, response) {
|
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/** SCORM 1.2 interaction vocabulary (RTE 1.2.7). */
|
|
233
|
+
export const SCORM_12_INTERACTION_TYPES = [
|
|
234
|
+
'true-false',
|
|
235
|
+
'choice',
|
|
236
|
+
'fill-in',
|
|
237
|
+
'matching',
|
|
238
|
+
'performance',
|
|
239
|
+
'likert',
|
|
240
|
+
'sequencing',
|
|
241
|
+
'numeric'
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Converts framework/SCORM 2004 interaction data to the materially different
|
|
246
|
+
* SCORM 1.2 vocabulary and response syntax.
|
|
247
|
+
*
|
|
248
|
+
* The framework keeps one lossless internal representation. Unsupported 2004
|
|
249
|
+
* types are represented as fill-in rows in the optional native 1.2 interaction
|
|
250
|
+
* array; the original record remains intact in suspend_data.
|
|
251
|
+
*/
|
|
252
|
+
export function serializeInteractionForScorm12(interaction) {
|
|
253
|
+
const type = SCORM_12_INTERACTION_TYPES.includes(interaction.type)
|
|
254
|
+
? interaction.type
|
|
255
|
+
: 'fill-in';
|
|
256
|
+
|
|
257
|
+
const convertResponse = value => {
|
|
258
|
+
if (value === null || value === undefined) return '';
|
|
259
|
+
let serialized = String(value);
|
|
260
|
+
|
|
261
|
+
if (interaction.type === 'true-false') {
|
|
262
|
+
const normalized = serialized.toLowerCase().trim();
|
|
263
|
+
if (normalized === 'true' || normalized === 't' || normalized === '1') return 't';
|
|
264
|
+
if (normalized === 'false' || normalized === 'f' || normalized === '0') return 'f';
|
|
265
|
+
return '';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (interaction.type === 'choice' || interaction.type === 'sequencing') {
|
|
269
|
+
serialized = serialized.replaceAll('[,]', ',');
|
|
270
|
+
} else if (interaction.type === 'matching') {
|
|
271
|
+
serialized = serialized.replaceAll('[.]', '.').replaceAll('[,]', ',');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// CMIString values in SCORM 1.2 are ASCII. Escape non-ASCII code units
|
|
275
|
+
// instead of submitting an invalid value that strict LMSs reject.
|
|
276
|
+
return serialized.replace(/[^\x00-\x7F]/g, char =>
|
|
277
|
+
`\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`
|
|
278
|
+
);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const result = interaction.result === 'incorrect'
|
|
282
|
+
? 'wrong'
|
|
283
|
+
: interaction.result;
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
...interaction,
|
|
287
|
+
id: convertResponse(interaction.id),
|
|
288
|
+
type,
|
|
289
|
+
learner_response: convertResponse(interaction.learner_response),
|
|
290
|
+
result,
|
|
291
|
+
correct_responses: interaction.correct_responses?.map(item => {
|
|
292
|
+
if (typeof item === 'object' && item !== null && 'pattern' in item) {
|
|
293
|
+
return { ...item, pattern: convertResponse(item.pattern) };
|
|
294
|
+
}
|
|
295
|
+
return convertResponse(item);
|
|
296
|
+
}),
|
|
297
|
+
objectives: interaction.objectives?.map(convertResponse)
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
232
301
|
/**
|
|
233
302
|
* SCORM 2004 4th Edition valid completion status values.
|
|
234
303
|
* @constant {string[]}
|
package/framework/version.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.1.
|
|
2
|
+
"version": "0.1.58",
|
|
3
3
|
"name": "CourseCode Framework",
|
|
4
4
|
"description": "Multi-format course authoring and learner runtime framework",
|
|
5
|
-
"released": "2026-07-
|
|
5
|
+
"released": "2026-07-13",
|
|
6
6
|
"formats": [
|
|
7
7
|
"SCORM 2004 4th Edition",
|
|
8
8
|
"SCORM 1.2",
|
package/lib/authoring-api.js
CHANGED
|
@@ -965,7 +965,32 @@ export async function getWorkflowStatus(port = 4173) {
|
|
|
965
965
|
// Infer stage
|
|
966
966
|
let stage, stageNumber, nextAction, recommendedTool;
|
|
967
967
|
|
|
968
|
-
|
|
968
|
+
const isPowerPointImport = checklist.source === 'powerpoint-import';
|
|
969
|
+
const needsReferenceConversion = checklist.hasRawRefs && !checklist.hasConvertedRefs && !isPowerPointImport;
|
|
970
|
+
const needsOutline = checklist.hasConvertedRefs && !checklist.hasOutline && !isPowerPointImport;
|
|
971
|
+
const hasNoAuthoringInputs = !checklist.hasRawRefs && !checklist.hasConvertedRefs && !checklist.hasOutline && !isPowerPointImport;
|
|
972
|
+
|
|
973
|
+
if (needsReferenceConversion) {
|
|
974
|
+
stage = 'source-ingestion';
|
|
975
|
+
stageNumber = 1;
|
|
976
|
+
nextAction = 'Convert reference files to markdown: coursecode convert';
|
|
977
|
+
recommendedTool = 'coursecode_workflow_status';
|
|
978
|
+
} else if (needsOutline) {
|
|
979
|
+
stage = 'outline-creation';
|
|
980
|
+
stageNumber = 2;
|
|
981
|
+
nextAction = 'Create course outline from reference materials. Stage instructions have all file paths.';
|
|
982
|
+
recommendedTool = 'coursecode_workflow_status';
|
|
983
|
+
} else if (hasNoAuthoringInputs) {
|
|
984
|
+
stage = 'source-ingestion';
|
|
985
|
+
stageNumber = 1;
|
|
986
|
+
nextAction = 'Add reference files (PDF, DOCX, PPTX, MD) to course/references/ and run coursecode convert';
|
|
987
|
+
recommendedTool = 'coursecode_workflow_status';
|
|
988
|
+
} else if (checklist.hasOutline && (!checklist.hasSlides || !checklist.hasCourseConfig)) {
|
|
989
|
+
stage = 'course-building';
|
|
990
|
+
stageNumber = 3;
|
|
991
|
+
nextAction = 'Build slide files and course-config.js based on the outline. Stage instructions have all file paths.';
|
|
992
|
+
recommendedTool = 'coursecode_workflow_status';
|
|
993
|
+
} else if (checklist.hasSlides && checklist.hasCourseConfig) {
|
|
969
994
|
// Course is built — run lint to decide polish vs export
|
|
970
995
|
let lintPassed = false;
|
|
971
996
|
try {
|
|
@@ -993,21 +1018,6 @@ export async function getWorkflowStatus(port = 4173) {
|
|
|
993
1018
|
}
|
|
994
1019
|
recommendedTool = 'coursecode_lint';
|
|
995
1020
|
}
|
|
996
|
-
} else if (!checklist.hasRawRefs && !checklist.hasConvertedRefs) {
|
|
997
|
-
stage = 'source-ingestion';
|
|
998
|
-
stageNumber = 1;
|
|
999
|
-
nextAction = 'Add reference files (PDF, DOCX, PPTX, MD) to course/references/ and run coursecode convert';
|
|
1000
|
-
recommendedTool = 'coursecode_workflow_status';
|
|
1001
|
-
} else if (checklist.hasRawRefs && !checklist.hasConvertedRefs) {
|
|
1002
|
-
stage = 'source-ingestion';
|
|
1003
|
-
stageNumber = 1;
|
|
1004
|
-
nextAction = 'Convert reference files to markdown: coursecode convert';
|
|
1005
|
-
recommendedTool = 'coursecode_workflow_status';
|
|
1006
|
-
} else if (!checklist.hasOutline) {
|
|
1007
|
-
stage = 'outline-creation';
|
|
1008
|
-
stageNumber = 2;
|
|
1009
|
-
nextAction = 'Create course outline from reference materials. Stage instructions have all file paths.';
|
|
1010
|
-
recommendedTool = 'coursecode_workflow_status';
|
|
1011
1021
|
} else {
|
|
1012
1022
|
stage = 'course-building';
|
|
1013
1023
|
stageNumber = 3;
|
|
@@ -1050,7 +1060,7 @@ export async function buildCourse(options = {}) {
|
|
|
1050
1060
|
const child = spawn('npx', ['vite', 'build'], {
|
|
1051
1061
|
cwd: courseRoot,
|
|
1052
1062
|
env,
|
|
1053
|
-
shell:
|
|
1063
|
+
shell: process.platform === 'win32',
|
|
1054
1064
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
1055
1065
|
});
|
|
1056
1066
|
|
|
@@ -1059,6 +1069,16 @@ export async function buildCourse(options = {}) {
|
|
|
1059
1069
|
|
|
1060
1070
|
child.stdout.on('data', (data) => { _stdout += data.toString(); });
|
|
1061
1071
|
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
1072
|
+
child.on('error', (error) => {
|
|
1073
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
|
|
1074
|
+
resolve({
|
|
1075
|
+
success: false,
|
|
1076
|
+
error: error.message,
|
|
1077
|
+
errors: [error.message],
|
|
1078
|
+
warnings: [],
|
|
1079
|
+
duration
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1062
1082
|
|
|
1063
1083
|
child.on('close', (code) => {
|
|
1064
1084
|
const duration = ((Date.now() - startTime) / 1000).toFixed(1) + 's';
|
|
@@ -1235,4 +1255,3 @@ export async function generateNarration(options = {}) {
|
|
|
1235
1255
|
});
|
|
1236
1256
|
});
|
|
1237
1257
|
}
|
|
1238
|
-
|
package/lib/build-packaging.js
CHANGED
|
@@ -28,8 +28,30 @@ function sanitizeTitle(title) {
|
|
|
28
28
|
|
|
29
29
|
function withClientCredentials(externalUrl, clientId, token) {
|
|
30
30
|
if (!clientId || !token) return externalUrl;
|
|
31
|
-
const
|
|
32
|
-
|
|
31
|
+
const url = new URL(externalUrl);
|
|
32
|
+
url.searchParams.set('clientId', clientId);
|
|
33
|
+
url.searchParams.set('token', token);
|
|
34
|
+
return url.toString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validateExternalUrl(value) {
|
|
38
|
+
let url;
|
|
39
|
+
try {
|
|
40
|
+
url = new URL(value);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(`externalUrl must be an absolute URL, received: ${value}`);
|
|
43
|
+
}
|
|
44
|
+
const localHttp = url.protocol === 'http:' && ['localhost', '127.0.0.1', '::1'].includes(url.hostname);
|
|
45
|
+
if (url.protocol !== 'https:' && !localHttp) {
|
|
46
|
+
throw new Error('externalUrl must use HTTPS (HTTP is allowed only for local development)');
|
|
47
|
+
}
|
|
48
|
+
if (url.hash) {
|
|
49
|
+
throw new Error('externalUrl must not contain a URL fragment because launch credentials would not reach the server');
|
|
50
|
+
}
|
|
51
|
+
if (url.username || url.password) {
|
|
52
|
+
throw new Error('externalUrl must not embed HTTP credentials');
|
|
53
|
+
}
|
|
54
|
+
return url;
|
|
33
55
|
}
|
|
34
56
|
|
|
35
57
|
function zipDirectory(sourceDir, zipFilePath) {
|
|
@@ -55,6 +77,7 @@ export function validateExternalHostingConfig(config) {
|
|
|
55
77
|
if (!config.externalUrl) {
|
|
56
78
|
throw new Error(`${config.lmsFormat} format requires 'externalUrl' in course-config.js`);
|
|
57
79
|
}
|
|
80
|
+
validateExternalUrl(config.externalUrl);
|
|
58
81
|
|
|
59
82
|
if (config.accessControl?.enforcement !== 'server') {
|
|
60
83
|
throw new Error(`${config.lmsFormat} requires accessControl.enforcement = 'server'. Browser-only token checks are not secure.`);
|
|
@@ -164,6 +187,10 @@ export async function createProxyPackage({ rootDir, config, clientId = null, tok
|
|
|
164
187
|
|
|
165
188
|
let proxyHtml = fs.readFileSync(path.join(templatesDir, 'proxy.html'), 'utf-8');
|
|
166
189
|
proxyHtml = proxyHtml.replace('{{EXTERNAL_URL_JSON}}', JSON.stringify(externalUrl));
|
|
190
|
+
proxyHtml = proxyHtml.replace(
|
|
191
|
+
'{{BASE_FORMAT_JSON}}',
|
|
192
|
+
JSON.stringify(config.lmsFormat === 'scorm1.2-proxy' ? 'scorm1.2' : 'scorm2004')
|
|
193
|
+
);
|
|
167
194
|
fs.writeFileSync(path.join(proxyDir, 'proxy.html'), proxyHtml);
|
|
168
195
|
|
|
169
196
|
fs.copyFileSync(path.join(templatesDir, 'scorm-bridge.js'), path.join(proxyDir, 'scorm-bridge.js'));
|
|
@@ -172,6 +199,22 @@ export async function createProxyPackage({ rootDir, config, clientId = null, tok
|
|
|
172
199
|
const { filename, content } = generateManifest(config.lmsFormat, config, [], { externalUrl: config.externalUrl });
|
|
173
200
|
fs.writeFileSync(path.join(proxyDir, filename), content);
|
|
174
201
|
|
|
202
|
+
const schemasDir = path.join(PACKAGE_ROOT, 'schemas');
|
|
203
|
+
for (const entry of fs.readdirSync(schemasDir, { withFileTypes: true })) {
|
|
204
|
+
if (entry.isFile() && /\.(?:xsd|dtd|xml)$/i.test(entry.name)) {
|
|
205
|
+
fs.copyFileSync(path.join(schemasDir, entry.name), path.join(proxyDir, entry.name));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
fs.cpSync(path.join(schemasDir, 'common'), path.join(proxyDir, 'common'), { recursive: true });
|
|
209
|
+
|
|
210
|
+
const required = config.lmsFormat === 'scorm1.2-proxy'
|
|
211
|
+
? ['imsmanifest.xml', 'proxy.html', 'scorm-bridge.js', 'pipwerks.js', 'imscp_rootv1p1p2.xsd', 'adlcp_rootv1p2.xsd', 'ims_xml.xsd']
|
|
212
|
+
: ['imsmanifest.xml', 'proxy.html', 'scorm-bridge.js', 'pipwerks.js', 'imscp_v1p1.xsd', 'adlcp_v1p3.xsd', 'imsss_v1p0.xsd'];
|
|
213
|
+
const missing = required.filter(file => !fs.existsSync(path.join(proxyDir, file)));
|
|
214
|
+
if (missing.length > 0 || proxyHtml.includes('{{')) {
|
|
215
|
+
throw new Error(`Proxy package validation failed: ${missing.length ? `missing ${missing.join(', ')}` : 'unresolved template placeholder'}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
175
218
|
if (fs.existsSync(zipFilePath)) fs.unlinkSync(zipFilePath);
|
|
176
219
|
const bytes = await zipDirectory(proxyDir, zipFilePath);
|
|
177
220
|
const sizeKB = (bytes / 1024).toFixed(1);
|
|
@@ -206,7 +249,8 @@ export async function createRemotePackage({ rootDir, config, clientId = null, to
|
|
|
206
249
|
const bytes = await zipDirectory(remoteDir, zipFilePath);
|
|
207
250
|
const sizeKB = (bytes / 1024).toFixed(1);
|
|
208
251
|
console.warn(`📦 Created ${zipFileName} (${sizeKB} KB) - Upload to LMS`);
|
|
209
|
-
|
|
252
|
+
const auUrl = content.match(/<url>([^<]+)<\/url>/)?.[1] || externalUrl;
|
|
253
|
+
console.warn(` AU URL points to: ${auUrl}`);
|
|
210
254
|
return zipFilePath;
|
|
211
255
|
} finally {
|
|
212
256
|
if (fs.existsSync(remoteDir)) fs.rmSync(remoteDir, { recursive: true });
|
package/lib/build.js
CHANGED
package/lib/cloud.js
CHANGED
|
@@ -11,7 +11,7 @@ import crypto from 'crypto';
|
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import os from 'os';
|
|
14
|
-
import {
|
|
14
|
+
import { execFile } from 'child_process';
|
|
15
15
|
import readline from 'readline';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
17
|
|
|
@@ -427,10 +427,23 @@ function handleResponseError(status, body) {
|
|
|
427
427
|
*/
|
|
428
428
|
function openBrowser(url) {
|
|
429
429
|
const platform = process.platform;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
430
|
+
let command;
|
|
431
|
+
let args;
|
|
432
|
+
if (platform === 'darwin') {
|
|
433
|
+
command = 'open';
|
|
434
|
+
args = [url];
|
|
435
|
+
} else if (platform === 'win32') {
|
|
436
|
+
command = 'rundll32';
|
|
437
|
+
args = ['url.dll,FileProtocolHandler', url];
|
|
438
|
+
} else {
|
|
439
|
+
command = 'xdg-open';
|
|
440
|
+
args = [url];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const child = execFile(command, args);
|
|
444
|
+
child.on('error', (error) => {
|
|
445
|
+
console.warn(` ⚠ Could not open the browser automatically: ${error.message}`);
|
|
446
|
+
});
|
|
434
447
|
}
|
|
435
448
|
|
|
436
449
|
/**
|
|
@@ -987,9 +1000,17 @@ export async function deploy(options = {}) {
|
|
|
987
1000
|
// Reconcile local sourceType with cloud truth (handles unlink-via-dashboard)
|
|
988
1001
|
try {
|
|
989
1002
|
const statusData = JSON.parse(await statusRes.text());
|
|
990
|
-
const
|
|
1003
|
+
const serverGithubRepo = statusData.source?.githubRepo || statusData.github_repo;
|
|
991
1004
|
const localRc = readRcConfig();
|
|
992
|
-
if (
|
|
1005
|
+
if (serverGithubRepo && (
|
|
1006
|
+
localRc?.sourceType !== 'github' || localRc?.githubRepo !== serverGithubRepo
|
|
1007
|
+
)) {
|
|
1008
|
+
updateRcConfig((rc) => {
|
|
1009
|
+
rc.sourceType = 'github';
|
|
1010
|
+
rc.githubRepo = serverGithubRepo;
|
|
1011
|
+
return rc;
|
|
1012
|
+
});
|
|
1013
|
+
} else if (localRc?.sourceType === 'github' && !serverGithubRepo) {
|
|
993
1014
|
updateRcConfig((rc) => {
|
|
994
1015
|
delete rc.sourceType;
|
|
995
1016
|
delete rc.githubRepo;
|
|
@@ -1508,8 +1529,16 @@ export async function status(options = {}) {
|
|
|
1508
1529
|
|
|
1509
1530
|
// Reconcile local sourceType with cloud truth (handles unlink-via-dashboard)
|
|
1510
1531
|
const localRc = readRcConfig();
|
|
1511
|
-
const
|
|
1512
|
-
if (
|
|
1532
|
+
const serverGithubRepo = data.source?.githubRepo || data.github_repo;
|
|
1533
|
+
if (serverGithubRepo && (
|
|
1534
|
+
localRc?.sourceType !== 'github' || localRc?.githubRepo !== serverGithubRepo
|
|
1535
|
+
)) {
|
|
1536
|
+
updateRcConfig((rc) => {
|
|
1537
|
+
rc.sourceType = 'github';
|
|
1538
|
+
rc.githubRepo = serverGithubRepo;
|
|
1539
|
+
return rc;
|
|
1540
|
+
});
|
|
1541
|
+
} else if (localRc?.sourceType === 'github' && !serverGithubRepo) {
|
|
1513
1542
|
updateRcConfig((rc) => {
|
|
1514
1543
|
delete rc.sourceType;
|
|
1515
1544
|
delete rc.githubRepo;
|
|
@@ -1527,9 +1556,9 @@ export async function status(options = {}) {
|
|
|
1527
1556
|
|
|
1528
1557
|
console.log(`\n${data.slug} — ${data.name} (${data.orgName})\n`);
|
|
1529
1558
|
|
|
1530
|
-
const sourceType = data.source?.type || data.source_type;
|
|
1531
1559
|
const githubRepo = data.source?.githubRepo || data.github_repo;
|
|
1532
|
-
|
|
1560
|
+
const sourceType = data.source?.type || data.source_type;
|
|
1561
|
+
if (githubRepo) {
|
|
1533
1562
|
console.log(`Source: GitHub — ${githubRepo}`);
|
|
1534
1563
|
console.log(' production=github | preview=cli+github');
|
|
1535
1564
|
} else if (sourceType) {
|
package/lib/convert.js
CHANGED
|
@@ -238,13 +238,41 @@ async function convertFile(filePath) {
|
|
|
238
238
|
async function convertDocx(filePath) {
|
|
239
239
|
const mammoth = await import('mammoth');
|
|
240
240
|
const result = await mammoth.convertToMarkdown({ path: filePath });
|
|
241
|
+
const sanitized = replaceEmbeddedDataImages(result.value);
|
|
242
|
+
const warnings = result.messages.filter(m => m.type === 'warning');
|
|
243
|
+
|
|
244
|
+
if (sanitized.omittedImageCount > 0) {
|
|
245
|
+
warnings.push({
|
|
246
|
+
type: 'warning',
|
|
247
|
+
message: `Omitted ${sanitized.omittedImageCount} embedded image payload(s). Review the source document for visual content.`
|
|
248
|
+
});
|
|
249
|
+
}
|
|
241
250
|
|
|
242
251
|
return {
|
|
243
|
-
markdown:
|
|
244
|
-
warnings
|
|
252
|
+
markdown: sanitized.markdown,
|
|
253
|
+
warnings
|
|
245
254
|
};
|
|
246
255
|
}
|
|
247
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Replace mammoth's inline base64 image output with compact review markers.
|
|
259
|
+
* Reference Markdown is consumed by AI authoring tools, where multi-megabyte
|
|
260
|
+
* data URIs waste context and do not preserve useful visual meaning.
|
|
261
|
+
*/
|
|
262
|
+
export function replaceEmbeddedDataImages(markdown) {
|
|
263
|
+
let omittedImageCount = 0;
|
|
264
|
+
const sanitized = markdown.replace(
|
|
265
|
+
/!\[([^\]]*)\]\(data:image\/[^;]+;base64,[^)]+\)/gi,
|
|
266
|
+
(_match, altText) => {
|
|
267
|
+
omittedImageCount++;
|
|
268
|
+
const label = altText?.trim() ? `: ${altText.trim()}` : '';
|
|
269
|
+
return `*[Embedded image omitted${label}. Review the source document for visual content.]*`;
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return { markdown: sanitized, omittedImageCount };
|
|
274
|
+
}
|
|
275
|
+
|
|
248
276
|
/**
|
|
249
277
|
* Convert PPTX to markdown using node-pptx-parser
|
|
250
278
|
*/
|
package/lib/create.js
CHANGED
|
@@ -172,7 +172,7 @@ function npmInstall(cwd) {
|
|
|
172
172
|
const child = spawn(command, args, {
|
|
173
173
|
cwd,
|
|
174
174
|
stdio: 'inherit',
|
|
175
|
-
shell: !npmCli
|
|
175
|
+
shell: !npmCli && process.platform === 'win32'
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
child.on('close', (code) => {
|
|
@@ -182,6 +182,7 @@ function npmInstall(cwd) {
|
|
|
182
182
|
reject(new Error(`npm install failed with code ${code}`));
|
|
183
183
|
}
|
|
184
184
|
});
|
|
185
|
+
child.on('error', reject);
|
|
185
186
|
});
|
|
186
187
|
}
|
|
187
188
|
|
|
@@ -193,7 +194,7 @@ function gitInit(cwd) {
|
|
|
193
194
|
const child = spawn('git', ['init'], {
|
|
194
195
|
cwd,
|
|
195
196
|
stdio: 'pipe',
|
|
196
|
-
shell:
|
|
197
|
+
shell: false
|
|
197
198
|
});
|
|
198
199
|
|
|
199
200
|
child.on('close', (code) => {
|
|
@@ -203,6 +204,7 @@ function gitInit(cwd) {
|
|
|
203
204
|
reject(new Error(`git init failed with code ${code}`));
|
|
204
205
|
}
|
|
205
206
|
});
|
|
207
|
+
child.on('error', reject);
|
|
206
208
|
});
|
|
207
209
|
}
|
|
208
210
|
|
|
@@ -384,7 +386,7 @@ export async function create(name, options = {}) {
|
|
|
384
386
|
const child = spawn(command, args, {
|
|
385
387
|
cwd: targetDir,
|
|
386
388
|
stdio: 'inherit',
|
|
387
|
-
shell: !npxCli
|
|
389
|
+
shell: !npxCli && process.platform === 'win32'
|
|
388
390
|
});
|
|
389
391
|
|
|
390
392
|
child.on('error', () => {
|
package/lib/dev.js
CHANGED