@patch-adams/core 1.4.36 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -115,7 +115,9 @@ var PatchAdamsConfigSchema = zod.z.object({
115
115
  /** LRS Bridge configuration for xAPI/LRS communication with Bravais */
116
116
  lrsBridge: LrsBridgeConfigSchema.default({}),
117
117
  /** Plugin configurations - each plugin is keyed by its name */
118
- plugins: PluginsConfigSchema
118
+ plugins: PluginsConfigSchema,
119
+ /** Optional skin name — adds 'pa-skinned' + skin class to <html>, loads skin CSS/JS after core files */
120
+ skin: zod.z.string().min(1).optional()
119
121
  });
120
122
 
121
123
  // src/config/defaults.ts
@@ -605,6 +607,7 @@ function generateLrsBridgeCode(options) {
605
607
  sessionId: null, // Session UUID
606
608
  launchTime: null, // ISO timestamp of course launch
607
609
  courseAttemptId: null, // Unique ID for this course attempt session (Xyleme format)
610
+ employeeId: null, // Employee ID from employee lookup
608
611
  // Statistics
609
612
  stats: {
610
613
  statementsSent: 0,
@@ -1435,6 +1438,11 @@ function generateLrsBridgeCode(options) {
1435
1438
  };
1436
1439
  log('Updated actor account: bravaisUserId=' + employeeData.bravaisUserId + ', homePage=' + actor.account.homePage + ' (was: ' + originalAccountName + ')');
1437
1440
  }
1441
+
1442
+ // Expose employee ID on LRS object for plugins (e.g. feedback)
1443
+ if (employeeData.employeeId) {
1444
+ LRS.employeeId = employeeData.employeeId;
1445
+ }
1438
1446
  }
1439
1447
  callback(actor);
1440
1448
  });
@@ -4380,7 +4388,7 @@ function escapeJs(str) {
4380
4388
  return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
4381
4389
  }
4382
4390
  function generateJsBeforeLoader(options) {
4383
- const { remoteUrl, localPath, htmlClass, loadingClass, metadata, lrsBridge } = options;
4391
+ const { remoteUrl, localPath, htmlClass, loadingClass, metadata, lrsBridge, skin } = options;
4384
4392
  const lrsBridgeCode = generateLrsBridgeCode(lrsBridge);
4385
4393
  const courseLines = [];
4386
4394
  if (metadata) {
@@ -4423,12 +4431,13 @@ ${courseLines.join("\n")}
4423
4431
  window.pa_patcher = window.pa_patcher || {
4424
4432
  version: '1.0.25',
4425
4433
  htmlClass: '${htmlClass}',
4426
- loadingClass: '${loadingClass}',${courseBlock}
4434
+ loadingClass: '${loadingClass}',${skin ? `
4435
+ skin: '${escapeJs(skin)}',` : ""}${courseBlock}
4427
4436
  loaded: {
4428
4437
  cssBefore: false,
4429
4438
  cssAfter: false,
4430
4439
  jsBefore: false,
4431
- jsAfter: false
4440
+ jsAfter: false${skin ? ",\n skinCss: false,\n skinJs: false" : ""}
4432
4441
  }
4433
4442
  };
4434
4443
 
@@ -4549,7 +4558,8 @@ function buildJsBeforeOptions(config, metadata) {
4549
4558
  htmlClass: config.htmlClass,
4550
4559
  loadingClass: config.loadingClass,
4551
4560
  metadata: metadata ?? null,
4552
- lrsBridge
4561
+ lrsBridge,
4562
+ skin: config.skin
4553
4563
  };
4554
4564
  }
4555
4565
 
@@ -4672,6 +4682,196 @@ function buildJsAfterOptions(config) {
4672
4682
  };
4673
4683
  }
4674
4684
 
