@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.js CHANGED
@@ -454,7 +454,9 @@ var PatchAdamsConfigSchema = z.object({
454
454
  /** LRS Bridge configuration for xAPI/LRS communication with Bravais */
455
455
  lrsBridge: LrsBridgeConfigSchema.default({}),
456
456
  /** Plugin configurations - each plugin is keyed by its name */
457
- plugins: PluginsConfigSchema
457
+ plugins: PluginsConfigSchema,
458
+ /** Optional skin name — adds 'pa-skinned' + skin class to <html>, loads skin CSS/JS after core files */
459
+ skin: z.string().min(1).optional()
458
460
  });
459
461
 
460
462
  // src/config/defaults.ts
@@ -928,6 +930,7 @@ function generateLrsBridgeCode(options) {
928
930
  sessionId: null, // Session UUID
929
931
  launchTime: null, // ISO timestamp of course launch
930
932
  courseAttemptId: null, // Unique ID for this course attempt session (Xyleme format)
933
+ employeeId: null, // Employee ID from employee lookup
931
934
  // Statistics
932
935
  stats: {
933
936
  statementsSent: 0,
@@ -1758,6 +1761,11 @@ function generateLrsBridgeCode(options) {
1758
1761
  };
1759
1762
  log('Updated actor account: bravaisUserId=' + employeeData.bravaisUserId + ', homePage=' + actor.account.homePage + ' (was: ' + originalAccountName + ')');
1760
1763
  }
1764
+
1765
+ // Expose employee ID on LRS object for plugins (e.g. feedback)
1766
+ if (employeeData.employeeId) {
1767
+ LRS.employeeId = employeeData.employeeId;
1768
+ }
1761
1769
  }
1762
1770
  callback(actor);
1763
1771
  });
@@ -4703,7 +4711,7 @@ function escapeJs(str) {
4703
4711
  return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
4704
4712
  }
4705
4713
  function generateJsBeforeLoader(options) {
4706
- const { remoteUrl, localPath, htmlClass, loadingClass, metadata, lrsBridge } = options;
4714
+ const { remoteUrl, localPath, htmlClass, loadingClass, metadata, lrsBridge, skin } = options;
4707
4715
  const lrsBridgeCode = generateLrsBridgeCode(lrsBridge);
4708
4716
  const courseLines = [];
4709
4717
  if (metadata) {
@@ -4746,12 +4754,13 @@ ${courseLines.join("\n")}
4746
4754
  window.pa_patcher = window.pa_patcher || {
4747
4755
  version: '1.0.25',
4748
4756
  htmlClass: '${htmlClass}',
4749
- loadingClass: '${loadingClass}',${courseBlock}
4757
+ loadingClass: '${loadingClass}',${skin ? `
4758
+ skin: '${escapeJs(skin)}',` : ""}${courseBlock}
4750
4759
  loaded: {
4751
4760
  cssBefore: false,
4752
4761
  cssAfter: false,
4753
4762
  jsBefore: false,
4754
- jsAfter: false
4763
+ jsAfter: false${skin ? ",\n skinCss: false,\n skinJs: false" : ""}
4755
4764
  }
4756
4765
  };
4757
4766
 
@@ -4872,7 +4881,8 @@ function buildJsBeforeOptions(config, metadata) {
4872
4881
  htmlClass: config.htmlClass,
4873
4882
  loadingClass: config.loadingClass,
4874
4883
  metadata: metadata ?? null,
4875
- lrsBridge
4884
+ lrsBridge,
4885
+ skin: config.skin
4876
4886
  };
4877
4887
  }
4878
4888
 
@@ -4995,6 +5005,196 @@ function buildJsAfterOptions(config) {
4995
5005
  };
4996
5006
  }
4997
5007
 
