@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/cli.cjs CHANGED
@@ -464,7 +464,9 @@ var PatchAdamsConfigSchema = zod.z.object({
464
464
  /** LRS Bridge configuration for xAPI/LRS communication with Bravais */
465
465
  lrsBridge: LrsBridgeConfigSchema.default({}),
466
466
  /** Plugin configurations - each plugin is keyed by its name */
467
- plugins: PluginsConfigSchema
467
+ plugins: PluginsConfigSchema,
468
+ /** Optional skin name — adds 'pa-skinned' + skin class to <html>, loads skin CSS/JS after core files */
469
+ skin: zod.z.string().min(1).optional()
468
470
  });
469
471
 
470
472
  // src/config/defaults.ts
@@ -938,6 +940,7 @@ function generateLrsBridgeCode(options) {
938
940
  sessionId: null, // Session UUID
939
941
  launchTime: null, // ISO timestamp of course launch
940
942
  courseAttemptId: null, // Unique ID for this course attempt session (Xyleme format)
943
+ employeeId: null, // Employee ID from employee lookup
941
944
  // Statistics
942
945
  stats: {
943
946
  statementsSent: 0,
@@ -1768,6 +1771,11 @@ function generateLrsBridgeCode(options) {
1768
1771
  };
1769
1772
  log('Updated actor account: bravaisUserId=' + employeeData.bravaisUserId + ', homePage=' + actor.account.homePage + ' (was: ' + originalAccountName + ')');
1770
1773
  }
1774
+
1775
+ // Expose employee ID on LRS object for plugins (e.g. feedback)
1776
+ if (employeeData.employeeId) {
1777
+ LRS.employeeId = employeeData.employeeId;
1778
+ }
1771
1779
  }
1772
1780
  callback(actor);
1773
1781
  });
@@ -4713,7 +4721,7 @@ function escapeJs(str) {
4713
4721
  return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
4714
4722
  }
4715
4723
  function generateJsBeforeLoader(options) {
4716
- const { remoteUrl, localPath, htmlClass, loadingClass, metadata, lrsBridge } = options;
4724
+ const { remoteUrl, localPath, htmlClass, loadingClass, metadata, lrsBridge, skin } = options;
4717
4725
  const lrsBridgeCode = generateLrsBridgeCode(lrsBridge);
4718
4726
  const courseLines = [];
4719
4727
  if (metadata) {
@@ -4756,12 +4764,13 @@ ${courseLines.join("\n")}
4756
4764
  window.pa_patcher = window.pa_patcher || {
4757
4765
  version: '1.0.25',
4758
4766
  htmlClass: '${htmlClass}',
4759
- loadingClass: '${loadingClass}',${courseBlock}
4767
+ loadingClass: '${loadingClass}',${skin ? `
4768
+ skin: '${escapeJs(skin)}',` : ""}${courseBlock}
4760
4769
  loaded: {
4761
4770
  cssBefore: false,
4762
4771
  cssAfter: false,
4763
4772
  jsBefore: false,
4764
- jsAfter: false
4773
+ jsAfter: false${skin ? ",\n skinCss: false,\n skinJs: false" : ""}
4765
4774
  }
4766
4775
  };
4767
4776
 
@@ -4882,7 +4891,8 @@ function buildJsBeforeOptions(config, metadata) {
4882
4891
  htmlClass: config.htmlClass,
4883
4892
  loadingClass: config.loadingClass,
4884
4893
  metadata: metadata ?? null,
4885
- lrsBridge
4894
+ lrsBridge,
4895
+ skin: config.skin
4886
4896
  };
4887
4897
  }
4888
4898
 
@@ -5005,6 +5015,196 @@ function buildJsAfterOptions(config) {
5005
5015
  };
5006
5016
  }
5007
5017
 