4685
+ // src/templates/skin-css.ts
4686
+ function generateSkinCssLoader(options) {
4687
+ const { remoteUrl, localPath, timeout } = options;
4688
+ return `<!-- === PATCH-ADAMS: SKIN CSS (async with fallback) === -->
4689
+ <script data-pa="skin-css-loader">
4690
+ (function() {
4691
+ 'use strict';
4692
+ var REMOTE_URL = "${remoteUrl}";
4693
+ var LOCAL_PATH = "${localPath}";
4694
+ var TIMEOUT = ${timeout};
4695
+
4696
+ function loadCSS(url, onSuccess, onError) {
4697
+ var link = document.createElement('link');
4698
+ link.rel = 'stylesheet';
4699
+ link.href = url;
4700
+ link.setAttribute('data-pa', 'skin-css');
4701
+
4702
+ link.onload = function() {
4703
+ if (onSuccess) onSuccess();
4704
+ };
4705
+
4706
+ link.onerror = function() {
4707
+ if (onError) onError();
4708
+ };
4709
+
4710
+ document.head.appendChild(link);
4711
+ return link;
4712
+ }
4713
+
4714
+ function loadCSSWithFallback() {
4715
+ var loaded = false;
4716
+ var timeoutId;
4717
+
4718
+ // Try remote first
4719
+ var remoteLink = loadCSS(
4720
+ REMOTE_URL,
4721
+ function() {
4722
+ if (loaded) return;
4723
+ loaded = true;
4724
+ clearTimeout(timeoutId);
4725
+ console.log('[PA-Patcher] Skin CSS loaded from remote:', REMOTE_URL);
4726
+ },
4727
+ function() {
4728
+ if (loaded) return;
4729
+ loaded = true;
4730
+ clearTimeout(timeoutId);
4731
+ loadLocalFallback();
4732
+ }
4733
+ );
4734
+
4735
+ // Timeout fallback
4736
+ timeoutId = setTimeout(function() {
4737
+ if (loaded) return;
4738
+ loaded = true;
4739
+ console.warn('[PA-Patcher] Skin CSS timed out, using local fallback');
4740
+ if (remoteLink.parentNode) {
4741
+ document.head.removeChild(remoteLink);
4742
+ }
4743
+ loadLocalFallback();
4744
+ }, TIMEOUT);
4745
+ }
4746
+
4747
+ function loadLocalFallback() {
4748
+ loadCSS(
4749
+ LOCAL_PATH,
4750
+ function() {
4751
+ console.log('[PA-Patcher] Skin CSS loaded from local fallback:', LOCAL_PATH);
4752
+ },
4753
+ function() {
4754
+ console.error('[PA-Patcher] Skin CSS failed to load from both remote and local');
4755
+ }
4756
+ );
4757
+ }
4758
+
4759
+ // Execute immediately
4760
+ loadCSSWithFallback();
4761
+ })();
4762
+ </script>`;
4763
+ }
4764
+ function buildSkinCssOptions(config) {
4765
+ if (!config.skin) return null;
4766
+ return {
4767
+ remoteUrl: `${config.remoteDomain}/skin/${config.skin}.css`,
4768
+ localPath: `skin/${config.skin}.css`,
4769
+ timeout: config.cssAfter.timeout
4770
+ // reuse cssAfter timeout
4771
+ };
4772
+ }
4773
+
4774
+ // src/templates/skin-js.ts
4775
+ function generateSkinJsLoader(options) {
4776
+ const { remoteUrl, localPath, timeout } = options;
4777
+ return `<!-- === PATCH-ADAMS: SKIN JS (async with fallback) === -->
4778
+ <script data-pa="skin-js-loader">
4779
+ (function() {
4780
+ 'use strict';
4781
+ var REMOTE_URL = "${remoteUrl}";
4782
+ var LOCAL_PATH = "${localPath}";
4783
+ var TIMEOUT = ${timeout};
4784
+
4785
+ function loadJS(url, onSuccess, onError) {
4786
+ var script = document.createElement('script');
4787
+ script.src = url;
4788
+ script.async = true;
4789
+ script.setAttribute('data-pa', 'skin-js');
4790
+
4791
+ script.onload = function() {
4792
+ if (onSuccess) onSuccess();
4793
+ };
4794
+
4795
+ script.onerror = function() {
4796
+ if (onError) onError();
4797
+ };
4798
+
4799
+ document.body.appendChild(script);
4800
+ return script;
4801
+ }
4802
+
4803
+ function loadJSWithFallback() {
4804
+ var loaded = false;
4805
+ var timeoutId;
4806
+
4807
+ // Try remote first
4808
+ var remoteScript = loadJS(
4809
+ REMOTE_URL,
4810
+ function() {
4811
+ if (loaded) return;
4812
+ loaded = true;
4813
+ clearTimeout(timeoutId);
4814
+ console.log('[PA-Patcher] Skin JS loaded from remote:', REMOTE_URL);
4815
+ if (window.pa_patcher && window.pa_patcher.loaded) {
4816
+ window.pa_patcher.loaded.skinJs = true;
4817
+ }
4818
+ },
4819
+ function() {
4820
+ if (loaded) return;
4821
+ loaded = true;
4822
+ clearTimeout(timeoutId);
4823
+ loadLocalFallback();
4824
+ }
4825
+ );
4826
+
4827
+ // Timeout fallback
4828
+ timeoutId = setTimeout(function() {
4829
+ if (loaded) return;
4830
+ loaded = true;
4831
+ console.warn('[PA-Patcher] Skin JS timed out, using local fallback');
4832
+ if (remoteScript.parentNode) {
4833
+ document.body.removeChild(remoteScript);
4834
+ }
4835
+ loadLocalFallback();
4836
+ }, TIMEOUT);
4837
+ }
4838
+
4839
+ function loadLocalFallback() {
4840
+ loadJS(
4841
+ LOCAL_PATH,
4842
+ function() {
4843
+ console.log('[PA-Patcher] Skin JS loaded from local fallback:', LOCAL_PATH);
4844
+ if (window.pa_patcher && window.pa_patcher.loaded) {
4845
+ window.pa_patcher.loaded.skinJs = true;
4846
+ }
4847
+ },
4848
+ function() {
4849
+ console.error('[PA-Patcher] Skin JS failed to load from both remote and local');
4850
+ }
4851
+ );
4852
+ }
4853
+
4854
+ // Execute after DOM is ready
4855
+ if (document.readyState === 'loading') {
4856
+ document.addEventListener('DOMContentLoaded', function() {
4857
+ setTimeout(loadJSWithFallback, 150);
4858
+ });
4859
+ } else {
4860
+ setTimeout(loadJSWithFallback, 150);
4861
+ }
4862
+ })();
4863
+ </script>`;
4864
+ }
4865
+ function buildSkinJsOptions(config) {
4866
+ if (!config.skin) return null;
4867
+ return {
4868
+ remoteUrl: `${config.remoteDomain}/skin/${config.skin}.js`,
4869
+ localPath: `skin/${config.skin}.js`,
4870
+ timeout: config.jsAfter.timeout
4871
+ // reuse jsAfter timeout
4872
+ };
4873
+ }
4874
+
4675
4875
  // src/patcher/html-injector.ts
