@patch-adams/core 1.4.37 → 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
@@ -4719,7 +4721,7 @@ function escapeJs(str) {
4719
4721
  return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
4720
4722
  }
4721
4723
  function generateJsBeforeLoader(options) {
4722
- const { remoteUrl, localPath, htmlClass, loadingClass, metadata, lrsBridge } = options;
4724
+ const { remoteUrl, localPath, htmlClass, loadingClass, metadata, lrsBridge, skin } = options;
4723
4725
  const lrsBridgeCode = generateLrsBridgeCode(lrsBridge);
4724
4726
  const courseLines = [];
4725
4727
  if (metadata) {
@@ -4762,12 +4764,13 @@ ${courseLines.join("\n")}
4762
4764
  window.pa_patcher = window.pa_patcher || {
4763
4765
  version: '1.0.25',
4764
4766
  htmlClass: '${htmlClass}',
4765
- loadingClass: '${loadingClass}',${courseBlock}
4767
+ loadingClass: '${loadingClass}',${skin ? `
4768
+ skin: '${escapeJs(skin)}',` : ""}${courseBlock}
4766
4769
  loaded: {
4767
4770
  cssBefore: false,
4768
4771
  cssAfter: false,
4769
4772
  jsBefore: false,
4770
- jsAfter: false
4773
+ jsAfter: false${skin ? ",\n skinCss: false,\n skinJs: false" : ""}
4771
4774
  }
4772
4775
  };
4773
4776
 
@@ -4888,7 +4891,8 @@ function buildJsBeforeOptions(config, metadata) {
4888
4891
  htmlClass: config.htmlClass,
4889
4892
  loadingClass: config.loadingClass,
4890
4893
  metadata: metadata ?? null,
4891
- lrsBridge
4894
+ lrsBridge,
4895
+ skin: config.skin
4892
4896
  };
4893
4897
  }
4894
4898
 
@@ -5011,6 +5015,196 @@ function buildJsAfterOptions(config) {
5011
5015
  };
5012
5016
  }
5013
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
+
5014
5208
  // src/patcher/html-injector.ts
5015
5209
  var HtmlInjector = class {
5016
5210
  config;
@@ -5074,6 +5268,10 @@ var HtmlInjector = class {
5074
5268
  if (this.config.jsAfter.enabled) {
5075
5269
  result = this.injectJsAfter(result);
5076
5270
  }
5271
+ if (this.config.skin) {
5272
+ result = this.injectSkinCss(result);
5273
+ result = this.injectSkinJs(result);
5274
+ }
5077
5275
  if (this.pluginAssets) {
5078
5276
  result = this.injectPluginAssets(result);
5079
5277
  }
@@ -5120,8 +5318,8 @@ ${pluginJs}
5120
5318
  * Also adds data attributes for course metadata
5121
5319
  */
5122
5320
  addHtmlAttributes(html) {
5123
- const { htmlClass, loadingClass } = this.config;
5124
- const classes = `${htmlClass} ${loadingClass}`;
5321
+ const { htmlClass, loadingClass, skin } = this.config;
5322
+ const classes = `${htmlClass} ${loadingClass}${skin ? ` pa-skinned ${skin}` : ""}`;
5125
5323
  const dataAttrs = [];
5126
5324
  if (this.metadata) {
5127
5325
  dataAttrs.push(`data-pa-course-id="${this.escapeAttr(this.metadata.courseId)}"`);
@@ -5130,6 +5328,9 @@ ${pluginJs}
5130
5328
  dataAttrs.push(`data-pa-title="${this.escapeAttr(this.metadata.title)}"`);
5131
5329
  }
5132
5330
  }
5331
+ if (skin) {
5332
+ dataAttrs.push(`data-pa-skin="${this.escapeAttr(skin)}"`);
5333
+ }
5133
5334
  const dataAttrString = dataAttrs.length > 0 ? " " + dataAttrs.join(" ") : "";
5134
5335
  const htmlTagPattern = /<html([^>]*)>/i;
5135
5336
  const match = html.match(htmlTagPattern);
@@ -5208,6 +5409,26 @@ ${loader}`);
5208
5409
  ${loader}`);
5209
5410
  }
