@patch-adams/core 1.0.5 → 1.0.7

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/dist/cli.cjs ADDED
@@ -0,0 +1,1121 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var commander = require('commander');
5
+ var fs = require('fs');
6
+ var path = require('path');
7
+ var chalk = require('chalk');
8
+ var ora = require('ora');
9
+ var AdmZip = require('adm-zip');
10
+ var fastXmlParser = require('fast-xml-parser');
11
+ var zod = require('zod');
12
+ var url = require('url');
13
+
14
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
15
+
16
+ var chalk__default = /*#__PURE__*/_interopDefault(chalk);
17
+ var ora__default = /*#__PURE__*/_interopDefault(ora);
18
+ var AdmZip__default = /*#__PURE__*/_interopDefault(AdmZip);
19
+
20
+ var __defProp = Object.defineProperty;
21
+ var __getOwnPropNames = Object.getOwnPropertyNames;
22
+ var __esm = (fn, res) => function __init() {
23
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
24
+ };
25
+ var __export = (target, all) => {
26
+ for (var name in all)
27
+ __defProp(target, name, { get: all[name], enumerable: true });
28
+ };
29
+
30
+ // src/detectors/format-detector.ts
31
+ var FormatDetector;
32
+ var init_format_detector = __esm({
33
+ "src/detectors/format-detector.ts"() {
34
+ FormatDetector = class {
35
+ /**
36
+ * Detect the package format from a ZIP file
37
+ */
38
+ detect(zip) {
39
+ if (zip.getEntry("cmi5.xml")) {
40
+ return "cmi5";
41
+ }
42
+ if (zip.getEntry("tincan.xml")) {
43
+ return "xapi";
44
+ }
45
+ const manifest = zip.getEntry("imsmanifest.xml");
46
+ if (manifest) {
47
+ const content = manifest.getData().toString("utf-8");
48
+ return this.detectScormVersion(content);
49
+ }
50
+ if (this.hasAiccFiles(zip)) {
51
+ return "aicc";
52
+ }
53
+ return "unknown";
54
+ }
55
+ /**
56
+ * Detect SCORM version from manifest content
57
+ */
58
+ detectScormVersion(content) {
59
+ if (content.includes("4th Edition") || content.includes("CAM 1.4")) {
60
+ return "scorm2004-4";
61
+ }
62
+ if (content.includes("3rd Edition") || content.includes("2004 3rd") || content.includes("adlcp_v1p3")) {
63
+ return "scorm2004-3";
64
+ }
65
+ if (content.includes("2004") && content.includes("adlseq")) {
66
+ return "scorm2004-3";
67
+ }
68
+ if (content.includes("1.2") || content.includes("adlcp_rootv1p2") || content.includes("imscp_rootv1p1p2")) {
69
+ return "scorm12";
70
+ }
71
+ if (content.toLowerCase().includes("aicc")) {
72
+ return "aicc";
73
+ }
74
+ return "scorm12";
75
+ }
76
+ /**
77
+ * Check for AICC-specific files
78
+ */
79
+ hasAiccFiles(zip) {
80
+ const aiccExtensions = [".crs", ".au", ".des", ".cst"];
81
+ const entries = zip.getEntries();
82
+ for (const entry of entries) {
83
+ const name = entry.entryName.toLowerCase();
84
+ if (aiccExtensions.some((ext) => name.endsWith(ext))) {
85
+ return true;
86
+ }
87
+ }
88
+ return false;
89
+ }
90
+ /**
91
+ * Get a human-readable format name
92
+ */
93
+ getFormatDisplayName(format) {
94
+ const names = {
95
+ scorm12: "SCORM 1.2",
96
+ "scorm2004-3": "SCORM 2004 3rd Edition",
97
+ "scorm2004-4": "SCORM 2004 4th Edition",
98
+ cmi5: "cmi5",
99
+ xapi: "xAPI (Tin Can)",
100
+ aicc: "AICC",
101
+ unknown: "Unknown"
102
+ };
103
+ return names[format];
104
+ }
105
+ };
106
+ }
107
+ });
108
+
109
+ // src/detectors/index.ts
110
+ var detectors_exports = {};
111
+ __export(detectors_exports, {
112
+ FormatDetector: () => FormatDetector
113
+ });
114
+ var init_detectors = __esm({
115
+ "src/detectors/index.ts"() {
116
+ init_format_detector();
117
+ }
118
+ });
119
+
120
+ // src/templates/css-before.ts
121
+ function generateCssBeforeLoader(options) {
122
+ const { remoteUrl, localPath, htmlClass, loadingClass } = options;
123
+ return `<!-- === PATCH-ADAMS: CSS BEFORE (blocking) === -->
124
+ <style data-pa="css-before-critical">
125
+ /* Critical: Hide content until ready */
126
+ html.${htmlClass}.${loadingClass} body {
127
+ visibility: hidden !important;
128
+ }
129
+ </style>
130
+ <script data-pa="css-before-loader">
131
+ (function() {
132
+ 'use strict';
133
+ var REMOTE_URL = "${remoteUrl}";
134
+ var LOCAL_PATH = "${localPath}";
135
+
136
+ function loadCSSSync(url) {
137
+ var link = document.createElement('link');
138
+ link.rel = 'stylesheet';
139
+ link.href = url;
140
+ link.setAttribute('data-pa', 'css-before');
141
+ document.head.appendChild(link);
142
+ return link;
143
+ }
144
+
145
+ // Try remote first
146
+ var link = loadCSSSync(REMOTE_URL);
147
+
148
+ link.onerror = function() {
149
+ console.warn('[Patch-Adams] CSS before failed to load from remote, using local fallback');
150
+ document.head.removeChild(link);
151
+ loadCSSSync(LOCAL_PATH);
152
+ };
153
+
154
+ link.onload = function() {
155
+ console.log('[Patch-Adams] CSS before loaded from remote:', REMOTE_URL);
156
+ };
157
+ })();
158
+ </script>`;
159
+ }
160
+ function buildCssBeforeOptions(config) {
161
+ return {
162
+ remoteUrl: `${config.remoteDomain}/${config.cssBefore.filename}`,
163
+ localPath: `${config.localFolders.css}/${config.cssBefore.filename}`,
164
+ htmlClass: config.htmlClass,
165
+ loadingClass: config.loadingClass
166
+ };
167
+ }
168
+
169
+ // src/templates/css-after.ts
170
+ function generateCssAfterLoader(options) {
171
+ const { remoteUrl, localPath, timeout } = options;
172
+ return `<!-- === PATCH-ADAMS: CSS AFTER (async with fallback) === -->
173
+ <script data-pa="css-after-loader">
174
+ (function() {
175
+ 'use strict';
176
+ var REMOTE_URL = "${remoteUrl}";
177
+ var LOCAL_PATH = "${localPath}";
178
+ var TIMEOUT = ${timeout};
179
+
180
+ function loadCSS(url, onSuccess, onError) {
181
+ var link = document.createElement('link');
182
+ link.rel = 'stylesheet';
183
+ link.href = url;
184
+ link.setAttribute('data-pa', 'css-after');
185
+
186
+ link.onload = function() {
187
+ if (onSuccess) onSuccess();
188
+ };
189
+
190
+ link.onerror = function() {
191
+ if (onError) onError();
192
+ };
193
+
194
+ document.head.appendChild(link);
195
+ return link;
196
+ }
197
+
198
+ function loadCSSWithFallback() {
199
+ var loaded = false;
200
+ var timeoutId;
201
+
202
+ // Try remote first
203
+ var remoteLink = loadCSS(
204
+ REMOTE_URL,
205
+ function() {
206
+ if (loaded) return;
207
+ loaded = true;
208
+ clearTimeout(timeoutId);
209
+ console.log('[Patch-Adams] CSS after loaded from remote:', REMOTE_URL);
210
+ },
211
+ function() {
212
+ if (loaded) return;
213
+ loaded = true;
214
+ clearTimeout(timeoutId);
215
+ loadLocalFallback();
216
+ }
217
+ );
218
+
219
+ // Timeout fallback
220
+ timeoutId = setTimeout(function() {
221
+ if (loaded) return;
222
+ loaded = true;
223
+ console.warn('[Patch-Adams] CSS after timed out, using local fallback');
224
+ if (remoteLink.parentNode) {
225
+ document.head.removeChild(remoteLink);
226
+ }
227
+ loadLocalFallback();
228
+ }, TIMEOUT);
229
+ }
230
+
231
+ function loadLocalFallback() {
232
+ loadCSS(
233
+ LOCAL_PATH,
234
+ function() {
235
+ console.log('[Patch-Adams] CSS after loaded from local fallback:', LOCAL_PATH);
236
+ },
237
+ function() {
238
+ console.error('[Patch-Adams] CSS after failed to load from both remote and local');
239
+ }
240
+ );
241
+ }
242
+
243
+ // Execute immediately
244
+ loadCSSWithFallback();
245
+ })();
246
+ </script>`;
247
+ }
248
+ function buildCssAfterOptions(config) {
249
+ return {
250
+ remoteUrl: `${config.remoteDomain}/${config.cssAfter.filename}`,
251
+ localPath: `${config.localFolders.css}/${config.cssAfter.filename}`,
252
+ timeout: config.cssAfter.timeout
253
+ };
254
+ }
255
+
256
+ // src/templates/js-before.ts
257
+ function generateJsBeforeLoader(options) {
258
+ const { remoteUrl, localPath, htmlClass, loadingClass } = options;
259
+ return `<!-- === PATCH-ADAMS: JS BEFORE (blocking) === -->
260
+ <script data-pa="js-before-loader">
261
+ (function() {
262
+ 'use strict';
263
+ var REMOTE_URL = "${remoteUrl}";
264
+ var LOCAL_PATH = "${localPath}";
265
+
266
+ // Initialize Patch-Adams global namespace
267
+ window.PatchAdams = window.PatchAdams || {
268
+ version: '1.0.0',
269
+ htmlClass: '${htmlClass}',
270
+ loadingClass: '${loadingClass}',
271
+ loaded: {
272
+ cssBefore: false,
273
+ cssAfter: false,
274
+ jsBefore: false,
275
+ jsAfter: false
276
+ }
277
+ };
278
+
279
+ function loadJSSync(url) {
280
+ var script = document.createElement('script');
281
+ script.src = url;
282
+ script.setAttribute('data-pa', 'js-before');
283
+ // Note: For truly blocking behavior, we use document.write
284
+ // but that's fragile. Instead we'll handle errors gracefully.
285
+ document.head.appendChild(script);
286
+ return script;
287
+ }
288
+
289
+ // Try remote first
290
+ var script = loadJSSync(REMOTE_URL);
291
+
292
+ script.onerror = function() {
293
+ console.warn('[Patch-Adams] JS before failed to load from remote, using local fallback');
294
+ document.head.removeChild(script);
295
+ var fallback = loadJSSync(LOCAL_PATH);
296
+ fallback.onload = function() {
297
+ window.PatchAdams.loaded.jsBefore = true;
298
+ console.log('[Patch-Adams] JS before loaded from local fallback:', LOCAL_PATH);
299
+ };
300
+ fallback.onerror = function() {
301
+ console.error('[Patch-Adams] JS before failed to load from both remote and local');
302
+ };
303
+ };
304
+
305
+ script.onload = function() {
306
+ window.PatchAdams.loaded.jsBefore = true;
307
+ console.log('[Patch-Adams] JS before loaded from remote:', REMOTE_URL);
308
+ };
309
+ })();
310
+ </script>`;
311
+ }
312
+ function buildJsBeforeOptions(config) {
313
+ return {
314
+ remoteUrl: `${config.remoteDomain}/${config.jsBefore.filename}`,
315
+ localPath: `${config.localFolders.js}/${config.jsBefore.filename}`,
316
+ htmlClass: config.htmlClass,
317
+ loadingClass: config.loadingClass
318
+ };
319
+ }
320
+
321
+ // src/templates/js-after.ts
322
+ function generateJsAfterLoader(options) {
323
+ const { remoteUrl, localPath, timeout, loadingClass } = options;
324
+ return `<!-- === PATCH-ADAMS: JS AFTER (async with fallback) === -->
325
+ <script data-pa="js-after-loader">
326
+ (function() {
327
+ 'use strict';
328
+ var REMOTE_URL = "${remoteUrl}";
329
+ var LOCAL_PATH = "${localPath}";
330
+ var TIMEOUT = ${timeout};
331
+ var LOADING_CLASS = "${loadingClass}";
332
+
333
+ function loadJS(url, onSuccess, onError) {
334
+ var script = document.createElement('script');
335
+ script.src = url;
336
+ script.async = true;
337
+ script.setAttribute('data-pa', 'js-after');
338
+
339
+ script.onload = function() {
340
+ if (onSuccess) onSuccess();
341
+ };
342
+
343
+ script.onerror = function() {
344
+ if (onError) onError();
345
+ };
346
+
347
+ document.body.appendChild(script);
348
+ return script;
349
+ }
350
+
351
+ function removeLoadingClass() {
352
+ document.documentElement.classList.remove(LOADING_CLASS);
353
+ if (window.PatchAdams) {
354
+ window.PatchAdams.loaded.jsAfter = true;
355
+ }
356
+ console.log('[Patch-Adams] Loading complete, content revealed');
357
+ }
358
+
359
+ function loadJSWithFallback() {
360
+ var loaded = false;
361
+ var timeoutId;
362
+
363
+ // Try remote first
364
+ var remoteScript = loadJS(
365
+ REMOTE_URL,
366
+ function() {
367
+ if (loaded) return;
368
+ loaded = true;
369
+ clearTimeout(timeoutId);
370
+ console.log('[Patch-Adams] JS after loaded from remote:', REMOTE_URL);
371
+ // Give the script a moment to execute, then remove loading class
372
+ setTimeout(removeLoadingClass, 50);
373
+ },
374
+ function() {
375
+ if (loaded) return;
376
+ loaded = true;
377
+ clearTimeout(timeoutId);
378
+ loadLocalFallback();
379
+ }
380
+ );
381
+
382
+ // Timeout fallback
383
+ timeoutId = setTimeout(function() {
384
+ if (loaded) return;
385
+ loaded = true;
386
+ console.warn('[Patch-Adams] JS after timed out, using local fallback');
387
+ if (remoteScript.parentNode) {
388
+ document.body.removeChild(remoteScript);
389
+ }
390
+ loadLocalFallback();
391
+ }, TIMEOUT);
392
+ }
393
+
394
+ function loadLocalFallback() {
395
+ loadJS(
396
+ LOCAL_PATH,
397
+ function() {
398
+ console.log('[Patch-Adams] JS after loaded from local fallback:', LOCAL_PATH);
399
+ setTimeout(removeLoadingClass, 50);
400
+ },
401
+ function() {
402
+ console.error('[Patch-Adams] JS after failed to load from both remote and local');
403
+ // Still remove loading class so content is visible even if JS fails
404
+ removeLoadingClass();
405
+ }
406
+ );
407
+ }
408
+
409
+ // Execute after DOM is ready to ensure Rise has loaded
410
+ if (document.readyState === 'loading') {
411
+ document.addEventListener('DOMContentLoaded', function() {
412
+ // Small delay to ensure Rise has initialized
413
+ setTimeout(loadJSWithFallback, 100);
414
+ });
415
+ } else {
416
+ // DOM is already ready
417
+ setTimeout(loadJSWithFallback, 100);
418
+ }
419
+ })();
420
+ </script>`;
421
+ }
422
+ function buildJsAfterOptions(config) {
423
+ return {
424
+ remoteUrl: `${config.remoteDomain}/${config.jsAfter.filename}`,
425
+ localPath: `${config.localFolders.js}/${config.jsAfter.filename}`,
426
+ timeout: config.jsAfter.timeout,
427
+ loadingClass: config.loadingClass
428
+ };
429
+ }
430
+
431
+ // src/patcher/html-injector.ts
432
+ var HtmlInjector = class {
433
+ config;
434
+ constructor(config) {
435
+ this.config = config;
436
+ }
437
+ /**
438
+ * Inject all loaders into HTML
439
+ */
440
+ inject(html) {
441
+ let result = html;
442
+ result = this.addHtmlClasses(result);
443
+ if (this.config.cssBefore.enabled) {
444
+ result = this.injectCssBefore(result);
445
+ }
446
+ if (this.config.jsBefore.enabled) {
447
+ result = this.injectJsBefore(result);
448
+ }
449
+ if (this.config.cssAfter.enabled) {
450
+ result = this.injectCssAfter(result);
451
+ }
452
+ if (this.config.jsAfter.enabled) {
453
+ result = this.injectJsAfter(result);
454
+ }
455
+ return result;
456
+ }
457
+ /**
458
+ * Add pa-patched and pa-loading classes to <html> tag
459
+ */
460
+ addHtmlClasses(html) {
461
+ const { htmlClass, loadingClass } = this.config;
462
+ const classes = `${htmlClass} ${loadingClass}`;
463
+ const htmlTagPattern = /<html([^>]*)>/i;
464
+ const match = html.match(htmlTagPattern);
465
+ if (!match) {
466
+ return html;
467
+ }
468
+ const attributes = match[1];
469
+ if (/class\s*=\s*["'][^"']*["']/i.test(attributes)) {
470
+ return html.replace(
471
+ htmlTagPattern,
472
+ (_match, attrs) => {
473
+ const newAttrs = attrs.replace(
474
+ /class\s*=\s*["']([^"']*)["']/i,
475
+ (_fullMatch, existingClasses) => `class="${existingClasses} ${classes}"`
476
+ );
477
+ return `<html${newAttrs}>`;
478
+ }
479
+ );
480
+ } else {
481
+ return html.replace(htmlTagPattern, `<html${attributes} class="${classes}">`);
482
+ }
483
+ }
484
+ /**
485
+ * Inject CSS Before loader at start of <head>
486
+ */
487
+ injectCssBefore(html) {
488
+ const options = buildCssBeforeOptions(this.config);
489
+ const loader = generateCssBeforeLoader(options);
490
+ return html.replace(/<head([^>]*)>/i, `<head$1>
491
+ ${loader}`);
492
+ }
493
+ /**
494
+ * Inject JS Before loader at start of <head> (after CSS Before if present)
495
+ */
496
+ injectJsBefore(html) {
497
+ const options = buildJsBeforeOptions(this.config);
498
+ const loader = generateJsBeforeLoader(options);
499
+ const cssBeforeMarker = "<!-- === PATCH-ADAMS: CSS BEFORE";
500
+ if (html.includes(cssBeforeMarker)) {
501
+ const cssBeforeEndPattern = /<\/script>\s*(?=\n|<)/;
502
+ const cssBeforeStart = html.indexOf(cssBeforeMarker);
503
+ const afterCssBefore = html.substring(cssBeforeStart);
504
+ const endMatch = afterCssBefore.match(cssBeforeEndPattern);
505
+ if (endMatch && endMatch.index !== void 0) {
506
+ const insertPos = cssBeforeStart + endMatch.index + endMatch[0].length;
507
+ return html.slice(0, insertPos) + "\n" + loader + html.slice(insertPos);
508
+ }
509
+ }
510
+ return html.replace(/<head([^>]*)>/i, `<head$1>
511
+ ${loader}`);
512
+ }
513
+ /**
514
+ * Inject CSS After loader at end of <head>
515
+ */
516
+ injectCssAfter(html) {
517
+ const options = buildCssAfterOptions(this.config);
518
+ const loader = generateCssAfterLoader(options);
519
+ return html.replace(/<\/head>/i, `${loader}
520
+ </head>`);
521
+ }
522
+ /**
523
+ * Inject JS After loader at end of <body> (after __loadEntry)
524
+ */
525
+ injectJsAfter(html) {
526
+ const options = buildJsAfterOptions(this.config);
527
+ const loader = generateJsAfterLoader(options);
528
+ const loadEntryPattern = /(<script>__loadEntry\(\)<\/script>)/i;
529
+ if (loadEntryPattern.test(html)) {
530
+ return html.replace(loadEntryPattern, `$1
531
+ ${loader}`);
532
+ }
533
+ return html.replace(/<\/body>/i, `${loader}
534
+ </body>`);
535
+ }
536
+ };
537
+ var ManifestUpdater = class {
538
+ config;
539
+ parser;
540
+ builder;
541
+ constructor(config) {
542
+ this.config = config;
543
+ this.parser = new fastXmlParser.XMLParser({
544
+ ignoreAttributes: false,
545
+ attributeNamePrefix: "@_",
546
+ preserveOrder: false,
547
+ parseAttributeValue: false,
548
+ trimValues: true
549
+ });
550
+ this.builder = new fastXmlParser.XMLBuilder({
551
+ ignoreAttributes: false,
552
+ attributeNamePrefix: "@_",
553
+ format: true,
554
+ indentBy: " ",
555
+ suppressEmptyNode: true
556
+ });
557
+ }
558
+ /**
559
+ * Update manifest files based on package format
560
+ */
561
+ update(zip, format, paths) {
562
+ const modified = [];
563
+ switch (format) {
564
+ case "scorm12":
565
+ case "scorm2004-3":
566
+ case "scorm2004-4":
567
+ modified.push(...this.updateImsManifest(zip, paths));
568
+ break;
569
+ case "cmi5":
570
+ modified.push(...this.updateImsManifest(zip, paths));
571
+ break;
572
+ case "xapi":
573
+ break;
574
+ case "aicc":
575
+ const aiccManifest = zip.getEntry("imsmanifest.xml");
576
+ if (aiccManifest) {
577
+ modified.push(...this.updateImsManifest(zip, paths));
578
+ }
579
+ break;
580
+ }
581
+ return modified;
582
+ }
583
+ /**
584
+ * Update imsmanifest.xml to include new file references
585
+ */
586
+ updateImsManifest(zip, paths) {
587
+ const entry = zip.getEntry("imsmanifest.xml");
588
+ if (!entry) {
589
+ return [];
590
+ }
591
+ try {
592
+ const xmlContent = entry.getData().toString("utf-8");
593
+ const parsed = this.parser.parse(xmlContent);
594
+ this.addFilesToManifest(parsed, paths);
595
+ const xmlDeclaration = this.extractXmlDeclaration(xmlContent);
596
+ const updatedXml = xmlDeclaration + this.builder.build(parsed);
597
+ zip.updateFile("imsmanifest.xml", Buffer.from(updatedXml, "utf-8"));
598
+ return ["imsmanifest.xml"];
599
+ } catch (error) {
600
+ console.error("[Patch-Adams] Failed to update imsmanifest.xml:", error);
601
+ return [];
602
+ }
603
+ }
604
+ /**
605
+ * Extract XML declaration from original content
606
+ */
607
+ extractXmlDeclaration(xml) {
608
+ const match = xml.match(/<\?xml[^?]*\?>/);
609
+ return match ? match[0] + "\n" : '<?xml version="1.0" encoding="UTF-8"?>\n';
610
+ }
611
+ /**
612
+ * Add file entries to the manifest
613
+ */
614
+ addFilesToManifest(parsed, paths) {
615
+ const manifest = parsed.manifest;
616
+ if (!manifest) return;
617
+ const resources = manifest.resources;
618
+ if (!resources) return;
619
+ let resource = resources.resource;
620
+ if (!resource) return;
621
+ if (Array.isArray(resource)) {
622
+ resource = resource[0];
623
+ }
624
+ if (!resource.file) {
625
+ resource.file = [];
626
+ }
627
+ let files = resource.file;
628
+ if (!Array.isArray(files)) {
629
+ files = [files];
630
+ resource.file = files;
631
+ }
632
+ const filesToAdd = [
633
+ paths.cssBefore,
634
+ paths.cssAfter,
635
+ paths.jsBefore,
636
+ paths.jsAfter
637
+ ].filter(Boolean);
638
+ for (const filePath of filesToAdd) {
639
+ const exists = files.some((f) => f["@_href"] === filePath);
640
+ if (!exists) {
641
+ files.push({ "@_href": filePath });
642
+ }
643
+ }
644
+ }
645
+ /**
646
+ * Get the file paths that will be added to the package
647
+ */
648
+ getFilePaths() {
649
+ const paths = {};
650
+ if (this.config.cssBefore.enabled) {
651
+ paths.cssBefore = `scormcontent/${this.config.localFolders.css}/${this.config.cssBefore.filename}`;
652
+ }
653
+ if (this.config.cssAfter.enabled) {
654
+ paths.cssAfter = `scormcontent/${this.config.localFolders.css}/${this.config.cssAfter.filename}`;
655
+ }
656
+ if (this.config.jsBefore.enabled) {
657
+ paths.jsBefore = `scormcontent/${this.config.localFolders.js}/${this.config.jsBefore.filename}`;
658
+ }
659
+ if (this.config.jsAfter.enabled) {
660
+ paths.jsAfter = `scormcontent/${this.config.localFolders.js}/${this.config.jsAfter.filename}`;
661
+ }
662
+ return paths;
663
+ }
664
+ };
665
+
666
+ // src/patcher/index.ts
667
+ init_format_detector();
668
+ var DEFAULT_CSS_BEFORE = `/* Patch-Adams: CSS Before (blocking)
669
+ * This file loads at the start of <head> and blocks rendering.
670
+ * Use it to hide content and prevent flash of unstyled content.
671
+ *
672
+ * Example:
673
+ * html.pa-patched.pa-loading body {
674
+ * visibility: hidden !important;
675
+ * }
676
+ */
677
+ `;
678
+ var DEFAULT_CSS_AFTER = `/* Patch-Adams: CSS After (async)
679
+ * This file loads at the end of <head> with remote fallback.
680
+ * Use it for style overrides that take precedence over Rise styles.
681
+ *
682
+ * Example:
683
+ * html.pa-patched .blocks-text {
684
+ * font-family: 'Your Font', sans-serif !important;
685
+ * }
686
+ */
687
+ `;
688
+ var DEFAULT_JS_BEFORE = `// Patch-Adams: JS Before (blocking)
689
+ // This file loads at the start of <head> and blocks rendering.
690
+ // Use it for setup, API interception, and preparing globals.
691
+ //
692
+ // Example:
693
+ // window.PatchAdams.customConfig = {
694
+ // theme: 'dark',
695
+ // analytics: true
696
+ // };
697
+
698
+ console.log('[Patch-Adams] JS Before loaded');
699
+ `;
700
+ var DEFAULT_JS_AFTER = `// Patch-Adams: JS After (async)
701
+ // This file loads at the end of <body> with remote fallback.
702
+ // Use it for DOM manipulation after Rise has initialized.
703
+ //
704
+ // IMPORTANT: This script should NOT remove the loading class.
705
+ // The loader script handles that automatically after this script loads.
706
+ //
707
+ // Example:
708
+ // document.querySelectorAll('.blocks-text').forEach(function(el) {
709
+ // // Your modifications here
710
+ // });
711
+
712
+ console.log('[Patch-Adams] JS After loaded');
713
+ `;
714
+ var Patcher = class {
715
+ config;
716
+ htmlInjector;
717
+ manifestUpdater;
718
+ formatDetector;
719
+ constructor(config) {
720
+ this.config = config;
721
+ this.htmlInjector = new HtmlInjector(config);
722
+ this.manifestUpdater = new ManifestUpdater(config);
723
+ this.formatDetector = new FormatDetector();
724
+ }
725
+ /**
726
+ * Patch a Rise course ZIP file
727
+ * @param inputBuffer - Input ZIP file as Buffer
728
+ * @param options - Additional patching options
729
+ * @returns Patched ZIP file as Buffer and result details
730
+ */
731
+ async patch(inputBuffer, options = {}) {
732
+ const result = {
733
+ success: false,
734
+ format: "unknown",
735
+ formatDisplayName: "Unknown",
736
+ filesModified: [],
737
+ filesAdded: [],
738
+ errors: [],
739
+ warnings: []
740
+ };
741
+ try {
742
+ const zip = new AdmZip__default.default(inputBuffer);
743
+ result.format = this.formatDetector.detect(zip);
744
+ result.formatDisplayName = this.formatDetector.getFormatDisplayName(result.format);
745
+ if (result.format === "unknown") {
746
+ result.warnings.push("Could not determine package format, proceeding anyway");
747
+ }
748
+ const indexHtmlEntry = zip.getEntry("scormcontent/index.html");
749
+ if (!indexHtmlEntry) {
750
+ throw new Error("Could not find scormcontent/index.html in package. Is this a valid Rise export?");
751
+ }
752
+ const originalHtml = indexHtmlEntry.getData().toString("utf-8");
753
+ const patchedHtml = this.htmlInjector.inject(originalHtml);
754
+ zip.updateFile("scormcontent/index.html", Buffer.from(patchedHtml, "utf-8"));
755
+ result.filesModified.push("scormcontent/index.html");
756
+ const addedFiles = this.addFallbackFiles(zip, options);
757
+ result.filesAdded.push(...addedFiles);
758
+ if (this.config.updateManifests) {
759
+ const manifestPaths = this.manifestUpdater.getFilePaths();
760
+ const modifiedManifests = this.manifestUpdater.update(zip, result.format, manifestPaths);
761
+ result.filesModified.push(...modifiedManifests);
762
+ }
763
+ result.success = true;
764
+ return { buffer: zip.toBuffer(), result };
765
+ } catch (error) {
766
+ const message = error instanceof Error ? error.message : String(error);
767
+ result.errors.push(message);
768
+ throw error;
769
+ }
770
+ }
771
+ /**
772
+ * Add local fallback files to the ZIP
773
+ */
774
+ addFallbackFiles(zip, options) {
775
+ const added = [];
776
+ const cssFolder = `scormcontent/${this.config.localFolders.css}`;
777
+ const jsFolder = `scormcontent/${this.config.localFolders.js}`;
778
+ if (this.config.cssBefore.enabled) {
779
+ const path = `${cssFolder}/${this.config.cssBefore.filename}`;
780
+ const content = options.cssBeforeContent ?? DEFAULT_CSS_BEFORE;
781
+ zip.addFile(path, Buffer.from(content, "utf-8"));
782
+ added.push(path);
783
+ }
784
+ if (this.config.cssAfter.enabled) {
785
+ const path = `${cssFolder}/${this.config.cssAfter.filename}`;
786
+ const content = options.cssAfterContent ?? DEFAULT_CSS_AFTER;
787
+ zip.addFile(path, Buffer.from(content, "utf-8"));
788
+ added.push(path);
789
+ }
790
+ if (this.config.jsBefore.enabled) {
791
+ const path = `${jsFolder}/${this.config.jsBefore.filename}`;
792
+ const content = options.jsBeforeContent ?? DEFAULT_JS_BEFORE;
793
+ zip.addFile(path, Buffer.from(content, "utf-8"));
794
+ added.push(path);
795
+ }
796
+ if (this.config.jsAfter.enabled) {
797
+ const path = `${jsFolder}/${this.config.jsAfter.filename}`;
798
+ const content = options.jsAfterContent ?? DEFAULT_JS_AFTER;
799
+ zip.addFile(path, Buffer.from(content, "utf-8"));
800
+ added.push(path);
801
+ }
802
+ return added;
803
+ }
804
+ /**
805
+ * Get the current configuration
806
+ */
807
+ getConfig() {
808
+ return this.config;
809
+ }
810
+ /**
811
+ * Convenience method to patch a buffer and return only the patched buffer.
812
+ * Used by package-uploader integration.
813
+ * @param buffer - Input ZIP file as Buffer
814
+ * @returns Patched ZIP file as Buffer
815
+ */
816
+ async patchBuffer(buffer) {
817
+ const { buffer: patchedBuffer } = await this.patch(buffer);
818
+ return patchedBuffer;
819
+ }
820
+ };
821
+ var BlockingAssetConfigSchema = zod.z.object({
822
+ /** Filename for the asset */
823
+ filename: zod.z.string().min(1),
824
+ /** Whether this asset is enabled */
825
+ enabled: zod.z.boolean().default(true)
826
+ });
827
+ var AsyncAssetConfigSchema = zod.z.object({
828
+ /** Filename for the asset */
829
+ filename: zod.z.string().min(1),
830
+ /** Whether this asset is enabled */
831
+ enabled: zod.z.boolean().default(true),
832
+ /** Timeout in milliseconds for remote loading before falling back to local */
833
+ timeout: zod.z.number().min(1e3).max(3e4).default(5e3)
834
+ });
835
+ var LocalFoldersConfigSchema = zod.z.object({
836
+ /** Folder name for CSS files within scormcontent */
837
+ css: zod.z.string().default("css"),
838
+ /** Folder name for JS files within scormcontent */
839
+ js: zod.z.string().default("js")
840
+ });
841
+ var PatchAdamsConfigSchema = zod.z.object({
842
+ /** Remote domain for assets (e.g., "https://cdn.example.com/rise-overrides") */
843
+ remoteDomain: zod.z.string().url(),
844
+ /** CSS class added to <html> tag for specificity (default: "pa-patched") */
845
+ htmlClass: zod.z.string().default("pa-patched"),
846
+ /** CSS class added during loading, removed when ready (default: "pa-loading") */
847
+ loadingClass: zod.z.string().default("pa-loading"),
848
+ /** CSS loaded at start of <head> - blocking, prevents FOUC */
849
+ cssBefore: BlockingAssetConfigSchema.default({
850
+ filename: "before.css",
851
+ enabled: true
852
+ }),
853
+ /** CSS loaded at end of <head> - async with fallback, for overrides */
854
+ cssAfter: AsyncAssetConfigSchema.default({
855
+ filename: "after.css",
856
+ enabled: true,
857
+ timeout: 5e3
858
+ }),
859
+ /** JS loaded at start of <head> - blocking, for setup/interception */
860
+ jsBefore: BlockingAssetConfigSchema.default({
861
+ filename: "before.js",
862
+ enabled: true
863
+ }),
864
+ /** JS loaded at end of <body> - async with fallback, removes loading class */
865
+ jsAfter: AsyncAssetConfigSchema.default({
866
+ filename: "after.js",
867
+ enabled: true,
868
+ timeout: 5e3
869
+ }),
870
+ /** Local folder names for fallback files */
871
+ localFolders: LocalFoldersConfigSchema.default({}),
872
+ /** Whether to update manifest files (imsmanifest.xml, etc.) for SCORM compliance */
873
+ updateManifests: zod.z.boolean().default(true)
874
+ });
875
+
876
+ // src/config/defaults.ts
877
+ var defaultConfig = {
878
+ remoteDomain: "https://cdn.example.com/rise-overrides",
879
+ htmlClass: "pa-patched",
880
+ loadingClass: "pa-loading",
881
+ cssBefore: {
882
+ filename: "before.css",
883
+ enabled: true
884
+ },
885
+ cssAfter: {
886
+ filename: "after.css",
887
+ enabled: true,
888
+ timeout: 5e3
889
+ },
890
+ jsBefore: {
891
+ filename: "before.js",
892
+ enabled: true
893
+ },
894
+ jsAfter: {
895
+ filename: "after.js",
896
+ enabled: true,
897
+ timeout: 5e3
898
+ },
899
+ localFolders: {
900
+ css: "css",
901
+ js: "js"
902
+ },
903
+ updateManifests: true
904
+ };
905
+ async function loadConfig(configPath) {
906
+ const absolutePath = path.resolve(configPath);
907
+ if (!fs.existsSync(absolutePath)) {
908
+ throw new Error(`Configuration file not found: ${absolutePath}`);
909
+ }
910
+ const ext = path.extname(absolutePath).toLowerCase();
911
+ let rawConfig;
912
+ if (ext === ".json") {
913
+ const content = fs.readFileSync(absolutePath, "utf-8");
914
+ rawConfig = JSON.parse(content);
915
+ } else if (ext === ".ts" || ext === ".js" || ext === ".mjs") {
916
+ const fileUrl = url.pathToFileURL(absolutePath).href;
917
+ const module = await import(fileUrl);
918
+ rawConfig = module.default ?? module;
919
+ } else {
920
+ throw new Error(`Unsupported config file extension: ${ext}. Use .ts, .js, .mjs, or .json`);
921
+ }
922
+ const validated = PatchAdamsConfigSchema.parse(rawConfig);
923
+ return validated;
924
+ }
925
+ function mergeWithDefaults(partial) {
926
+ return PatchAdamsConfigSchema.parse({
927
+ ...defaultConfig,
928
+ ...partial
929
+ });
930
+ }
931
+ function generateConfigTemplate() {
932
+ return `import type { PatchAdamsConfig } from '@patch-adams/core';
933
+
934
+ const config: PatchAdamsConfig = {
935
+ // Remote domain where your CSS/JS files are hosted
936
+ remoteDomain: 'https://cdn.example.com/rise-overrides',
937
+
938
+ // HTML classes for specificity and loading state
939
+ htmlClass: 'pa-patched', // Always present on <html>
940
+ loadingClass: 'pa-loading', // Removed when assets are loaded
941
+
942
+ // CSS loaded at start of <head> - BLOCKING
943
+ // Use this to hide content and prevent flash of unstyled content
944
+ cssBefore: {
945
+ filename: 'before.css',
946
+ enabled: true,
947
+ },
948
+
949
+ // CSS loaded at end of <head> - ASYNC with fallback
950
+ // Use this for style overrides
951
+ cssAfter: {
952
+ filename: 'after.css',
953
+ enabled: true,
954
+ timeout: 5000, // ms before falling back to local
955
+ },
956
+
957
+ // JS loaded at start of <head> - BLOCKING
958
+ // Use this for setup, API interception, globals
959
+ jsBefore: {
960
+ filename: 'before.js',
961
+ enabled: true,
962
+ },
963
+
964
+ // JS loaded at end of <body> - ASYNC with fallback
965
+ // Use this for DOM manipulation after Rise loads
966
+ // This should remove the loadingClass when done
967
+ jsAfter: {
968
+ filename: 'after.js',
969
+ enabled: true,
970
+ timeout: 5000, // ms before falling back to local
971
+ },
972
+
973
+ // Folder names for local fallback files within scormcontent/
974
+ localFolders: {
975
+ css: 'css',
976
+ js: 'js',
977
+ },
978
+
979
+ // Update manifest files for SCORM compliance
980
+ updateManifests: true,
981
+ };
982
+
983
+ export default config;
984
+ `;
985
+ }
986
+
987
+ // src/cli.ts
988
+ var program = new commander.Command();
989
+ program.name("patch-adams").description("Patch Rise course packages to inject remote CSS/JS with local fallback").version("1.0.0");
990
+ program.command("patch <input>").description("Patch a Rise course ZIP file").option("-o, --output <path>", "Output file path (default: <input>-patched.zip)").option("-c, --config <path>", "Configuration file path (default: ./patch-adams.config.ts)").option("--css-before <path>", 'Local CSS file for "before" fallback').option("--css-after <path>", 'Local CSS file for "after" fallback').option("--js-before <path>", 'Local JS file for "before" fallback').option("--js-after <path>", 'Local JS file for "after" fallback').option("--no-manifest", "Skip updating manifest files").action(async (input, options) => {
991
+ const spinner = ora__default.default("Loading configuration...").start();
992
+ try {
993
+ let config;
994
+ const configPath = options.config || "./patch-adams.config.ts";
995
+ if (fs.existsSync(path.resolve(configPath))) {
996
+ config = await loadConfig(configPath);
997
+ spinner.text = "Configuration loaded";
998
+ } else {
999
+ spinner.text = "No config file found, using defaults";
1000
+ config = mergeWithDefaults({});
1001
+ }
1002
+ if (options.manifest === false) {
1003
+ config = { ...config, updateManifests: false };
1004
+ }
1005
+ const validatedConfig = PatchAdamsConfigSchema.parse(config);
1006
+ spinner.text = "Reading input file...";
1007
+ const inputPath = path.resolve(input);
1008
+ if (!fs.existsSync(inputPath)) {
1009
+ throw new Error(`Input file not found: ${inputPath}`);
1010
+ }
1011
+ const inputBuffer = fs.readFileSync(inputPath);
1012
+ const patchOptions = {};
1013
+ if (options.cssBefore) {
1014
+ patchOptions.cssBeforeContent = fs.readFileSync(path.resolve(options.cssBefore), "utf-8");
1015
+ }
1016
+ if (options.cssAfter) {
1017
+ patchOptions.cssAfterContent = fs.readFileSync(path.resolve(options.cssAfter), "utf-8");
1018
+ }
1019
+ if (options.jsBefore) {
1020
+ patchOptions.jsBeforeContent = fs.readFileSync(path.resolve(options.jsBefore), "utf-8");
1021
+ }
1022
+ if (options.jsAfter) {
1023
+ patchOptions.jsAfterContent = fs.readFileSync(path.resolve(options.jsAfter), "utf-8");
1024
+ }
1025
+ spinner.text = "Patching package...";
1026
+ const patcher = new Patcher(validatedConfig);
1027
+ const { buffer, result } = await patcher.patch(inputBuffer, patchOptions);
1028
+ const outputPath = options.output || inputPath.replace(/\.zip$/i, "-patched.zip");
1029
+ spinner.text = "Writing output file...";
1030
+ fs.writeFileSync(outputPath, buffer);
1031
+ spinner.succeed(chalk__default.default.green("Patching complete!"));
1032
+ console.log("");
1033
+ console.log(chalk__default.default.blue("Format detected:"), result.formatDisplayName);
1034
+ console.log(chalk__default.default.blue("Remote domain:"), validatedConfig.remoteDomain);
1035
+ console.log("");
1036
+ if (result.filesModified.length > 0) {
1037
+ console.log(chalk__default.default.blue("Files modified:"));
1038
+ result.filesModified.forEach((f) => console.log(` ${chalk__default.default.gray("\u2022")} ${f}`));
1039
+ }
1040
+ if (result.filesAdded.length > 0) {
1041
+ console.log(chalk__default.default.blue("Files added:"));
1042
+ result.filesAdded.forEach((f) => console.log(` ${chalk__default.default.gray("\u2022")} ${f}`));
1043
+ }
1044
+ if (result.warnings.length > 0) {
1045
+ console.log("");
1046
+ console.log(chalk__default.default.yellow("Warnings:"));
1047
+ result.warnings.forEach((w) => console.log(` ${chalk__default.default.yellow("!")} ${w}`));
1048
+ }
1049
+ console.log("");
1050
+ console.log(chalk__default.default.green("Output:"), outputPath);
1051
+ } catch (error) {
1052
+ spinner.fail(chalk__default.default.red("Patching failed"));
1053
+ console.error(
1054
+ chalk__default.default.red(error instanceof Error ? error.message : String(error))
1055
+ );
1056
+ process.exit(1);
1057
+ }
1058
+ });
1059
+ program.command("init").description("Create a sample configuration file").option("-o, --output <path>", "Output path (default: ./patch-adams.config.ts)").action((options) => {
1060
+ const outputPath = path.resolve(options.output || "./patch-adams.config.ts");
1061
+ if (fs.existsSync(outputPath)) {
1062
+ console.log(
1063
+ chalk__default.default.yellow(`Configuration file already exists: ${outputPath}`)
1064
+ );
1065
+ console.log(chalk__default.default.yellow("Use --output to specify a different path"));
1066
+ process.exit(1);
1067
+ }
1068
+ const template = generateConfigTemplate();
1069
+ fs.writeFileSync(outputPath, template);
1070
+ console.log(chalk__default.default.green(`Configuration file created: ${outputPath}`));
1071
+ console.log("");
1072
+ console.log("Next steps:");
1073
+ console.log(` 1. Edit ${chalk__default.default.cyan(path.basename(outputPath))} to configure your settings`);
1074
+ console.log(` 2. Run ${chalk__default.default.cyan("patch-adams patch <input.zip>")} to patch a course`);
1075
+ });
1076
+ program.command("info <input>").description("Display information about a Rise course package").action(async (input) => {
1077
+ const spinner = ora__default.default("Analyzing package...").start();
1078
+ try {
1079
+ const inputPath = path.resolve(input);
1080
+ if (!fs.existsSync(inputPath)) {
1081
+ throw new Error(`Input file not found: ${inputPath}`);
1082
+ }
1083
+ const inputBuffer = fs.readFileSync(inputPath);
1084
+ const AdmZip2 = (await import('adm-zip')).default;
1085
+ const zip = new AdmZip2(inputBuffer);
1086
+ const { FormatDetector: FormatDetector2 } = await Promise.resolve().then(() => (init_detectors(), detectors_exports));
1087
+ const detector = new FormatDetector2();
1088
+ const format = detector.detect(zip);
1089
+ const formatName = detector.getFormatDisplayName(format);
1090
+ spinner.succeed("Package analyzed");
1091
+ console.log("");
1092
+ console.log(chalk__default.default.blue("File:"), inputPath);
1093
+ console.log(chalk__default.default.blue("Format:"), formatName);
1094
+ console.log("");
1095
+ const keyFiles = [
1096
+ "scormcontent/index.html",
1097
+ "scormdriver/indexAPI.html",
1098
+ "imsmanifest.xml",
1099
+ "cmi5.xml",
1100
+ "tincan.xml"
1101
+ ];
1102
+ console.log(chalk__default.default.blue("Key files:"));
1103
+ for (const file of keyFiles) {
1104
+ const exists = zip.getEntry(file) !== null;
1105
+ const icon = exists ? chalk__default.default.green("\u2713") : chalk__default.default.gray("\u25CB");
1106
+ console.log(` ${icon} ${file}`);
1107
+ }
1108
+ const entries = zip.getEntries();
1109
+ console.log("");
1110
+ console.log(chalk__default.default.blue("Total files:"), entries.length);
1111
+ } catch (error) {
1112
+ spinner.fail(chalk__default.default.red("Analysis failed"));
1113
+ console.error(
1114
+ chalk__default.default.red(error instanceof Error ? error.message : String(error))
1115
+ );
1116
+ process.exit(1);
1117
+ }
1118
+ });
1119
+ program.parse();
1120
+ //# sourceMappingURL=cli.cjs.map
1121
+ //# sourceMappingURL=cli.cjs.map