5018
+ // src/templates/skin-css.ts
5019
+ function generateSkinCssLoader(options) {
5020
+ const { remoteUrl, localPath, timeout } = options;
5021
+ return `<!-- === PATCH-ADAMS: SKIN CSS (async with fallback) === -->
5022
+ <script data-pa="skin-css-loader">
5023
+ (function() {
5024
+ 'use strict';
5025
+ var REMOTE_URL = "${remoteUrl}";
5026
+ var LOCAL_PATH = "${localPath}";
5027
+ var TIMEOUT = ${timeout};
5028
+
5029
+ function loadCSS(url, onSuccess, onError) {
5030
+ var link = document.createElement('link');
5031
+ link.rel = 'stylesheet';
5032
+ link.href = url;
5033
+ link.setAttribute('data-pa', 'skin-css');
5034
+
5035
+ link.onload = function() {
5036
+ if (onSuccess) onSuccess();
5037
+ };
5038
+
5039
+ link.onerror = function() {
5040
+ if (onError) onError();
5041
+ };
5042
+
5043
+ document.head.appendChild(link);
5044
+ return link;
5045
+ }
5046
+
5047
+ function loadCSSWithFallback() {
5048
+ var loaded = false;
5049
+ var timeoutId;
5050
+
5051
+ // Try remote first
5052
+ var remoteLink = loadCSS(
5053
+ REMOTE_URL,
5054
+ function() {
5055
+ if (loaded) return;
5056
+ loaded = true;
5057
+ clearTimeout(timeoutId);
5058
+ console.log('[PA-Patcher] Skin CSS loaded from remote:', REMOTE_URL);
5059
+ },
5060
+ function() {
5061
+ if (loaded) return;
5062
+ loaded = true;
5063
+ clearTimeout(timeoutId);
5064
+ loadLocalFallback();
5065
+ }
5066
+ );
5067
+
5068
+ // Timeout fallback
5069
+ timeoutId = setTimeout(function() {
5070
+ if (loaded) return;
5071
+ loaded = true;
5072
+ console.warn('[PA-Patcher] Skin CSS timed out, using local fallback');
5073
+ if (remoteLink.parentNode) {
5074
+ document.head.removeChild(remoteLink);
5075
+ }
5076
+ loadLocalFallback();
5077
+ }, TIMEOUT);
5078
+ }
5079
+
5080
+ function loadLocalFallback() {
5081
+ loadCSS(
5082
+ LOCAL_PATH,
5083
+ function() {
5084
+ console.log('[PA-Patcher] Skin CSS loaded from local fallback:', LOCAL_PATH);
5085
+ },
5086
+ function() {
5087
+ console.error('[PA-Patcher] Skin CSS failed to load from both remote and local');
5088
+ }
5089
+ );
5090
+ }
5091
+
5092
+ // Execute immediately
5093
+ loadCSSWithFallback();
5094
+ })();
5095
+ </script>`;
5096
+ }
5097
+ function buildSkinCssOptions(config) {
5098
+ if (!config.skin) return null;
5099
+ return {
5100
+ remoteUrl: `${config.remoteDomain}/skin/${config.skin}.css`,
5101
+ localPath: `skin/${config.skin}.css`,
5102
+ timeout: config.cssAfter.timeout
5103
+ // reuse cssAfter timeout
5104
+ };
5105
+ }
5106
+
5107
+ // src/templates/skin-js.ts
5108
+ function generateSkinJsLoader(options) {
5109
+ const { remoteUrl, localPath, timeout } = options;
5110
+ return `<!-- === PATCH-ADAMS: SKIN JS (async with fallback) === -->
5111
+ <script data-pa="skin-js-loader">
5112
+ (function() {
5113
+ 'use strict';
5114
+ var REMOTE_URL = "${remoteUrl}";
5115
+ var LOCAL_PATH = "${localPath}";
5116
+ var TIMEOUT = ${timeout};
5117
+
5118
+ function loadJS(url, onSuccess, onError) {
5119
+ var script = document.createElement('script');
5120
+ script.src = url;
5121
+ script.async = true;
5122
+ script.setAttribute('data-pa', 'skin-js');
5123
+
5124
+ script.onload = function() {
5125
+ if (onSuccess) onSuccess();
5126
+ };
5127
+
5128
+ script.onerror = function() {
5129
+ if (onError) onError();
5130
+ };
5131
+
5132
+ document.body.appendChild(script);
5133
+ return script;
5134
+ }
5135
+
5136
+ function loadJSWithFallback() {
5137
+ var loaded = false;
5138
+ var timeoutId;
5139
+
5140
+ // Try remote first
5141
+ var remoteScript = loadJS(
5142
+ REMOTE_URL,
5143
+ function() {
5144
+ if (loaded) return;
5145
+ loaded = true;
5146
+ clearTimeout(timeoutId);
5147
+ console.log('[PA-Patcher] Skin JS loaded from remote:', REMOTE_URL);
5148
+ if (window.pa_patcher && window.pa_patcher.loaded) {
5149
+ window.pa_patcher.loaded.skinJs = true;
5150
+ }
5151
+ },
5152
+ function() {
5153
+ if (loaded) return;
5154
+ loaded = true;
5155
+ clearTimeout(timeoutId);
5156
+ loadLocalFallback();
5157
+ }
5158
+ );
5159
+
5160
+ // Timeout fallback
5161
+ timeoutId = setTimeout(function() {
5162
+ if (loaded) return;
5163
+ loaded = true;
5164
+ console.warn('[PA-Patcher] Skin JS timed out, using local fallback');
5165
+ if (remoteScript.parentNode) {
5166
+ document.body.removeChild(remoteScript);
5167
+ }
5168
+ loadLocalFallback();
5169
+ }, TIMEOUT);
5170
+ }
5171
+
5172
+ function loadLocalFallback() {
5173
+ loadJS(
5174
+ LOCAL_PATH,
5175
+ function() {
5176
+ console.log('[PA-Patcher] Skin JS loaded from local fallback:', LOCAL_PATH);
5177
+ if (window.pa_patcher && window.pa_patcher.loaded) {
5178
+ window.pa_patcher.loaded.skinJs = true;
5179
+ }
5180
+ },
5181
+ function() {
5182
+ console.error('[PA-Patcher] Skin JS failed to load from both remote and local');
5183
+ }
5184
+ );
5185
+ }
5186
+
5187
+ // Execute after DOM is ready
5188
+ if (document.readyState === 'loading') {
5189
+ document.addEventListener('DOMContentLoaded', function() {
5190
+ setTimeout(loadJSWithFallback, 150);
5191
+ });
5192
+ } else {
5193
+ setTimeout(loadJSWithFallback, 150);
5194
+ }
5195
+ })();
5196
+ </script>`;
5197
+ }
5198
+ function buildSkinJsOptions(config) {
5199
+ if (!config.skin) return null;
5200
+ return {
5201
+ remoteUrl: `${config.remoteDomain}/skin/${config.skin}.js`,
5202
+ localPath: `skin/${config.skin}.js`,
5203
+ timeout: config.jsAfter.timeout
5204
+ // reuse jsAfter timeout
5205
+ };
5206
+ }
5207
+
5008
5208
  // src/patcher/html-injector.ts
