@patch-adams/core 1.4.37 → 1.5.1

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
@@ -4709,7 +4711,7 @@ function escapeJs(str) {
4709
4711
  return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
4710
4712
  }
4711
4713
  function generateJsBeforeLoader(options) {
4712
- const { remoteUrl, localPath, htmlClass, loadingClass, metadata, lrsBridge } = options;
4714
+ const { remoteUrl, localPath, htmlClass, loadingClass, metadata, lrsBridge, skin } = options;
4713
4715
  const lrsBridgeCode = generateLrsBridgeCode(lrsBridge);
4714
4716
  const courseLines = [];
4715
4717
  if (metadata) {
@@ -4752,12 +4754,13 @@ ${courseLines.join("\n")}
4752
4754
  window.pa_patcher = window.pa_patcher || {
4753
4755
  version: '1.0.25',
4754
4756
  htmlClass: '${htmlClass}',
4755
- loadingClass: '${loadingClass}',${courseBlock}
4757
+ loadingClass: '${loadingClass}',${skin ? `
4758
+ skin: '${escapeJs(skin)}',` : ""}${courseBlock}
4756
4759
  loaded: {
4757
4760
  cssBefore: false,
4758
4761
  cssAfter: false,
4759
4762
  jsBefore: false,
4760
- jsAfter: false
4763
+ jsAfter: false${skin ? ",\n skinCss: false,\n skinJs: false" : ""}
4761
4764
  }
4762
4765
  };
4763
4766
 
@@ -4878,7 +4881,8 @@ function buildJsBeforeOptions(config, metadata) {
4878
4881
  htmlClass: config.htmlClass,
4879
4882
  loadingClass: config.loadingClass,
4880
4883
  metadata: metadata ?? null,
4881
- lrsBridge
4884
+ lrsBridge,
4885
+ skin: config.skin
4882
4886
  };
4883
4887
  }
4884
4888
 
@@ -5001,6 +5005,196 @@ function buildJsAfterOptions(config) {
5001
5005
  };
5002
5006
  }
5003
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}/style.css`,
5091
+ localPath: `skin/${config.skin}/style.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}/script.js`,
5192
+ localPath: `skin/${config.skin}/script.js`,
5193
+ timeout: config.jsAfter.timeout
5194
+ // reuse jsAfter timeout
5195
+ };
5196
+ }
5197
+
5004
5198
  // src/patcher/html-injector.ts
5005
5199
  var HtmlInjector = class {
5006
5200
  config;
@@ -5064,6 +5258,10 @@ var HtmlInjector = class {
5064
5258
  if (this.config.jsAfter.enabled) {
5065
5259
  result = this.injectJsAfter(result);
5066
5260
  }
5261
+ if (this.config.skin) {
5262
+ result = this.injectSkinCss(result);
5263
+ result = this.injectSkinJs(result);
5264
+ }
5067
5265
  if (this.pluginAssets) {
5068
5266
  result = this.injectPluginAssets(result);
5069
5267
  }
@@ -5110,8 +5308,8 @@ ${pluginJs}
5110
5308
  * Also adds data attributes for course metadata
5111
5309
  */
5112
5310
  addHtmlAttributes(html) {
5113
- const { htmlClass, loadingClass } = this.config;
5114
- const classes = `${htmlClass} ${loadingClass}`;
5311
+ const { htmlClass, loadingClass, skin } = this.config;
5312
+ const classes = `${htmlClass} ${loadingClass}${skin ? ` pa-skinned ${skin}` : ""}`;
5115
5313
  const dataAttrs = [];
5116
5314
  if (this.metadata) {
5117
5315
  dataAttrs.push(`data-pa-course-id="${this.escapeAttr(this.metadata.courseId)}"`);
@@ -5120,6 +5318,9 @@ ${pluginJs}
5120
5318
  dataAttrs.push(`data-pa-title="${this.escapeAttr(this.metadata.title)}"`);
5121
5319
  }
5122
5320
  }
5321
+ if (skin) {
5322
+ dataAttrs.push(`data-pa-skin="${this.escapeAttr(skin)}"`);
5323
+ }
5123
5324
  const dataAttrString = dataAttrs.length > 0 ? " " + dataAttrs.join(" ") : "";
5124
5325
  const htmlTagPattern = /<html([^>]*)>/i;
5125
5326
  const match = html.match(htmlTagPattern);
@@ -5198,6 +5399,26 @@ ${loader}`);
5198
5399
  ${loader}`);
5199
5400
  }
5200
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}
5201
5422
  </body>`);
5202
5423
  }
5203
5424
  };