5008
+ // src/templates/skin-css.ts
5009
+ function generateSkinCssLoader(options) {
5010
+ const { remoteUrl, localPath, timeout } = options;
5011
+ return `<!-- === PATCH-ADAMS: SKIN CSS (async with fallback) === -->
5012
+ <script data-pa="skin-css-loader">
5013
+ (function() {
5014
+ 'use strict';
5015
+ var REMOTE_URL = "${remoteUrl}";
5016
+ var LOCAL_PATH = "${localPath}";
5017
+ var TIMEOUT = ${timeout};
5018
+
5019
+ function loadCSS(url, onSuccess, onError) {
5020
+ var link = document.createElement('link');
5021
+ link.rel = 'stylesheet';
5022
+ link.href = url;
5023
+ link.setAttribute('data-pa', 'skin-css');
5024
+
5025
+ link.onload = function() {
5026
+ if (onSuccess) onSuccess();
5027
+ };
5028
+
5029
+ link.onerror = function() {
5030
+ if (onError) onError();
5031
+ };
5032
+
5033
+ document.head.appendChild(link);
5034
+ return link;
5035
+ }
5036
+
5037
+ function loadCSSWithFallback() {
5038
+ var loaded = false;
5039
+ var timeoutId;
5040
+
5041
+ // Try remote first
5042
+ var remoteLink = loadCSS(
5043
+ REMOTE_URL,
5044
+ function() {
5045
+ if (loaded) return;
5046
+ loaded = true;
5047
+ clearTimeout(timeoutId);
5048
+ console.log('[PA-Patcher] Skin CSS loaded from remote:', REMOTE_URL);
5049
+ },
5050
+ function() {
5051
+ if (loaded) return;
5052
+ loaded = true;
5053
+ clearTimeout(timeoutId);
5054
+ loadLocalFallback();
5055
+ }
5056
+ );
5057
+
5058
+ // Timeout fallback
5059
+ timeoutId = setTimeout(function() {
5060
+ if (loaded) return;
5061
+ loaded = true;
5062
+ console.warn('[PA-Patcher] Skin CSS timed out, using local fallback');
5063
+ if (remoteLink.parentNode) {
5064
+ document.head.removeChild(remoteLink);
5065
+ }
5066
+ loadLocalFallback();
5067
+ }, TIMEOUT);
5068
+ }
5069
+
5070
+ function loadLocalFallback() {
5071
+ loadCSS(
5072
+ LOCAL_PATH,
5073
+ function() {
5074
+ console.log('[PA-Patcher] Skin CSS loaded from local fallback:', LOCAL_PATH);
5075
+ },
5076
+ function() {
5077
+ console.error('[PA-Patcher] Skin CSS failed to load from both remote and local');
5078
+ }
5079
+ );
5080
+ }
5081
+
5082
+ // Execute immediately
5083
+ loadCSSWithFallback();
5084
+ })();
5085
+ </script>`;
5086
+ }
5087
+ function buildSkinCssOptions(config) {
5088
+ if (!config.skin) return null;
5089
+ return {
5090
+ remoteUrl: `${config.remoteDomain}/skin/${config.skin}.css`,
5091
+ localPath: `skin/${config.skin}.css`,
5092
+ timeout: config.cssAfter.timeout
5093
+ // reuse cssAfter timeout
5094
+ };
5095
+ }
5096
+
5097
+ // src/templates/skin-js.ts
5098
+ function generateSkinJsLoader(options) {
5099
+ const { remoteUrl, localPath, timeout } = options;
5100
+ return `<!-- === PATCH-ADAMS: SKIN JS (async with fallback) === -->
5101
+ <script data-pa="skin-js-loader">
5102
+ (function() {
5103
+ 'use strict';
5104
+ var REMOTE_URL = "${remoteUrl}";
5105
+ var LOCAL_PATH = "${localPath}";
5106
+ var TIMEOUT = ${timeout};
5107
+
5108
+ function loadJS(url, onSuccess, onError) {
5109
+ var script = document.createElement('script');
5110
+ script.src = url;
5111
+ script.async = true;
5112
+ script.setAttribute('data-pa', 'skin-js');
5113
+
5114
+ script.onload = function() {
5115
+ if (onSuccess) onSuccess();
5116
+ };
5117
+
5118
+ script.onerror = function() {
5119
+ if (onError) onError();
5120
+ };
5121
+
5122
+ document.body.appendChild(script);
5123
+ return script;
5124
+ }
5125
+
5126
+ function loadJSWithFallback() {
5127
+ var loaded = false;
5128
+ var timeoutId;
5129
+
5130
+ // Try remote first
5131
+ var remoteScript = loadJS(
5132
+ REMOTE_URL,
5133
+ function() {
5134
+ if (loaded) return;
5135
+ loaded = true;
5136
+ clearTimeout(timeoutId);
5137
+ console.log('[PA-Patcher] Skin JS loaded from remote:', REMOTE_URL);
5138
+ if (window.pa_patcher && window.pa_patcher.loaded) {
5139
+ window.pa_patcher.loaded.skinJs = true;
5140
+ }
5141
+ },
5142
+ function() {
5143
+ if (loaded) return;
5144
+ loaded = true;
5145
+ clearTimeout(timeoutId);
5146
+ loadLocalFallback();
5147
+ }
5148
+ );
5149
+
5150
+ // Timeout fallback
5151
+ timeoutId = setTimeout(function() {
5152
+ if (loaded) return;
5153
+ loaded = true;
5154
+ console.warn('[PA-Patcher] Skin JS timed out, using local fallback');
5155
+ if (remoteScript.parentNode) {
5156
+ document.body.removeChild(remoteScript);
5157
+ }
5158
+ loadLocalFallback();
5159
+ }, TIMEOUT);
5160
+ }
5161
+
5162
+ function loadLocalFallback() {
5163
+ loadJS(
5164
+ LOCAL_PATH,
5165
+ function() {
5166
+ console.log('[PA-Patcher] Skin JS loaded from local fallback:', LOCAL_PATH);
5167
+ if (window.pa_patcher && window.pa_patcher.loaded) {
5168
+ window.pa_patcher.loaded.skinJs = true;
5169
+ }
5170
+ },
5171
+ function() {
5172
+ console.error('[PA-Patcher] Skin JS failed to load from both remote and local');
5173
+ }
5174
+ );
5175
+ }
5176
+
5177
+ // Execute after DOM is ready
5178
+ if (document.readyState === 'loading') {
5179
+ document.addEventListener('DOMContentLoaded', function() {
5180
+ setTimeout(loadJSWithFallback, 150);
5181
+ });
5182
+ } else {
5183
+ setTimeout(loadJSWithFallback, 150);
5184
+ }
5185
+ })();
5186
+ </script>`;
5187
+ }
5188
+ function buildSkinJsOptions(config) {
5189
+ if (!config.skin) return null;
5190
+ return {
5191
+ remoteUrl: `${config.remoteDomain}/skin/${config.skin}.js`,
5192
+ localPath: `skin/${config.skin}.js`,
5193
+ timeout: config.jsAfter.timeout
5194
+ // reuse jsAfter timeout
5195
+ };
5196
+ }
5197
+
4998
5198
  // src/patcher/html-injector.ts