4676
4876
  var HtmlInjector = class {
4677
4877
  config;
@@ -4735,6 +4935,10 @@ var HtmlInjector = class {
4735
4935
  if (this.config.jsAfter.enabled) {
4736
4936
  result = this.injectJsAfter(result);
4737
4937
  }
4938
+ if (this.config.skin) {
4939
+ result = this.injectSkinCss(result);
4940
+ result = this.injectSkinJs(result);
4941
+ }
4738
4942
  if (this.pluginAssets) {
4739
4943
  result = this.injectPluginAssets(result);
4740
4944
  }
@@ -4781,8 +4985,8 @@ ${pluginJs}
4781
4985
  * Also adds data attributes for course metadata
4782
4986
  */
4783
4987
  addHtmlAttributes(html) {
4784
- const { htmlClass, loadingClass } = this.config;
4785
- const classes = `${htmlClass} ${loadingClass}`;
4988
+ const { htmlClass, loadingClass, skin } = this.config;
4989
+ const classes = `${htmlClass} ${loadingClass}${skin ? ` pa-skinned ${skin}` : ""}`;
4786
4990
  const dataAttrs = [];
4787
4991
  if (this.metadata) {
4788
4992
  dataAttrs.push(`data-pa-course-id="${this.escapeAttr(this.metadata.courseId)}"`);
@@ -4791,6 +4995,9 @@ ${pluginJs}
4791
4995
  dataAttrs.push(`data-pa-title="${this.escapeAttr(this.metadata.title)}"`);
4792
4996
  }
4793
4997
  }
4998
+ if (skin) {
4999
+ dataAttrs.push(`data-pa-skin="${this.escapeAttr(skin)}"`);
5000
+ }
4794
5001
  const dataAttrString = dataAttrs.length > 0 ? " " + dataAttrs.join(" ") : "";
4795
5002
  const htmlTagPattern = /<html([^>]*)>/i;
4796
5003
  const match = html.match(htmlTagPattern);
@@ -4869,6 +5076,26 @@ ${loader}`);
4869
5076
  ${loader}`);
4870
5077
  }
4871
5078
  return html.replace(/<\/body>/i, `${loader}
5079
+ </body>`);
5080
+ }
5081
+ /**
5082
+ * Inject Skin CSS loader after CSS After (before </head>)
5083
+ */
5084
+ injectSkinCss(html) {
5085
+ const options = buildSkinCssOptions(this.config);
5086
+ if (!options) return html;
5087
+ const loader = generateSkinCssLoader(options);
5088
+ return html.replace(/<\/head>/i, `${loader}
5089
+ </head>`);
5090
+ }
5091
+ /**
5092
+ * Inject Skin JS loader after JS After (before </body>)
5093
+ */
5094
+ injectSkinJs(html) {
5095
+ const options = buildSkinJsOptions(this.config);
5096
+ if (!options) return html;
5097
+ const loader = generateSkinJsLoader(options);
5098
+ return html.replace(/<\/body>/i, `${loader}
4872
5099
  </body>`);
4873
5100
  }