5009
5209
  var HtmlInjector = class {
5010
5210
  config;
@@ -5068,6 +5268,10 @@ var HtmlInjector = class {
5068
5268
  if (this.config.jsAfter.enabled) {
5069
5269
  result = this.injectJsAfter(result);
5070
5270
  }
5271
+ if (this.config.skin) {
5272
+ result = this.injectSkinCss(result);
5273
+ result = this.injectSkinJs(result);
5274
+ }
5071
5275
  if (this.pluginAssets) {
5072
5276
  result = this.injectPluginAssets(result);
5073
5277
  }
@@ -5114,8 +5318,8 @@ ${pluginJs}
5114
5318
  * Also adds data attributes for course metadata
5115
5319
  */
5116
5320
  addHtmlAttributes(html) {
5117
- const { htmlClass, loadingClass } = this.config;
5118
- const classes = `${htmlClass} ${loadingClass}`;
5321
+ const { htmlClass, loadingClass, skin } = this.config;
5322
+ const classes = `${htmlClass} ${loadingClass}${skin ? ` pa-skinned ${skin}` : ""}`;
5119
5323
  const dataAttrs = [];
5120
5324
  if (this.metadata) {
5121
5325
  dataAttrs.push(`data-pa-course-id="${this.escapeAttr(this.metadata.courseId)}"`);
@@ -5124,6 +5328,9 @@ ${pluginJs}
5124
5328
  dataAttrs.push(`data-pa-title="${this.escapeAttr(this.metadata.title)}"`);
5125
5329
  }
5126
5330
  }
5331
+ if (skin) {
5332
+ dataAttrs.push(`data-pa-skin="${this.escapeAttr(skin)}"`);
5333
+ }
5127
5334
  const dataAttrString = dataAttrs.length > 0 ? " " + dataAttrs.join(" ") : "";
5128
5335
  const htmlTagPattern = /<html([^>]*)>/i;
5129
5336
  const match = html.match(htmlTagPattern);
@@ -5202,6 +5409,26 @@ ${loader}`);
5202
5409
  ${loader}`);
5203
5410
  }
5204
5411
  return html.replace(/<\/body>/i, `${loader}
5412
+ </body>`);
5413
+ }
5414
+ /**
5415
+ * Inject Skin CSS loader after CSS After (before </head>)
5416
+ */
5417
+ injectSkinCss(html) {
5418
+ const options = buildSkinCssOptions(this.config);
5419
+ if (!options) return html;
5420
+ const loader = generateSkinCssLoader(options);
5421
+ return html.replace(/<\/head>/i, `${loader}
5422
+ </head>`);
5423
+ }
5424
+ /**
5425
+ * Inject Skin JS loader after JS After (before </body>)
5426
+ */
5427
+ injectSkinJs(html) {
5428
+ const options = buildSkinJsOptions(this.config);
5429
+ if (!options) return html;
5430
+ const loader = generateSkinJsLoader(options);
5431
+ return html.replace(/<\/body>/i, `${loader}
5205
5432
  </body>`);
5206
5433
  }