@@ -5241,6 +5462,10 @@ var StorylineHtmlInjector = class {
5241
5462
  if (this.config.jsAfter.enabled) {
5242
5463
  result = this.injectJsAfter(result);
5243
5464
  }
5465
+ if (this.config.skin) {
5466
+ result = this.injectSkinCss(result);
5467
+ result = this.injectSkinJs(result);
5468
+ }
5244
5469
  if (this.pluginAssets) {
5245
5470
  result = this.injectPluginAssets(result);
5246
5471
  }
@@ -5285,8 +5510,8 @@ ${pluginJs}
5285
5510
  * Also adds data attributes for course metadata
5286
5511
  */
5287
5512
  addHtmlAttributes(html) {
5288
- const { htmlClass, loadingClass } = this.config;
5289
- const classes = `${htmlClass} ${loadingClass}`;
5513
+ const { htmlClass, loadingClass, skin } = this.config;
5514
+ const classes = `${htmlClass} ${loadingClass}${skin ? ` pa-skinned ${skin}` : ""}`;
5290
5515
  const dataAttrs = [];
5291
5516
  if (this.metadata) {
5292
5517
  dataAttrs.push(`data-pa-course-id="${this.escapeAttr(this.metadata.courseId)}"`);
@@ -5296,6 +5521,9 @@ ${pluginJs}
5296
5521
  dataAttrs.push(`data-pa-title="${this.escapeAttr(this.metadata.title)}"`);
5297
5522
  }
5298
5523
  }
5524
+ if (skin) {
5525
+ dataAttrs.push(`data-pa-skin="${this.escapeAttr(skin)}"`);
5526
+ }
5299
5527
  const dataAttrString = dataAttrs.length > 0 ? " " + dataAttrs.join(" ") : "";
5300
5528
  const htmlTagPattern = /<html([^>]*)>/i;
5301
5529
  const match = html.match(htmlTagPattern);
@@ -5373,6 +5601,26 @@ ${loader}`);
5373
5601
  const options = buildJsAfterOptions(this.config);
5374
5602
  const loader = generateJsAfterLoader(options);
5375
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}
5376
5624
  </body>`);
5377
5625
  }
5378
5626
  };
@@ -5423,7 +5671,9 @@ var ManifestUpdater = class {
5423
5671
  paths.cssBefore,
5424
5672
  paths.cssAfter,
5425
5673
  paths.jsBefore,
5426
- paths.jsAfter
5674
+ paths.jsAfter,
5675
+ paths.skinCss,
5676
+ paths.skinJs
5427
5677
  ].filter(Boolean);
5428
5678
  if (filesToAdd.length === 0) {
5429
5679
  return [];
@@ -5903,6 +6153,20 @@ var Patcher = class {
5903
6153
  })
5904
6154
  );
5905
6155
  }
6156
+ if (this.config.skin) {
6157
+ const skinCssUrl = `${remoteDomain}/skin/${this.config.skin}/style.css`;
6158
+ fetchPromises.push(
6159
+ this.fetchFile(skinCssUrl).then((content) => {
6160
+ fetched.skinCss = content;
6161
+ })
6162
+ );
6163
+ const skinJsUrl = `${remoteDomain}/skin/${this.config.skin}/script.js`;
6164
+ fetchPromises.push(
6165
+ this.fetchFile(skinJsUrl).then((content) => {
6166
+ fetched.skinJs = content;
6167
+ })
6168
+ );
6169
+ }
5906
6170
  await Promise.all(fetchPromises);
5907
6171
  return fetched;
5908
6172
  }
@@ -5986,6 +6250,16 @@ var Patcher = class {
5986
6250
  console.log(`[Patcher] Wrote generated UUID to manifest identifier`);
5987
6251
  }
5988
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
+ }
5989
6263
  const htmlInjector = this.getHtmlInjector(toolInfo.tool);
5990
6264
  htmlInjector.setMetadata(metadata);
5991
6265
  let fetchedFallbacks = {};
@@ -6114,7 +6388,11 @@ var Patcher = class {
6114
6388
  buildManifestPaths(addedFiles) {
6115
6389
  const paths = {};
6116
6390
  for (const filePath of addedFiles) {
6117
- if (filePath.endsWith(this.config.cssBefore.filename)) {
6391
+ if (this.config.skin && filePath.endsWith(`skin/${this.config.skin}/style.css`)) {
6392
+ paths.skinCss = filePath;
6393
+ } else if (this.config.skin && filePath.endsWith(`skin/${this.config.skin}/script.js`)) {
6394
+ paths.skinJs = filePath;
6395
+ } else if (filePath.endsWith(this.config.cssBefore.filename)) {
6118
6396
  paths.cssBefore = filePath;
6119
6397
  } else if (filePath.endsWith(this.config.cssAfter.filename)) {
6120
6398
  paths.cssAfter = filePath;
@@ -6198,6 +6476,22 @@ var Patcher = class {
6198
6476
  added.push(path);
6199
6477
  if (fetched.jsAfter) console.log(`[Patcher] Using fetched content for ${path}`);
6200
6478
  }
6479
+ if (this.config.skin) {
6480
+ const skinCssPath = `${basePath}skin/${this.config.skin}/style.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}/script.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
+ }
6201
6495
  return added;
6202
6496
  }
6203
6497
  /**
@@ -6212,9 +6506,9 @@ var Patcher = class {
6212
6506
  * @param buffer - Input ZIP file as Buffer
6213
6507
  * @returns Patched ZIP file as Buffer
6214
6508
  */
6215
- async patchBuffer(buffer) {
6509
+ async patchBuffer(buffer, options) {
6216
6510
  console.log(`[Patcher] patchBuffer called with ${buffer.length} bytes`);
6217
- const { buffer: patchedBuffer, result } = await this.patch(buffer);
6511
+ const { buffer: patchedBuffer, result } = await this.patch(buffer, options);
6218
6512
  console.log(`[Patcher] Patch result:`, JSON.stringify(result, null, 2));
6219
6513
  console.log(`[Patcher] Returning ${patchedBuffer.length} bytes`);
6220
6514
  return patchedBuffer;