5210
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}
5211
5432
  </body>`);
5212
5433
  }
5213
5434
  };
@@ -5251,6 +5472,10 @@ var StorylineHtmlInjector = class {
5251
5472
  if (this.config.jsAfter.enabled) {
5252
5473
  result = this.injectJsAfter(result);
5253
5474
  }
5475
+ if (this.config.skin) {
5476
+ result = this.injectSkinCss(result);
5477
+ result = this.injectSkinJs(result);
5478
+ }
5254
5479
  if (this.pluginAssets) {
5255
5480
  result = this.injectPluginAssets(result);
5256
5481
  }
@@ -5295,8 +5520,8 @@ ${pluginJs}
5295
5520
  * Also adds data attributes for course metadata
5296
5521
  */
5297
5522
  addHtmlAttributes(html) {
5298
- const { htmlClass, loadingClass } = this.config;
5299
- const classes = `${htmlClass} ${loadingClass}`;
5523
+ const { htmlClass, loadingClass, skin } = this.config;
5524
+ const classes = `${htmlClass} ${loadingClass}${skin ? ` pa-skinned ${skin}` : ""}`;
5300
5525
  const dataAttrs = [];
5301
5526
  if (this.metadata) {
5302
5527
  dataAttrs.push(`data-pa-course-id="${this.escapeAttr(this.metadata.courseId)}"`);
@@ -5306,6 +5531,9 @@ ${pluginJs}
5306
5531
  dataAttrs.push(`data-pa-title="${this.escapeAttr(this.metadata.title)}"`);
5307
5532
  }
5308
5533
  }
5534
+ if (skin) {
5535
+ dataAttrs.push(`data-pa-skin="${this.escapeAttr(skin)}"`);
5536
+ }
5309
5537
  const dataAttrString = dataAttrs.length > 0 ? " " + dataAttrs.join(" ") : "";
5310
5538
  const htmlTagPattern = /<html([^>]*)>/i;
5311
5539
  const match = html.match(htmlTagPattern);
@@ -5383,6 +5611,26 @@ ${loader}`);
5383
5611
  const options = buildJsAfterOptions(this.config);
5384
5612
  const loader = generateJsAfterLoader(options);
5385
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}
5386
5634
  </body>`);
5387
5635
  }
5388
5636
  };
@@ -5433,7 +5681,9 @@ var ManifestUpdater = class {
5433
5681
  paths.cssBefore,
5434
5682
  paths.cssAfter,
5435
5683
  paths.jsBefore,
5436
- paths.jsAfter
5684
+ paths.jsAfter,
5685
+ paths.skinCss,
5686
+ paths.skinJs
5437
5687
  ].filter(Boolean);
5438
5688
  if (filesToAdd.length === 0) {
5439
5689
  return [];
@@ -5913,6 +6163,20 @@ var Patcher = class {
5913
6163
  })
5914
6164
  );
5915
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
+ }
5916
6180
  await Promise.all(fetchPromises);
5917
6181
  return fetched;
5918
6182
  }
@@ -5996,6 +6260,16 @@ var Patcher = class {
5996
6260
  console.log(`[Patcher] Wrote generated UUID to manifest identifier`);
5997
6261
  }
5998
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
+ }
5999
6273
  const htmlInjector = this.getHtmlInjector(toolInfo.tool);
6000
6274
  htmlInjector.setMetadata(metadata);
6001
6275
  let fetchedFallbacks = {};
@@ -6124,7 +6398,11 @@ var Patcher = class {
6124
6398
  buildManifestPaths(addedFiles) {
6125
6399
  const paths = {};
6126
6400
  for (const filePath of addedFiles) {
6127
- 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)) {
6128
6406
  paths.cssBefore = filePath;
6129
6407
  } else if (filePath.endsWith(this.config.cssAfter.filename)) {
6130
6408
  paths.cssAfter = filePath;
@@ -6208,6 +6486,22 @@ var Patcher = class {
6208
6486
  added.push(path);
6209
6487
  if (fetched.jsAfter) console.log(`[Patcher] Using fetched content for ${path}`);
6210
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
+ }
6211
6505
  return added;
6212
6506
  }
6213
6507
  /**
@@ -6222,9 +6516,9 @@ var Patcher = class {
6222
6516
  * @param buffer - Input ZIP file as Buffer
6223
6517
  * @returns Patched ZIP file as Buffer
6224
6518
  */
6225
- async patchBuffer(buffer) {
6519
+ async patchBuffer(buffer, options) {
6226
6520
  console.log(`[Patcher] patchBuffer called with ${buffer.length} bytes`);
6227
- const { buffer: patchedBuffer, result } = await this.patch(buffer);
6521
+ const { buffer: patchedBuffer, result } = await this.patch(buffer, options);
6228
6522
  console.log(`[Patcher] Patch result:`, JSON.stringify(result, null, 2));
6229
6523
  console.log(`[Patcher] Returning ${patchedBuffer.length} bytes`);
6230
6524
  return patchedBuffer;