5207
5434
  };
@@ -5245,6 +5472,10 @@ var StorylineHtmlInjector = class {
5245
5472
  if (this.config.jsAfter.enabled) {
5246
5473
  result = this.injectJsAfter(result);
5247
5474
  }
5475
+ if (this.config.skin) {
5476
+ result = this.injectSkinCss(result);
5477
+ result = this.injectSkinJs(result);
5478
+ }
5248
5479
  if (this.pluginAssets) {
5249
5480
  result = this.injectPluginAssets(result);
5250
5481
  }
@@ -5289,8 +5520,8 @@ ${pluginJs}
5289
5520
  * Also adds data attributes for course metadata
5290
5521
  */
5291
5522
  addHtmlAttributes(html) {
5292
- const { htmlClass, loadingClass } = this.config;
5293
- const classes = `${htmlClass} ${loadingClass}`;
5523
+ const { htmlClass, loadingClass, skin } = this.config;
5524
+ const classes = `${htmlClass} ${loadingClass}${skin ? ` pa-skinned ${skin}` : ""}`;
5294
5525
  const dataAttrs = [];
5295
5526
  if (this.metadata) {
5296
5527
  dataAttrs.push(`data-pa-course-id="${this.escapeAttr(this.metadata.courseId)}"`);
@@ -5300,6 +5531,9 @@ ${pluginJs}
5300
5531
  dataAttrs.push(`data-pa-title="${this.escapeAttr(this.metadata.title)}"`);
5301
5532
  }
5302
5533
  }
5534
+ if (skin) {
5535
+ dataAttrs.push(`data-pa-skin="${this.escapeAttr(skin)}"`);
5536
+ }
5303
5537
  const dataAttrString = dataAttrs.length > 0 ? " " + dataAttrs.join(" ") : "";
5304
5538
  const htmlTagPattern = /<html([^>]*)>/i;
5305
5539
  const match = html.match(htmlTagPattern);
@@ -5377,6 +5611,26 @@ ${loader}`);
5377
5611
  const options = buildJsAfterOptions(this.config);
5378
5612
  const loader = generateJsAfterLoader(options);
5379
5613
  return html.replace(/<\/body>/i, `${loader}
5614
+ </body>`);
5615
+ }
5616
+ /**
5617
+ * Inject Skin CSS loader after CSS After (before </head>)
5618
+ */
5619
+ injectSkinCss(html) {
5620
+ const options = buildSkinCssOptions(this.config);
5621
+ if (!options) return html;
5622
+ const loader = generateSkinCssLoader(options);
5623
+ return html.replace(/<\/head>/i, `${loader}
5624
+ </head>`);
5625
+ }
5626
+ /**
5627
+ * Inject Skin JS loader after JS After (before </body>)
5628
+ */
5629
+ injectSkinJs(html) {
5630
+ const options = buildSkinJsOptions(this.config);
5631
+ if (!options) return html;
5632
+ const loader = generateSkinJsLoader(options);
5633
+ return html.replace(/<\/body>/i, `${loader}
5380
5634
  </body>`);
5381
5635
  }
5382
5636
  };
@@ -5427,7 +5681,9 @@ var ManifestUpdater = class {
5427
5681
  paths.cssBefore,
5428
5682
  paths.cssAfter,
5429
5683
  paths.jsBefore,
5430
- paths.jsAfter
5684
+ paths.jsAfter,
5685
+ paths.skinCss,
5686
+ paths.skinJs
5431
5687
  ].filter(Boolean);
5432
5688
  if (filesToAdd.length === 0) {
5433
5689
  return [];
@@ -5907,6 +6163,20 @@ var Patcher = class {
5907
6163
  })
5908
6164
  );
5909
6165
  }
6166
+ if (this.config.skin) {
6167
+ const skinCssUrl = `${remoteDomain}/skin/${this.config.skin}.css`;
6168
+ fetchPromises.push(
6169
+ this.fetchFile(skinCssUrl).then((content) => {
6170
+ fetched.skinCss = content;
6171
+ })
6172
+ );
6173
+ const skinJsUrl = `${remoteDomain}/skin/${this.config.skin}.js`;
6174
+ fetchPromises.push(
6175
+ this.fetchFile(skinJsUrl).then((content) => {
6176
+ fetched.skinJs = content;
6177
+ })
6178
+ );
6179
+ }
5910
6180
  await Promise.all(fetchPromises);
5911
6181
  return fetched;
5912
6182
  }
@@ -5990,6 +6260,16 @@ var Patcher = class {
5990
6260
  console.log(`[Patcher] Wrote generated UUID to manifest identifier`);
5991
6261
  }
5992
6262
  }
6263
+ const effectiveSkin = options.skin || this.config.skin;
6264
+ if (options.skin && options.skin !== this.config.skin) {
6265
+ this.config.skin = options.skin;
6266
+ this.riseHtmlInjector = new HtmlInjector(this.config);
6267
+ this.storylineHtmlInjector = new StorylineHtmlInjector(this.config);
6268
+ console.log(`[Patcher] Using per-call skin override: ${options.skin}`);
6269
+ }
6270
+ if (effectiveSkin) {
6271
+ console.log(`[Patcher] Skin: ${effectiveSkin}`);
6272
+ }
5993
6273
  const htmlInjector = this.getHtmlInjector(toolInfo.tool);
5994
6274
  htmlInjector.setMetadata(metadata);
5995
6275
  let fetchedFallbacks = {};
@@ -6118,7 +6398,11 @@ var Patcher = class {
6118
6398
  buildManifestPaths(addedFiles) {
6119
6399
  const paths = {};
6120
6400
  for (const filePath of addedFiles) {
6121
- if (filePath.endsWith(this.config.cssBefore.filename)) {
6401
+ if (this.config.skin && filePath.endsWith(`skin/${this.config.skin}.css`)) {
6402
+ paths.skinCss = filePath;
6403
+ } else if (this.config.skin && filePath.endsWith(`skin/${this.config.skin}.js`)) {
6404
+ paths.skinJs = filePath;
6405
+ } else if (filePath.endsWith(this.config.cssBefore.filename)) {
6122
6406
  paths.cssBefore = filePath;
6123
6407
  } else if (filePath.endsWith(this.config.cssAfter.filename)) {
6124
6408
  paths.cssAfter = filePath;
@@ -6202,6 +6486,22 @@ var Patcher = class {
6202
6486
  added.push(path);
6203
6487
  if (fetched.jsAfter) console.log(`[Patcher] Using fetched content for ${path}`);
6204
6488
  }
6489
+ if (this.config.skin) {
6490
+ const skinCssPath = `${basePath}skin/${this.config.skin}.css`;
6491
+ const skinCssContent = fetched.skinCss ?? `/* PA-Patcher: Skin CSS (${this.config.skin}) */
6492
+ `;
6493
+ zip.addFile(skinCssPath, Buffer.from(skinCssContent, "utf-8"));
6494
+ added.push(skinCssPath);
6495
+ if (fetched.skinCss) console.log(`[Patcher] Using fetched skin CSS for ${skinCssPath}`);
6496
+ else console.log(`[Patcher] Added placeholder skin CSS: ${skinCssPath}`);
6497
+ const skinJsPath = `${basePath}skin/${this.config.skin}.js`;
6498
+ const skinJsContent = fetched.skinJs ?? `// PA-Patcher: Skin JS (${this.config.skin})
6499
+ `;
6500
+ zip.addFile(skinJsPath, Buffer.from(skinJsContent, "utf-8"));
6501
+ added.push(skinJsPath);
6502
+ if (fetched.skinJs) console.log(`[Patcher] Using fetched skin JS for ${skinJsPath}`);
6503
+ else console.log(`[Patcher] Added placeholder skin JS: ${skinJsPath}`);
6504
+ }
6205
6505
  return added;
6206
6506
  }
6207
6507
  /**
@@ -6216,9 +6516,9 @@ var Patcher = class {
6216
6516
  * @param buffer - Input ZIP file as Buffer
6217
6517
  * @returns Patched ZIP file as Buffer
6218
6518
  */
6219
- async patchBuffer(buffer) {
6519
+ async patchBuffer(buffer, options) {
6220
6520
  console.log(`[Patcher] patchBuffer called with ${buffer.length} bytes`);
6221
- const { buffer: patchedBuffer, result } = await this.patch(buffer);
6521
+ const { buffer: patchedBuffer, result } = await this.patch(buffer, options);
6222
6522
  console.log(`[Patcher] Patch result:`, JSON.stringify(result, null, 2));
6223
6523
  console.log(`[Patcher] Returning ${patchedBuffer.length} bytes`);
6224
6524
  return patchedBuffer;