@forwardimpact/pathway 0.25.0 → 0.25.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.
@@ -30,11 +30,13 @@
30
30
  */
31
31
 
32
32
  import { join, resolve, dirname } from "path";
33
- import { existsSync } from "fs";
34
- import { homedir } from "os";
35
33
  import { fileURLToPath } from "url";
34
+ import fs from "fs/promises";
35
+ import { homedir } from "os";
36
36
  import { createDataLoader } from "@forwardimpact/map/loader";
37
37
  import { validateAllData } from "@forwardimpact/map/validation";
38
+ import { Finder } from "@forwardimpact/libutil";
39
+ import { createLogger } from "@forwardimpact/libtelemetry";
38
40
  import { formatError } from "../src/lib/cli-output.js";
39
41
  import { createTemplateLoader } from "@forwardimpact/libtemplate";
40
42
 
@@ -336,50 +338,6 @@ function printHelp() {
336
338
  console.log(HELP_TEXT);
337
339
  }
338
340
 
339
- /**
340
- * Resolve the data directory path.
341
- * @param {Object} options - Parsed command options
342
- * @returns {string} Resolved absolute path to data directory
343
- */
344
- function resolveDataPath(options) {
345
- if (options.data) {
346
- return resolve(options.data);
347
- }
348
-
349
- if (process.env.PATHWAY_DATA) {
350
- return resolve(process.env.PATHWAY_DATA);
351
- }
352
-
353
- const homeData = join(homedir(), ".fit", "pathway", "data");
354
- if (existsSync(homeData)) {
355
- return homeData;
356
- }
357
-
358
- const cwdDataPathway = join(process.cwd(), "data/pathway");
359
- if (existsSync(cwdDataPathway)) {
360
- return cwdDataPathway;
361
- }
362
-
363
- const cwdExamplesPathway = join(process.cwd(), "examples/pathway");
364
- if (existsSync(cwdExamplesPathway)) {
365
- return cwdExamplesPathway;
366
- }
367
-
368
- const cwdData = join(process.cwd(), "data");
369
- if (existsSync(cwdData)) {
370
- return cwdData;
371
- }
372
-
373
- const cwdExamples = join(process.cwd(), "examples");
374
- if (existsSync(cwdExamples)) {
375
- return cwdExamples;
376
- }
377
-
378
- throw new Error(
379
- "No data directory found. Create ./data/pathway/ or use --data=<path>",
380
- );
381
- }
382
-
383
341
  /**
384
342
  * Main CLI entry point
385
343
  */
@@ -404,7 +362,20 @@ async function main() {
404
362
  process.exit(0);
405
363
  }
406
364
 
407
- const dataDir = resolveDataPath(options);
365
+ let dataDir;
366
+ if (options.data) {
367
+ dataDir = resolve(options.data);
368
+ } else {
369
+ const logger = createLogger("pathway");
370
+ const finder = new Finder(fs, logger, process);
371
+ try {
372
+ dataDir = join(finder.findData("data", homedir()), "pathway");
373
+ } catch {
374
+ throw new Error(
375
+ "No data directory found. Use --data=<path> to specify location.",
376
+ );
377
+ }
378
+ }
408
379
 
409
380
  if (command === "dev") {
410
381
  await runDevCommand({ dataDir, options });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/pathway",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "Career progression web app and CLI for exploring roles and generating agent teams",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -45,8 +45,8 @@
45
45
  "@forwardimpact/libtemplate": "^0.2.0",
46
46
  "@forwardimpact/libui": "^1.0.0",
47
47
  "mustache": "^4.2.0",
48
- "simple-icons": "^16.7.0",
49
- "yaml": "^2.3.4"
48
+ "simple-icons": "^16.13.0",
49
+ "yaml": "^2.8.3"
50
50
  },
