@patch-adams/core 1.0.6 → 1.0.8

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