4999
5199
  var HtmlInjector = class {
5000
5200
  config;
@@ -5058,6 +5258,10 @@ var HtmlInjector = class {
5058
5258
  if (this.config.jsAfter.enabled) {
5059
5259
  result = this.injectJsAfter(result);
5060
5260
  }
5261
+ if (this.config.skin) {
5262
+ result = this.injectSkinCss(result);
5263
+ result = this.injectSkinJs(result);
5264
+ }
5061
5265
  if (this.pluginAssets) {
5062
5266
  result = this.injectPluginAssets(result);
5063
5267
  }
@@ -5104,8 +5308,8 @@ ${pluginJs}
5104
5308
  * Also adds data attributes for course metadata
5105
5309
  */
5106
5310
  addHtmlAttributes(html) {
5107
- const { htmlClass, loadingClass } = this.config;
5108
- const classes = `${htmlClass} ${loadingClass}`;
5311
+ const { htmlClass, loadingClass, skin } = this.config;
5312
+ const classes = `${htmlClass} ${loadingClass}${skin ? ` pa-skinned ${skin}` : ""}`;
5109
5313
  const dataAttrs = [];
5110
5314
  if (this.metadata) {
5111
5315
  dataAttrs.push(`data-pa-course-id="${this.escapeAttr(this.metadata.courseId)}"`);
@@ -5114,6 +5318,9 @@ ${pluginJs}
5114
5318
  dataAttrs.push(`data-pa-title="${this.escapeAttr(this.metadata.title)}"`);
5115
5319
  }
5116
5320
  }
5321
+ if (skin) {
5322
+ dataAttrs.push(`data-pa-skin="${this.escapeAttr(skin)}"`);
5323
+ }
5117
5324
  const dataAttrString = dataAttrs.length > 0 ? " " + dataAttrs.join(" ") : "";
5118
5325
  const htmlTagPattern = /<html([^>]*)>/i;
5119
5326
  const match = html.match(htmlTagPattern);
@@ -5192,6 +5399,26 @@ ${loader}`);
5192
5399
  ${loader}`);
5193
5400
  }
5194
5401
  return html.replace(/<\/body>/i, `${loader}
5402
+ </body>`);
5403
+ }
5404
+ /**
5405
+ * Inject Skin CSS loader after CSS After (before </head>)
5406
+ */
5407
+ injectSkinCss(html) {
5408
+ const options = buildSkinCssOptions(this.config);
5409
+ if (!options) return html;
5410
+ const loader = generateSkinCssLoader(options);
5411
+ return html.replace(/<\/head>/i, `${loader}
5412
+ </head>`);
5413
+ }
5414
+ /**
5415
+ * Inject Skin JS loader after JS After (before </body>)
5416
+ */
5417
+ injectSkinJs(html) {
5418
+ const options = buildSkinJsOptions(this.config);
5419
+ if (!options) return html;
5420
+ const loader = generateSkinJsLoader(options);
5421
+ return html.replace(/<\/body>/i, `${loader}
5195
5422
  </body>`);
5196
5423
  }