4874
5101
  };
@@ -4912,6 +5139,10 @@ var StorylineHtmlInjector = class {
4912
5139
  if (this.config.jsAfter.enabled) {
4913
5140
  result = this.injectJsAfter(result);
4914
5141
  }
5142
+ if (this.config.skin) {
5143
+ result = this.injectSkinCss(result);
5144
+ result = this.injectSkinJs(result);
5145
+ }
4915
5146
  if (this.pluginAssets) {
4916
5147
  result = this.injectPluginAssets(result);
4917
5148
  }
@@ -4956,8 +5187,8 @@ ${pluginJs}
4956
5187
  * Also adds data attributes for course metadata
4957
5188
  */
4958
5189
  addHtmlAttributes(html) {
4959
- const { htmlClass, loadingClass } = this.config;
4960
- const classes = `${htmlClass} ${loadingClass}`;
5190
+ const { htmlClass, loadingClass, skin } = this.config;
5191
+ const classes = `${htmlClass} ${loadingClass}${skin ? ` pa-skinned ${skin}` : ""}`;
4961
5192
  const dataAttrs = [];
4962
5193
  if (this.metadata) {
4963
5194
  dataAttrs.push(`data-pa-course-id="${this.escapeAttr(this.metadata.courseId)}"`);
@@ -4967,6 +5198,9 @@ ${pluginJs}
4967
5198
  dataAttrs.push(`data-pa-title="${this.escapeAttr(this.metadata.title)}"`);
4968
5199
  }
4969
5200
  }
5201
+ if (skin) {
5202
+ dataAttrs.push(`data-pa-skin="${this.escapeAttr(skin)}"`);
5203
+ }
4970
5204
  const dataAttrString = dataAttrs.length > 0 ? " " + dataAttrs.join(" ") : "";
4971
5205
  const htmlTagPattern = /<html([^>]*)>/i;
4972
5206
  const match = html.match(htmlTagPattern);
@@ -5044,6 +5278,26 @@ ${loader}`);
5044
5278
  const options = buildJsAfterOptions(this.config);
5045
5279
  const loader = generateJsAfterLoader(options);
5046
5280
  return html.replace(/<\/body>/i, `${loader}
5281
+ </body>`);
5282
+ }
5283
+ /**
5284
+ * Inject Skin CSS loader after CSS After (before </head>)
5285
+ */
5286
+ injectSkinCss(html) {
5287
+ const options = buildSkinCssOptions(this.config);
5288
+ if (!options) return html;
5289
+ const loader = generateSkinCssLoader(options);
5290
+ return html.replace(/<\/head>/i, `${loader}
5291
+ </head>`);
5292
+ }
5293
+ /**
5294
+ * Inject Skin JS loader after JS After (before </body>)
5295
+ */
5296
+ injectSkinJs(html) {
5297
+ const options = buildSkinJsOptions(this.config);
5298
+ if (!options) return html;
5299
+ const loader = generateSkinJsLoader(options);
5300
+ return html.replace(/<\/body>/i, `${loader}
5047
5301
  </body>`);
5048
5302
  }
5049
5303
  };
@@ -5094,7 +5348,9 @@ var ManifestUpdater = class {
5094
5348
  paths.cssBefore,
5095
5349
  paths.cssAfter,
5096
5350
  paths.jsBefore,
5097
- paths.jsAfter
5351
+ paths.jsAfter,
5352
+ paths.skinCss,
5353
+ paths.skinJs
5098
5354
  ].filter(Boolean);
5099
5355
  if (filesToAdd.length === 0) {
5100
5356
  return [];
@@ -5884,6 +6140,20 @@ var Patcher = class {
5884
6140
  })
5885
6141
  );
5886
6142
  }
6143
+ if (this.config.skin) {
6144
+ const skinCssUrl = `${remoteDomain}/skin/${this.config.skin}.css`;
6145
+ fetchPromises.push(
6146
+ this.fetchFile(skinCssUrl).then((content) => {
6147
+ fetched.skinCss = content;
6148
+ })
6149
+ );
6150
+ const skinJsUrl = `${remoteDomain}/skin/${this.config.skin}.js`;
6151
+ fetchPromises.push(
6152
+ this.fetchFile(skinJsUrl).then((content) => {
6153
+ fetched.skinJs = content;
6154
+ })
6155
+ );
6156
+ }
5887
6157
  await Promise.all(fetchPromises);
5888
6158
  return fetched;
5889
6159
  }
@@ -5967,6 +6237,16 @@ var Patcher = class {
5967
6237
  console.log(`[Patcher] Wrote generated UUID to manifest identifier`);
5968
6238
  }
5969
6239
  }
6240
+ const effectiveSkin = options.skin || this.config.skin;
6241
+ if (options.skin && options.skin !== this.config.skin) {
6242
+ this.config.skin = options.skin;
6243
+ this.riseHtmlInjector = new HtmlInjector(this.config);
6244
+ this.storylineHtmlInjector = new StorylineHtmlInjector(this.config);
6245
+ console.log(`[Patcher] Using per-call skin override: ${options.skin}`);
6246
+ }
6247
+ if (effectiveSkin) {
6248
+ console.log(`[Patcher] Skin: ${effectiveSkin}`);
6249
+ }
5970
6250
  const htmlInjector = this.getHtmlInjector(toolInfo.tool);
5971
6251
  htmlInjector.setMetadata(metadata);
5972
6252
  let fetchedFallbacks = {};
@@ -6095,7 +6375,11 @@ var Patcher = class {
6095
6375
  buildManifestPaths(addedFiles) {
6096
6376
  const paths = {};
6097
6377
  for (const filePath of addedFiles) {
6098
- if (filePath.endsWith(this.config.cssBefore.filename)) {
6378
+ if (this.config.skin && filePath.endsWith(`skin/${this.config.skin}.css`)) {
6379
+ paths.skinCss = filePath;
6380
+ } else if (this.config.skin && filePath.endsWith(`skin/${this.config.skin}.js`)) {
6381
+ paths.skinJs = filePath;
6382
+ } else if (filePath.endsWith(this.config.cssBefore.filename)) {
6099
6383
  paths.cssBefore = filePath;
6100
6384
  } else if (filePath.endsWith(this.config.cssAfter.filename)) {
6101
6385
  paths.cssAfter = filePath;
@@ -6179,6 +6463,22 @@ var Patcher = class {
6179
6463
  added.push(path);
6180
6464
  if (fetched.jsAfter) console.log(`[Patcher] Using fetched content for ${path}`);
6181
6465
  }
6466
+ if (this.config.skin) {
6467
+ const skinCssPath = `${basePath}skin/${this.config.skin}.css`;
6468
+ const skinCssContent = fetched.skinCss ?? `/* PA-Patcher: Skin CSS (${this.config.skin}) */
6469
+ `;
6470
+ zip.addFile(skinCssPath, Buffer.from(skinCssContent, "utf-8"));
6471
+ added.push(skinCssPath);
6472
+ if (fetched.skinCss) console.log(`[Patcher] Using fetched skin CSS for ${skinCssPath}`);
6473
+ else console.log(`[Patcher] Added placeholder skin CSS: ${skinCssPath}`);
6474
+ const skinJsPath = `${basePath}skin/${this.config.skin}.js`;
6475
+ const skinJsContent = fetched.skinJs ?? `// PA-Patcher: Skin JS (${this.config.skin})
6476
+ `;
6477
+ zip.addFile(skinJsPath, Buffer.from(skinJsContent, "utf-8"));
6478
+ added.push(skinJsPath);
6479
+ if (fetched.skinJs) console.log(`[Patcher] Using fetched skin JS for ${skinJsPath}`);
6480
+ else console.log(`[Patcher] Added placeholder skin JS: ${skinJsPath}`);
6481
+ }
6182
6482
  return added;
6183
6483
  }
6184
6484
  /**
@@ -6193,9 +6493,9 @@ var Patcher = class {
6193
6493
  * @param buffer - Input ZIP file as Buffer
6194
6494
  * @returns Patched ZIP file as Buffer
6195
6495
  */
6196
- async patchBuffer(buffer) {
6496
+ async patchBuffer(buffer, options) {
6197
6497
  console.log(`[Patcher] patchBuffer called with ${buffer.length} bytes`);
6198
- const { buffer: patchedBuffer, result } = await this.patch(buffer);
6498
+ const { buffer: patchedBuffer, result } = await this.patch(buffer, options);
6199
6499
  console.log(`[Patcher] Patch result:`, JSON.stringify(result, null, 2));
6200
6500
  console.log(`[Patcher] Returning ${patchedBuffer.length} bytes`);
6201
6501
  return patchedBuffer;