51
51
  "engines": {
52
52
  "node": ">=18.0.0"
@@ -9,7 +9,7 @@
9
9
  import { cp, mkdir, rm, readFile, writeFile, access } from "fs/promises";
10
10
  import { join } from "path";
11
11
  import { homedir } from "os";
12
- import { execFileSync, execSync } from "child_process";
12
+ import { execFileSync } from "child_process";
13
13
  import { createDataLoader } from "@forwardimpact/map/loader";
14
14
 
15
15
  const INSTALL_DIR = join(homedir(), ".fit", "pathway");
@@ -112,9 +112,13 @@ export async function runUpdateCommand({ dataDir: _dataDir, options }) {
112
112
  // 6. Update global pathway package if version changed
113
113
  if (oldVersion !== newVersion) {
114
114
  console.log(` Updating pathway ${oldVersion} → ${newVersion}...`);
115
- execSync(`npm install -g @forwardimpact/pathway@${newVersion}`, {
116
- stdio: "ignore",
117
- });
115
+ execFileSync(
116
+ "npm",
117
+ ["install", "-g", `@forwardimpact/pathway@${newVersion}`],
118
+ {
119
+ stdio: "ignore",
120
+ },
121
+ );
118
122
  console.log(" ✓ Global package updated");
119
123
  }
120
124
 
@@ -30,8 +30,12 @@ import { truncate } from "../formatters/shared.js";
30
30
  function sortByCapabilityThenLevel(skills, capabilityOrder) {
31
31
  const orderMap = new Map(capabilityOrder.map((id, i) => [id, i]));
32
32
  return [...skills].sort((a, b) => {
33
- const capA = orderMap.has(a.capability) ? orderMap.get(a.capability) : capabilityOrder.length;
34
- const capB = orderMap.has(b.capability) ? orderMap.get(b.capability) : capabilityOrder.length;
33
+ const capA = orderMap.has(a.capability)
34
+ ? orderMap.get(a.capability)
35
+ : capabilityOrder.length;
36
+ const capB = orderMap.has(b.capability)
37
+ ? orderMap.get(b.capability)
38
+ : capabilityOrder.length;
35
39
  if (capA !== capB) return capA - capB;
36
40
  const levelA = SKILL_PROFICIENCY_ORDER.indexOf(a.proficiency);
37
41
  const levelB = SKILL_PROFICIENCY_ORDER.indexOf(b.proficiency);
package/src/lib/radar.js CHANGED
@@ -2,6 +2,20 @@
2
2
  * Radar chart visualization using SVG
3
3
  */
4
4
 
5
+ /**
6
+ * Escape HTML special characters to prevent XSS
7
+ * @param {string} text
8
+ * @returns {string}
9
+ */
10
+ function escapeHtml(text) {
11
+ return text
12
+ .replace(/&/g, "&amp;")
13
+ .replace(/</g, "&lt;")
14
+ .replace(/>/g, "&gt;")
15
+ .replace(/"/g, "&quot;")
16
+ .replace(/'/g, "&#039;");
17
+ }
18
+
5
19
  /**
6
20
  * @typedef {Object} RadarDataPoint
7
21
  * @property {string} label - Label for this axis
@@ -393,9 +407,9 @@ export class RadarChart {
393
407
  const y = event.clientY - rect.top;
394
408
 
395
409
  this.tooltip.innerHTML = `
396
- <strong>${data.label}</strong><br>
410
+ <strong>${escapeHtml(data.label)}</strong><br>
397
411
  Value: ${data.value}/${data.maxValue}
398
- ${data.description ? `<br><small>${data.description}</small>` : ""}
412
+ ${data.description ? `<br><small>${escapeHtml(data.description)}</small>` : ""}
399
413
  `;
400
414
 
401
415
  this.tooltip.style.left = `${x + 10}px`;
@@ -815,9 +829,9 @@ export class ComparisonRadarChart {
815
829
  const typeLabel = type === "current" ? "Current" : "Target";
816
830
 
817
831
  this.tooltip.innerHTML = `
818
- <strong>${data.label}</strong><br>
832
+ <strong>${escapeHtml(data.label)}</strong><br>
819
833
  ${typeLabel}: ${data.value}/${data.maxValue}
820
- ${data.description ? `<br><small>${data.description}</small>` : ""}
834
+ ${data.description ? `<br><small>${escapeHtml(data.description)}</small>` : ""}
821
835
  `;
822
836
 
823
837
  this.tooltip.style.left = `${x + 10}px`;
@@ -844,7 +858,7 @@ export class ComparisonRadarChart {
844
858
  : "<span style='color: #94a3b8'>No change</span>";
845
859
 
846
860
  this.tooltip.innerHTML = `
847
- <strong>${currentData.label}</strong><br>
861
+ <strong>${escapeHtml(currentData.label)}</strong><br>
848
862
  Current: ${currentData.value}/${currentData.maxValue}<br>
849
863
  Target: ${targetData.value}/${targetData.maxValue}<br>
850
864
  ${diffText}
package/src/slide-main.js CHANGED
@@ -53,6 +53,20 @@ function showLoading() {
53
53
  }
54
54
  }
55
55
 
56
+ /**
57
+ * Escape HTML special characters to prevent XSS
58
+ * @param {string} text
59
+ * @returns {string}
60
+ */
61
+ function escapeHtml(text) {
62
+ return text
63
+ .replace(/&/g, "&amp;")
64
+ .replace(/</g, "&lt;")
65
+ .replace(/>/g, "&gt;")
66
+ .replace(/"/g, "&quot;")
67
+ .replace(/'/g, "&#039;");
68
+ }
69
+
56
70
  /**
57
71
  * Render error slide
58
72
  * @param {string} title
@@ -62,8 +76,8 @@ function renderError(title, message) {
62
76
  const container = getSlideContent();
63
77
  container.innerHTML = `
64
78
  <div class="slide-error">
65
- <h1>${title}</h1>
66
- <p>${message}</p>
79
+ <h1>${escapeHtml(title)}</h1>
80
+ <p>${escapeHtml(message)}</p>
67
81
  <a href="#/">← Back to Index</a>
68
82
  </div>
69
83
  `;