5197
5424
  };
@@ -5235,6 +5462,10 @@ var StorylineHtmlInjector = class {
5235
5462
  if (this.config.jsAfter.enabled) {
5236
5463
  result = this.injectJsAfter(result);
5237
5464
  }
5465
+ if (this.config.skin) {
5466
+ result = this.injectSkinCss(result);
5467
+ result = this.injectSkinJs(result);
5468
+ }
5238
5469
  if (this.pluginAssets) {
5239
5470
  result = this.injectPluginAssets(result);
5240
5471
  }
@@ -5279,8 +5510,8 @@ ${pluginJs}
5279
5510
  * Also adds data attributes for course metadata
5280
5511
  */
5281
5512
  addHtmlAttributes(html) {
5282
- const { htmlClass, loadingClass } = this.config;
5283
- const classes = `${htmlClass} ${loadingClass}`;
5513
+ const { htmlClass, loadingClass, skin } = this.config;
5514
+ const classes = `${htmlClass} ${loadingClass}${skin ? ` pa-skinned ${skin}` : ""}`;
5284
5515
  const dataAttrs = [];
5285
5516
  if (this.metadata) {
5286
5517
  dataAttrs.push(`data-pa-course-id="${this.escapeAttr(this.metadata.courseId)}"`);
@@ -5290,6 +5521,9 @@ ${pluginJs}
5290
5521
  dataAttrs.push(`data-pa-title="${this.escapeAttr(this.metadata.title)}"`);
5291
5522
  }
5292
5523
  }
5524
+ if (skin) {
5525
+ dataAttrs.push(`data-pa-skin="${this.escapeAttr(skin)}"`);
5526
+ }
5293
5527
  const dataAttrString = dataAttrs.length > 0 ? " " + dataAttrs.join(" ") : "";
5294
5528
  const htmlTagPattern = /<html([^>]*)>/i;
5295
5529
  const match = html.match(htmlTagPattern);
@@ -5367,6 +5601,26 @@ ${loader}`);
5367
5601
  const options = buildJsAfterOptions(this.config);
5368
5602
  const loader = generateJsAfterLoader(options);
5369
5603
  return html.replace(/<\/body>/i, `${loader}
5604
+ </body>`);
5605
+ }
5606
+ /**
5607
+ * Inject Skin CSS loader after CSS After (before </head>)
5608
+ */
5609
+ injectSkinCss(html) {
5610
+ const options = buildSkinCssOptions(this.config);
5611
+ if (!options) return html;
5612
+ const loader = generateSkinCssLoader(options);
5613
+ return html.replace(/<\/head>/i, `${loader}
5614
+ </head>`);
5615
+ }
5616
+ /**
5617
+ * Inject Skin JS loader after JS After (before </body>)
5618
+ */
5619
+ injectSkinJs(html) {
5620
+ const options = buildSkinJsOptions(this.config);
5621
+ if (!options) return html;
5622
+ const loader = generateSkinJsLoader(options);
5623
+ return html.replace(/<\/body>/i, `${loader}
5370
5624
  </body>`);
5371
5625
  }
5372
5626
  };
@@ -5417,7 +5671,9 @@ var ManifestUpdater = class {
5417
5671
  paths.cssBefore,
5418
5672
  paths.cssAfter,
5419
5673
  paths.jsBefore,
5420
- paths.jsAfter
5674
+ paths.jsAfter,
5675
+ paths.skinCss,
5676
+ paths.skinJs
5421
5677
  ].filter(Boolean);
5422
5678
  if (filesToAdd.length === 0) {
5423
5679
  return [];
@@ -5897,6 +6153,20 @@ var Patcher = class {
5897
6153
  })
5898
6154
  );
5899
6155
  }
6156
+ if (this.config.skin) {
6157
+ const skinCssUrl = `${remoteDomain}/skin/${this.config.skin}.css`;
6158
+ fetchPromises.push(
6159
+ this.fetchFile(skinCssUrl).then((content) => {
6160
+ fetched.skinCss = content;
6161
+ })
6162
+ );
6163
+ const skinJsUrl = `${remoteDomain}/skin/${this.config.skin}.js`;
6164
+ fetchPromises.push(
6165
+ this.fetchFile(skinJsUrl).then((content) => {
6166
+ fetched.skinJs = content;
6167
+ })
6168
+ );
6169
+ }
5900
6170
  await Promise.all(fetchPromises);
5901
6171
  return fetched;
5902
6172
  }
@@ -5980,6 +6250,16 @@ var Patcher = class {
5980
6250
  console.log(`[Patcher] Wrote generated UUID to manifest identifier`);
5981
6251
  }
5982
6252
  }
6253
+ const effectiveSkin = options.skin || this.config.skin;
6254
+ if (options.skin && options.skin !== this.config.skin) {
6255
+ this.config.skin = options.skin;
6256
+ this.riseHtmlInjector = new HtmlInjector(this.config);
6257
+ this.storylineHtmlInjector = new StorylineHtmlInjector(this.config);
6258
+ console.log(`[Patcher] Using per-call skin override: ${options.skin}`);
6259
+ }
6260
+ if (effectiveSkin) {
6261
+ console.log(`[Patcher] Skin: ${effectiveSkin}`);
6262
+ }
5983
6263
  const htmlInjector = this.getHtmlInjector(toolInfo.tool);
5984
6264
  htmlInjector.setMetadata(metadata);
5985
6265
  let fetchedFallbacks = {};
@@ -6108,7 +6388,11 @@ var Patcher = class {
6108
6388
  buildManifestPaths(addedFiles) {
6109
6389
  const paths = {};
6110
6390
  for (const filePath of addedFiles) {
6111
- if (filePath.endsWith(this.config.cssBefore.filename)) {
6391
+ if (this.config.skin && filePath.endsWith(`skin/${this.config.skin}.css`)) {
6392
+ paths.skinCss = filePath;
6393
+ } else if (this.config.skin && filePath.endsWith(`skin/${this.config.skin}.js`)) {
6394
+ paths.skinJs = filePath;
6395
+ } else if (filePath.endsWith(this.config.cssBefore.filename)) {
6112
6396
  paths.cssBefore = filePath;
6113
6397
  } else if (filePath.endsWith(this.config.cssAfter.filename)) {
6114
6398
  paths.cssAfter = filePath;
@@ -6192,6 +6476,22 @@ var Patcher = class {
6192
6476
  added.push(path);
6193
6477
  if (fetched.jsAfter) console.log(`[Patcher] Using fetched content for ${path}`);
6194
6478
  }
6479
+ if (this.config.skin) {
6480
+ const skinCssPath = `${basePath}skin/${this.config.skin}.css`;
6481
+ const skinCssContent = fetched.skinCss ?? `/* PA-Patcher: Skin CSS (${this.config.skin}) */
6482
+ `;
6483
+ zip.addFile(skinCssPath, Buffer.from(skinCssContent, "utf-8"));
6484
+ added.push(skinCssPath);
6485
+ if (fetched.skinCss) console.log(`[Patcher] Using fetched skin CSS for ${skinCssPath}`);
6486
+ else console.log(`[Patcher] Added placeholder skin CSS: ${skinCssPath}`);
6487
+ const skinJsPath = `${basePath}skin/${this.config.skin}.js`;
6488
+ const skinJsContent = fetched.skinJs ?? `// PA-Patcher: Skin JS (${this.config.skin})
6489
+ `;
6490
+ zip.addFile(skinJsPath, Buffer.from(skinJsContent, "utf-8"));
6491
+ added.push(skinJsPath);
6492
+ if (fetched.skinJs) console.log(`[Patcher] Using fetched skin JS for ${skinJsPath}`);
6493
+ else console.log(`[Patcher] Added placeholder skin JS: ${skinJsPath}`);
6494
+ }
6195
6495
  return added;
6196
6496
  }
6197
6497
  /**
@@ -6206,9 +6506,9 @@ var Patcher = class {
6206
6506
  * @param buffer - Input ZIP file as Buffer
6207
6507
  * @returns Patched ZIP file as Buffer
6208
6508
  */
6209
- async patchBuffer(buffer) {
6509
+ async patchBuffer(buffer, options) {
6210
6510
  console.log(`[Patcher] patchBuffer called with ${buffer.length} bytes`);
6211
- const { buffer: patchedBuffer, result } = await this.patch(buffer);
6511
+ const { buffer: patchedBuffer, result } = await this.patch(buffer, options);
6212
6512
  console.log(`[Patcher] Patch result:`, JSON.stringify(result, null, 2));
6213
6513
  console.log(`[Patcher] Returning ${patchedBuffer.length} bytes`);
6214
6514
  return patchedBuffer;