@forwardimpact/pathway 0.23.0 → 0.23.2
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/README.md +19 -19
- package/bin/fit-pathway.js +40 -44
- package/package.json +6 -2
- package/src/commands/agent.js +24 -20
- package/src/commands/build.js +6 -4
- package/src/commands/dev.js +6 -4
- package/src/commands/discipline.js +15 -17
- package/src/commands/init.js +23 -6
- package/src/commands/job.js +114 -23
- package/src/commands/level.js +7 -1
- package/src/commands/skill.js +10 -5
- package/src/commands/tool.js +3 -1
- package/src/commands/track.js +9 -3
- package/src/commands/update.js +3 -2
- package/src/formatters/discipline/markdown.js +16 -1
- package/src/formatters/job/description.js +2 -7
- package/src/formatters/track/markdown.js +14 -0
- package/src/lib/yaml-loader.js +9 -3
- package/src/lib/template-loader.js +0 -93
package/README.md
CHANGED
|
@@ -7,15 +7,15 @@ teams.
|
|
|
7
7
|
|
|
8
8
|
Pathway is the primary interface for interacting with engineering competency
|
|
9
9
|
data. It provides tools for browsing career paths, generating job descriptions,
|
|
10
|
-
|
|
11
|
-
experience and command line.
|
|
10
|
+
generating agent teams and skills, and preparing interviews—all from a unified
|
|
11
|
+
web experience and command line.
|
|
12
12
|
|
|
13
13
|
## What It Does
|
|
14
14
|
|
|
15
15
|
- **Web application** — Interactive browser for jobs, skills, and career paths
|
|
16
16
|
- **CLI tools** — Command-line access to all functionality
|
|
17
|
-
- **Agent
|
|
18
|
-
|
|
17
|
+
- **Agent teams** — Generate VS Code Custom Agent teams (`.agent.md`) and skills
|
|
18
|
+
(`SKILL.md`)
|
|
19
19
|
- **Interview prep** — Build interview question sets by role
|
|
20
20
|
- **Static site** — Export everything as a static site
|
|
21
21
|
|
|
@@ -29,24 +29,24 @@ npx fit-pathway serve
|
|
|
29
29
|
npx fit-pathway skill --list
|
|
30
30
|
npx fit-pathway job software_engineering senior --track=platform
|
|
31
31
|
|
|
32
|
-
# Generate agent
|
|
32
|
+
# Generate agent teams and skills
|
|
33
33
|
npx fit-pathway agent software_engineering --track=platform --output=./.github/agents
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## CLI Commands
|
|
37
37
|
|
|
38
|
-
| Command | Description
|
|
39
|
-
| ----------- |
|
|
40
|
-
| `serve` | Start web server
|
|
41
|
-
| `site` | Generate static site
|
|
42
|
-
| `init` | Create data directory
|
|
43
|
-
| `skill` | Browse skills
|
|
44
|
-
| `behaviour` | Browse behaviours
|
|
45
|
-
| `job` | Generate job definitions
|
|
46
|
-
| `agent` | Generate agent
|
|
47
|
-
| `interview` | Generate interview questions
|
|
48
|
-
| `progress` | Analyze career progression
|
|
49
|
-
| `questions` | Browse interview questions
|
|
38
|
+
| Command | Description |
|
|
39
|
+
| ----------- | ------------------------------- |
|
|
40
|
+
| `serve` | Start web server |
|
|
41
|
+
| `site` | Generate static site |
|
|
42
|
+
| `init` | Create data directory |
|
|
43
|
+
| `skill` | Browse skills |
|
|
44
|
+
| `behaviour` | Browse behaviours |
|
|
45
|
+
| `job` | Generate job definitions |
|
|
46
|
+
| `agent` | Generate agent teams and skills |
|
|
47
|
+
| `interview` | Generate interview questions |
|
|
48
|
+
| `progress` | Analyze career progression |
|
|
49
|
+
| `questions` | Browse interview questions |
|
|
50
50
|
|
|
51
51
|
Use `--help` with any command for full options.
|
|
52
52
|
|
|
@@ -56,7 +56,7 @@ Use `--help` with any command for full options.
|
|
|
56
56
|
- **Skill Browser** — View all skills with proficiency descriptions
|
|
57
57
|
- **Career Progression** — Compare levels and identify growth areas
|
|
58
58
|
- **Interview Prep** — Generate role-specific question sets
|
|
59
|
-
- **Agent Preview** — Preview generated agent
|
|
59
|
+
- **Agent Preview** — Preview generated agent teams and skills
|
|
60
60
|
|
|
61
61
|
## Package Exports
|
|
62
62
|
|
|
@@ -65,4 +65,4 @@ import { formatSkillForMarkdown } from "@forwardimpact/pathway/formatters";
|
|
|
65
65
|
import { runCommand } from "@forwardimpact/pathway/commands";
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
See the [documentation](../../docs/pathway/index.md) for usage details.
|
|
68
|
+
See the [documentation](../../website/docs/pathway/index.md) for usage details.
|
package/bin/fit-pathway.js
CHANGED
|
@@ -29,11 +29,14 @@
|
|
|
29
29
|
* --help Show help
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import { join, resolve } from "path";
|
|
32
|
+
import { join, resolve, dirname } from "path";
|
|
33
33
|
import { existsSync } from "fs";
|
|
34
34
|
import { homedir } from "os";
|
|
35
|
-
import {
|
|
35
|
+
import { fileURLToPath } from "url";
|
|
36
|
+
import { createDataLoader } from "@forwardimpact/map/loader";
|
|
37
|
+
import { validateAllData } from "@forwardimpact/map/validation";
|
|
36
38
|
import { formatError } from "../src/lib/cli-output.js";
|
|
39
|
+
import { createTemplateLoader } from "@forwardimpact/libtemplate";
|
|
37
40
|
|
|
38
41
|
// Import command handlers
|
|
39
42
|
import { runDisciplineCommand } from "../src/commands/discipline.js";
|
|
@@ -54,6 +57,9 @@ import { runInitCommand } from "../src/commands/init.js";
|
|
|
54
57
|
import { runBuildCommand } from "../src/commands/build.js";
|
|
55
58
|
import { runUpdateCommand } from "../src/commands/update.js";
|
|
56
59
|
|
|
60
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
61
|
+
const TEMPLATE_DIR = join(__dirname, "..", "templates");
|
|
62
|
+
|
|
57
63
|
const COMMANDS = {
|
|
58
64
|
discipline: runDisciplineCommand,
|
|
59
65
|
level: runLevelCommand,
|
|
@@ -117,7 +123,9 @@ Generate job definitions from discipline × level × track combinations.
|
|
|
117
123
|
|
|
118
124
|
Usage:
|
|
119
125
|
npx fit-pathway job Summary with stats
|
|
126
|
+
npx fit-pathway job --track=<track> Summary filtered by track
|
|
120
127
|
npx fit-pathway job --list All valid combinations
|
|
128
|
+
npx fit-pathway job --list --track=<track> Combinations for a track
|
|
121
129
|
npx fit-pathway job <discipline> <level> Detail view (trackless)
|
|
122
130
|
npx fit-pathway job <d> <l> --track=<track> Detail view (with track)
|
|
123
131
|
npx fit-pathway job <d> <l> --skills Plain list of skill IDs
|
|
@@ -126,15 +134,17 @@ Usage:
|
|
|
126
134
|
|
|
127
135
|
Options:
|
|
128
136
|
--track=TRACK Track specialization (e.g., platform, forward_deployed)
|
|
137
|
+
Also filters --list and summary modes
|
|
129
138
|
--skills Output plain list of skill IDs (for piping)
|
|
130
139
|
--tools Output plain list of tool names (for piping)
|
|
131
140
|
--checklist=STAGE Show checklist for stage handoff (plan, code)
|
|
132
141
|
|
|
133
142
|
Examples:
|
|
134
|
-
npx fit-pathway job
|
|
135
|
-
npx fit-pathway job
|
|
136
|
-
npx fit-pathway job
|
|
137
|
-
npx fit-pathway job
|
|
143
|
+
npx fit-pathway job # overview of all jobs
|
|
144
|
+
npx fit-pathway job --track=forward_deployed # jobs on a specific track
|
|
145
|
+
npx fit-pathway job --list --track=forward_deployed # list for piping
|
|
146
|
+
npx fit-pathway job software_engineering J060 # trackless job detail
|
|
147
|
+
npx fit-pathway job software_engineering J060 --track=platform # with track
|
|
138
148
|
|
|
139
149
|
────────────────────────────────────────────────────────────────────────────────
|
|
140
150
|
AGENT COMMAND
|
|
@@ -231,9 +241,7 @@ function parseArgs(args) {
|
|
|
231
241
|
type: "full",
|
|
232
242
|
compare: null,
|
|
233
243
|
data: null,
|
|
234
|
-
// Shared command options
|
|
235
244
|
track: null,
|
|
236
|
-
// Questions command options
|
|
237
245
|
level: null,
|
|
238
246
|
maturity: null,
|
|
239
247
|
skill: null,
|
|
@@ -241,20 +249,15 @@ function parseArgs(args) {
|
|
|
241
249
|
capability: null,
|
|
242
250
|
format: null,
|
|
243
251
|
stats: false,
|
|
244
|
-
// Job command options
|
|
245
252
|
checklist: null,
|
|
246
253
|
skills: false,
|
|
247
254
|
tools: false,
|
|
248
|
-
// Agent command options
|
|
249
255
|
output: null,
|
|
250
256
|
stage: null,
|
|
251
257
|
"all-stages": false,
|
|
252
258
|
agent: false,
|
|
253
|
-
// Serve command options
|
|
254
259
|
port: null,
|
|
255
|
-
// Init command options
|
|
256
260
|
path: null,
|
|
257
|
-
// Site command options
|
|
258
261
|
clean: true,
|
|
259
262
|
url: null,
|
|
260
263
|
};
|
|
@@ -335,54 +338,45 @@ function printHelp() {
|
|
|
335
338
|
|
|
336
339
|
/**
|
|
337
340
|
* Resolve the data directory path.
|
|
338
|
-
* Resolution order:
|
|
339
|
-
* 1. --data=<path> flag (explicit override)
|
|
340
|
-
* 2. PATHWAY_DATA environment variable
|
|
341
|
-
* 3. ~/.fit/pathway/data/ (home directory install)
|
|
342
|
-
* 4. ./data/ relative to current working directory
|
|
343
|
-
* 5. ./examples/ relative to current working directory
|
|
344
|
-
* 6. products/map/examples/ for monorepo development
|
|
345
|
-
*
|
|
346
341
|
* @param {Object} options - Parsed command options
|
|
347
342
|
* @returns {string} Resolved absolute path to data directory
|
|
348
343
|
*/
|
|
349
344
|
function resolveDataPath(options) {
|
|
350
|
-
// 1. Explicit flag
|
|
351
345
|
if (options.data) {
|
|
352
346
|
return resolve(options.data);
|
|
353
347
|
}
|
|
354
348
|
|
|
355
|
-
// 2. Environment variable
|
|
356
349
|
if (process.env.PATHWAY_DATA) {
|
|
357
350
|
return resolve(process.env.PATHWAY_DATA);
|
|
358
351
|
}
|
|
359
352
|
|
|
360
|
-
// 3. Home directory install (~/.fit/pathway/data/)
|
|
361
353
|
const homeData = join(homedir(), ".fit", "pathway", "data");
|
|
362
354
|
if (existsSync(homeData)) {
|
|
363
355
|
return homeData;
|
|
364
356
|
}
|
|
365
357
|
|
|
366
|
-
|
|
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
|
+
|
|
367
368
|
const cwdData = join(process.cwd(), "data");
|
|
368
369
|
if (existsSync(cwdData)) {
|
|
369
370
|
return cwdData;
|
|
370
371
|
}
|
|
371
372
|
|
|
372
|
-
// 5. Current working directory ./examples/
|
|
373
373
|
const cwdExamples = join(process.cwd(), "examples");
|
|
374
374
|
if (existsSync(cwdExamples)) {
|
|
375
375
|
return cwdExamples;
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
// 6. Monorepo: products/map/examples/
|
|
379
|
-
const mapExamples = join(process.cwd(), "products/map/examples");
|
|
380
|
-
if (existsSync(mapExamples)) {
|
|
381
|
-
return mapExamples;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
378
|
throw new Error(
|
|
385
|
-
"No data directory found. Create ./data/ or use --data=<path>",
|
|
379
|
+
"No data directory found. Create ./data/pathway/ or use --data=<path>",
|
|
386
380
|
);
|
|
387
381
|
}
|
|
388
382
|
|
|
@@ -398,7 +392,6 @@ async function main() {
|
|
|
398
392
|
process.exit(0);
|
|
399
393
|
}
|
|
400
394
|
|
|
401
|
-
// No command: show help
|
|
402
395
|
if (!options.command) {
|
|
403
396
|
printHelp();
|
|
404
397
|
process.exit(0);
|
|
@@ -406,7 +399,6 @@ async function main() {
|
|
|
406
399
|
|
|
407
400
|
const command = options.command;
|
|
408
401
|
|
|
409
|
-
// Handle init command (doesn't need data directory to exist)
|
|
410
402
|
if (command === "init") {
|
|
411
403
|
await runInitCommand({ options });
|
|
412
404
|
process.exit(0);
|
|
@@ -414,20 +406,16 @@ async function main() {
|
|
|
414
406
|
|
|
415
407
|
const dataDir = resolveDataPath(options);
|
|
416
408
|
|
|
417
|
-
// Handle dev command (needs data directory)
|
|
418
409
|
if (command === "dev") {
|
|
419
410
|
await runDevCommand({ dataDir, options });
|
|
420
|
-
// dev doesn't exit, keeps running
|
|
421
411
|
return;
|
|
422
412
|
}
|
|
423
413
|
|
|
424
|
-
// Handle build command (generates static site)
|
|
425
414
|
if (command === "build") {
|
|
426
415
|
await runBuildCommand({ dataDir, options });
|
|
427
416
|
process.exit(0);
|
|
428
417
|
}
|
|
429
418
|
|
|
430
|
-
// Handle update command (re-downloads bundle for local install)
|
|
431
419
|
if (command === "update") {
|
|
432
420
|
await runUpdateCommand({ dataDir, options });
|
|
433
421
|
process.exit(0);
|
|
@@ -442,12 +430,20 @@ async function main() {
|
|
|
442
430
|
}
|
|
443
431
|
|
|
444
432
|
try {
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
433
|
+
const loader = createDataLoader();
|
|
434
|
+
const templateLoader = createTemplateLoader(TEMPLATE_DIR);
|
|
435
|
+
|
|
436
|
+
const data = await loader.loadAllData(dataDir);
|
|
437
|
+
validateAllData(data);
|
|
438
|
+
|
|
439
|
+
await handler({
|
|
440
|
+
data,
|
|
441
|
+
args: options.args,
|
|
442
|
+
options,
|
|
443
|
+
dataDir,
|
|
444
|
+
templateLoader,
|
|
445
|
+
loader,
|
|
448
446
|
});
|
|
449
|
-
|
|
450
|
-
await handler({ data, args: options.args, options, dataDir });
|
|
451
447
|
} catch (error) {
|
|
452
448
|
console.error(formatError(error.message));
|
|
453
449
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/pathway",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.2",
|
|
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": {
|
|
@@ -40,8 +40,9 @@
|
|
|
40
40
|
"./commands": "./src/commands/index.js"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@forwardimpact/map": "^0.
|
|
43
|
+
"@forwardimpact/map": "^0.13.0",
|
|
44
44
|
"@forwardimpact/libskill": "^3.0.0",
|
|
45
|
+
"@forwardimpact/libtemplate": "^0.2.0",
|
|
45
46
|
"@forwardimpact/libui": "^1.0.0",
|
|
46
47
|
"mustache": "^4.2.0",
|
|
47
48
|
"simple-icons": "^16.7.0",
|
|
@@ -49,5 +50,8 @@
|
|
|
49
50
|
},
|
|
50
51
|
"engines": {
|
|
51
52
|
"node": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
52
56
|
}
|
|
53
57
|
}
|
package/src/commands/agent.js
CHANGED
|
@@ -27,10 +27,7 @@ import { writeFile, mkdir, readFile } from "fs/promises";
|
|
|
27
27
|
import { join, dirname } from "path";
|
|
28
28
|
import { existsSync } from "fs";
|
|
29
29
|
import { stringify as stringifyYaml } from "yaml";
|
|
30
|
-
import {
|
|
31
|
-
loadAgentData,
|
|
32
|
-
loadSkillsWithAgentData,
|
|
33
|
-
} from "@forwardimpact/map/loader";
|
|
30
|
+
import { createDataLoader } from "@forwardimpact/map/loader";
|
|
34
31
|
import {
|
|
35
32
|
generateStageAgentProfile,
|
|
36
33
|
validateAgentProfile,
|
|
@@ -50,12 +47,6 @@ import {
|
|
|
50
47
|
formatReference,
|
|
51
48
|
} from "../formatters/agent/skill.js";
|
|
52
49
|
import { formatError, formatSuccess } from "../lib/cli-output.js";
|
|
53
|
-
import {
|
|
54
|
-
loadAgentTemplate,
|
|
55
|
-
loadSkillTemplate,
|
|
56
|
-
loadSkillInstallTemplate,
|
|
57
|
-
loadSkillReferenceTemplate,
|
|
58
|
-
} from "../lib/template-loader.js";
|
|
59
50
|
import { toolkitToPlainList } from "../formatters/toolkit/markdown.js";
|
|
60
51
|
|
|
61
52
|
/**
|
|
@@ -209,8 +200,7 @@ function listAgentCombinations(data, agentData, verbose = false) {
|
|
|
209
200
|
if (humanDiscipline && humanTrack) {
|
|
210
201
|
const abbrev = getDisciplineAbbreviation(discipline.id);
|
|
211
202
|
const agentName = `${abbrev}-${toKebabCase(track.id)}`;
|
|
212
|
-
const specName =
|
|
213
|
-
humanDiscipline.specialization || humanDiscipline.id;
|
|
203
|
+
const specName = humanDiscipline.specialization || humanDiscipline.id;
|
|
214
204
|
console.log(
|
|
215
205
|
`${agentName} ${discipline.id} ${track.id}, ${specName} (${humanTrack.name})`,
|
|
216
206
|
);
|
|
@@ -326,10 +316,18 @@ async function writeSkills(skills, baseDir, templates) {
|
|
|
326
316
|
* @param {Object} params.options - Command options
|
|
327
317
|
* @param {string} params.dataDir - Path to data directory
|
|
328
318
|
*/
|
|
329
|
-
export async function runAgentCommand({
|
|
319
|
+
export async function runAgentCommand({
|
|
320
|
+
data,
|
|
321
|
+
args,
|
|
322
|
+
options,
|
|
323
|
+
dataDir,
|
|
324
|
+
templateLoader,
|
|
325
|
+
loader,
|
|
326
|
+
}) {
|
|
330
327
|
// Load agent-specific data
|
|
331
|
-
const
|
|
332
|
-
const
|
|
328
|
+
const dataLoader = loader || createDataLoader();
|
|
329
|
+
const agentData = await dataLoader.loadAgentData(dataDir);
|
|
330
|
+
const skillsWithAgent = await dataLoader.loadSkillsWithAgentData(dataDir);
|
|
333
331
|
|
|
334
332
|
// --list: Output clean lines for piping
|
|
335
333
|
if (options.list) {
|
|
@@ -501,7 +499,7 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
|
|
|
501
499
|
}
|
|
502
500
|
|
|
503
501
|
// Load template
|
|
504
|
-
const agentTemplate =
|
|
502
|
+
const agentTemplate = templateLoader.load("agent.template.md", dataDir);
|
|
505
503
|
|
|
506
504
|
// Output to console (default) or write to files (with --output)
|
|
507
505
|
if (!options.output) {
|
|
@@ -576,10 +574,16 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
|
|
|
576
574
|
}
|
|
577
575
|
|
|
578
576
|
// Load templates
|
|
579
|
-
const agentTemplate =
|
|
580
|
-
const skillTemplate =
|
|
581
|
-
const installTemplate =
|
|
582
|
-
|
|
577
|
+
const agentTemplate = templateLoader.load("agent.template.md", dataDir);
|
|
578
|
+
const skillTemplate = templateLoader.load("skill.template.md", dataDir);
|
|
579
|
+
const installTemplate = templateLoader.load(
|
|
580
|
+
"skill-install.template.sh",
|
|
581
|
+
dataDir,
|
|
582
|
+
);
|
|
583
|
+
const referenceTemplate = templateLoader.load(
|
|
584
|
+
"skill-reference.template.md",
|
|
585
|
+
dataDir,
|
|
586
|
+
);
|
|
583
587
|
const skillTemplates = {
|
|
584
588
|
skill: skillTemplate,
|
|
585
589
|
install: installTemplate,
|
package/src/commands/build.js
CHANGED
|
@@ -21,8 +21,8 @@ import { join, dirname, relative, resolve } from "path";
|
|
|
21
21
|
import { fileURLToPath } from "url";
|
|
22
22
|
import { execFileSync } from "child_process";
|
|
23
23
|
import Mustache from "mustache";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import { createIndexGenerator } from "@forwardimpact/map/index-generator";
|
|
25
|
+
import { createDataLoader } from "@forwardimpact/map/loader";
|
|
26
26
|
|
|
27
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
28
|
const __dirname = dirname(__filename);
|
|
@@ -85,7 +85,8 @@ export async function runBuildCommand({ dataDir, options }) {
|
|
|
85
85
|
// Load framework config for display
|
|
86
86
|
let framework;
|
|
87
87
|
try {
|
|
88
|
-
|
|
88
|
+
const loader = createDataLoader();
|
|
89
|
+
framework = await loader.loadFrameworkConfig(dataDir);
|
|
89
90
|
} catch {
|
|
90
91
|
framework = { emojiIcon: "🚀", title: "Engineering Pathway" };
|
|
91
92
|
}
|
|
@@ -110,7 +111,8 @@ ${framework.emojiIcon} Generating ${framework.title} static site...
|
|
|
110
111
|
|
|
111
112
|
// Generate index files in data directory
|
|
112
113
|
console.log("📇 Generating index files...");
|
|
113
|
-
|
|
114
|
+
const indexGenerator = createIndexGenerator();
|
|
115
|
+
await indexGenerator.generateAllIndexes(dataDir);
|
|
114
116
|
|
|
115
117
|
// Copy app assets
|
|
116
118
|
console.log("📦 Copying application files...");
|
package/src/commands/dev.js
CHANGED
|
@@ -9,8 +9,8 @@ import { createServer } from "http";
|
|
|
9
9
|
import { readFile, stat } from "fs/promises";
|
|
10
10
|
import { join, extname, dirname } from "path";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { createIndexGenerator } from "@forwardimpact/map/index-generator";
|
|
13
|
+
import { createDataLoader } from "@forwardimpact/map/loader";
|
|
14
14
|
|
|
15
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
16
|
const __dirname = dirname(__filename);
|
|
@@ -108,7 +108,8 @@ export async function runDevCommand({ dataDir, options }) {
|
|
|
108
108
|
// Load framework config for display
|
|
109
109
|
let framework;
|
|
110
110
|
try {
|
|
111
|
-
|
|
111
|
+
const loader = createDataLoader();
|
|
112
|
+
framework = await loader.loadFrameworkConfig(dataDir);
|
|
112
113
|
} catch {
|
|
113
114
|
// Fallback if framework config fails
|
|
114
115
|
framework = { emojiIcon: "🚀", title: "Engineering Pathway" };
|
|
@@ -116,7 +117,8 @@ export async function runDevCommand({ dataDir, options }) {
|
|
|
116
117
|
|
|
117
118
|
// Generate _index.yaml files before serving
|
|
118
119
|
console.log("Generating index files...");
|
|
119
|
-
|
|
120
|
+
const indexGenerator = createIndexGenerator();
|
|
121
|
+
await indexGenerator.generateAllIndexes(dataDir);
|
|
120
122
|
|
|
121
123
|
const server = createServer(async (req, res) => {
|
|
122
124
|
const url = new URL(req.url, `http://localhost:${port}`);
|
|
@@ -20,7 +20,11 @@ import { formatTable } from "../lib/cli-output.js";
|
|
|
20
20
|
* @returns {string} Formatted list line
|
|
21
21
|
*/
|
|
22
22
|
function formatListItem(discipline) {
|
|
23
|
-
|
|
23
|
+
const type = discipline.isProfessional ? "professional" : "management";
|
|
24
|
+
const tracks = (discipline.validTracks || [])
|
|
25
|
+
.filter((t) => t !== null)
|
|
26
|
+
.join("|");
|
|
27
|
+
return `${discipline.id}, ${discipline.specialization || discipline.id}, ${type}, ${tracks || "—"}`;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -30,21 +34,14 @@ function formatListItem(discipline) {
|
|
|
30
34
|
function formatSummary(disciplines) {
|
|
31
35
|
console.log(`\n📋 Disciplines\n`);
|
|
32
36
|
|
|
33
|
-
const rows = disciplines.map((d) =>
|
|
34
|
-
d.
|
|
35
|
-
d.
|
|
36
|
-
|
|
37
|
-
d.
|
|
38
|
-
|
|
39
|
-
d.broadSkills?.length || 0,
|
|
40
|
-
]);
|
|
37
|
+
const rows = disciplines.map((d) => {
|
|
38
|
+
const type = d.isProfessional ? "Professional" : "Management";
|
|
39
|
+
const validTracks = (d.validTracks || []).filter((t) => t !== null);
|
|
40
|
+
const trackStr = validTracks.length > 0 ? validTracks.join(", ") : "—";
|
|
41
|
+
return [d.id, d.specialization || d.id, type, trackStr];
|
|
42
|
+
});
|
|
41
43
|
|
|
42
|
-
console.log(
|
|
43
|
-
formatTable(
|
|
44
|
-
["ID", "Specialization", "Role Title", "Core", "Supporting", "Broad"],
|
|
45
|
-
rows,
|
|
46
|
-
),
|
|
47
|
-
);
|
|
44
|
+
console.log(formatTable(["ID", "Specialization", "Type", "Tracks"], rows));
|
|
48
45
|
console.log(`\nTotal: ${disciplines.length} disciplines`);
|
|
49
46
|
console.log(`\nRun 'npx pathway discipline --list' for IDs and names`);
|
|
50
47
|
console.log(`Run 'npx pathway discipline <id>' for details\n`);
|
|
@@ -55,8 +52,8 @@ function formatSummary(disciplines) {
|
|
|
55
52
|
* @param {Object} viewAndContext - Contains discipline entity and context
|
|
56
53
|
*/
|
|
57
54
|
function formatDetail(viewAndContext) {
|
|
58
|
-
const { discipline, skills, behaviours } = viewAndContext;
|
|
59
|
-
console.log(disciplineToMarkdown(discipline, { skills, behaviours }));
|
|
55
|
+
const { discipline, skills, behaviours, tracks } = viewAndContext;
|
|
56
|
+
console.log(disciplineToMarkdown(discipline, { skills, behaviours, tracks }));
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
export const runDisciplineCommand = createEntityCommand({
|
|
@@ -67,6 +64,7 @@ export const runDisciplineCommand = createEntityCommand({
|
|
|
67
64
|
discipline: entity,
|
|
68
65
|
skills: data.skills,
|
|
69
66
|
behaviours: data.behaviours,
|
|
67
|
+
tracks: data.tracks,
|
|
70
68
|
}),
|
|
71
69
|
formatSummary,
|
|
72
70
|
formatDetail,
|
package/src/commands/init.js
CHANGED
|
@@ -10,7 +10,17 @@ import { fileURLToPath } from "url";
|
|
|
10
10
|
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
12
|
const __dirname = dirname(__filename);
|
|
13
|
-
|
|
13
|
+
// Prefer monorepo root examples/framework/, fall back to legacy co-located examples/
|
|
14
|
+
const monorepoExamplesDir = join(
|
|
15
|
+
__dirname,
|
|
16
|
+
"..",
|
|
17
|
+
"..",
|
|
18
|
+
"..",
|
|
19
|
+
"..",
|
|
20
|
+
"examples",
|
|
21
|
+
"framework",
|
|
22
|
+
);
|
|
23
|
+
const legacyExamplesDir = join(__dirname, "..", "..", "examples");
|
|
14
24
|
|
|
15
25
|
/**
|
|
16
26
|
* Run the init command
|
|
@@ -31,13 +41,20 @@ export async function runInitCommand({ options }) {
|
|
|
31
41
|
// Directory doesn't exist, proceed
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
//
|
|
44
|
+
// Find examples directory — monorepo root first, then legacy
|
|
45
|
+
let examplesDir;
|
|
35
46
|
try {
|
|
36
|
-
await access(
|
|
47
|
+
await access(monorepoExamplesDir);
|
|
48
|
+
examplesDir = monorepoExamplesDir;
|
|
37
49
|
} catch {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
try {
|
|
51
|
+
await access(legacyExamplesDir);
|
|
52
|
+
examplesDir = legacyExamplesDir;
|
|
53
|
+
} catch {
|
|
54
|
+
console.error("Error: Examples directory not found in package.");
|
|
55
|
+
console.error("This may indicate a corrupted package installation.");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
41
58
|
}
|
|
42
59
|
|
|
43
60
|
// Copy example data
|
package/src/commands/job.js
CHANGED
|
@@ -16,13 +16,15 @@
|
|
|
16
16
|
|
|
17
17
|
import { prepareJobDetail } from "@forwardimpact/libskill/job";
|
|
18
18
|
import { jobToMarkdown } from "../formatters/job/markdown.js";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
generateJobTitle,
|
|
21
|
+
generateAllJobs,
|
|
22
|
+
} from "@forwardimpact/libskill/derivation";
|
|
20
23
|
import { formatTable } from "../lib/cli-output.js";
|
|
21
24
|
import {
|
|
22
25
|
deriveChecklist,
|
|
23
26
|
formatChecklistMarkdown,
|
|
24
27
|
} from "@forwardimpact/libskill/checklist";
|
|
25
|
-
import { loadJobTemplate } from "../lib/template-loader.js";
|
|
26
28
|
import { toolkitToPlainList } from "../formatters/toolkit/markdown.js";
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -44,7 +46,13 @@ function formatJob(view, _options, entities, jobTemplate) {
|
|
|
44
46
|
* @param {Object} params.options - Command options
|
|
45
47
|
* @param {string} params.dataDir - Path to data directory
|
|
46
48
|
*/
|
|
47
|
-
export async function runJobCommand({
|
|
49
|
+
export async function runJobCommand({
|
|
50
|
+
data,
|
|
51
|
+
args,
|
|
52
|
+
options,
|
|
53
|
+
dataDir,
|
|
54
|
+
templateLoader,
|
|
55
|
+
}) {
|
|
48
56
|
const jobs = generateAllJobs({
|
|
49
57
|
disciplines: data.disciplines,
|
|
50
58
|
levels: data.levels,
|
|
@@ -54,9 +62,33 @@ export async function runJobCommand({ data, args, options, dataDir }) {
|
|
|
54
62
|
validationRules: data.framework.validationRules,
|
|
55
63
|
});
|
|
56
64
|
|
|
65
|
+
// Apply --track filter to list and summary modes
|
|
66
|
+
const filteredJobs = options.track
|
|
67
|
+
? jobs.filter((j) => j.track && j.track.id === options.track)
|
|
68
|
+
: jobs;
|
|
69
|
+
|
|
70
|
+
if (options.track && filteredJobs.length === 0 && args.length === 0) {
|
|
71
|
+
const trackExists = data.tracks.some((t) => t.id === options.track);
|
|
72
|
+
if (!trackExists) {
|
|
73
|
+
console.error(`Track not found: ${options.track}`);
|
|
74
|
+
console.error(`Available: ${data.tracks.map((t) => t.id).join(", ")}`);
|
|
75
|
+
} else {
|
|
76
|
+
console.error(`No jobs found for track: ${options.track}`);
|
|
77
|
+
const trackDisciplines = data.disciplines
|
|
78
|
+
.filter((d) => d.validTracks && d.validTracks.includes(options.track))
|
|
79
|
+
.map((d) => d.id);
|
|
80
|
+
if (trackDisciplines.length > 0) {
|
|
81
|
+
console.error(
|
|
82
|
+
`Disciplines with this track: ${trackDisciplines.join(", ")}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
57
89
|
// --list: Output descriptive comma-separated lines for piping and AI agent discovery
|
|
58
90
|
if (options.list) {
|
|
59
|
-
for (const job of
|
|
91
|
+
for (const job of filteredJobs) {
|
|
60
92
|
const title = generateJobTitle(job.discipline, job.level, job.track);
|
|
61
93
|
if (job.track) {
|
|
62
94
|
console.log(
|
|
@@ -71,29 +103,40 @@ export async function runJobCommand({ data, args, options, dataDir }) {
|
|
|
71
103
|
|
|
72
104
|
// No args: Show summary
|
|
73
105
|
if (args.length === 0) {
|
|
74
|
-
|
|
106
|
+
const trackLabel = options.track ? ` — ${options.track}` : "";
|
|
107
|
+
console.log(`\n💼 Jobs${trackLabel}\n`);
|
|
75
108
|
|
|
76
|
-
// Count by discipline with name
|
|
109
|
+
// Count by discipline with name, grouped by track
|
|
77
110
|
const byDiscipline = {};
|
|
78
|
-
for (const job of
|
|
111
|
+
for (const job of filteredJobs) {
|
|
79
112
|
const key = job.discipline.id;
|
|
80
113
|
if (!byDiscipline[key]) {
|
|
81
114
|
byDiscipline[key] = {
|
|
82
115
|
name: job.discipline.specialization || job.discipline.id,
|
|
116
|
+
roleTitle: job.discipline.roleTitle || job.discipline.id,
|
|
117
|
+
type: job.discipline.isProfessional ? "Professional" : "Management",
|
|
118
|
+
tracks: new Set(),
|
|
83
119
|
count: 0,
|
|
84
120
|
};
|
|
85
121
|
}
|
|
122
|
+
if (job.track) byDiscipline[key].tracks.add(job.track.id);
|
|
86
123
|
byDiscipline[key].count++;
|
|
87
124
|
}
|
|
88
125
|
|
|
89
126
|
const rows = Object.entries(byDiscipline).map(([id, info]) => [
|
|
90
127
|
id,
|
|
91
128
|
info.name,
|
|
129
|
+
info.type,
|
|
92
130
|
info.count,
|
|
131
|
+
info.tracks.size > 0 ? [...info.tracks].join(", ") : "—",
|
|
93
132
|
]);
|
|
94
|
-
console.log(
|
|
95
|
-
|
|
96
|
-
|
|
133
|
+
console.log(
|
|
134
|
+
formatTable(["ID", "Specialization", "Type", "Jobs", "Tracks"], rows),
|
|
135
|
+
);
|
|
136
|
+
console.log(`\nTotal: ${filteredJobs.length} valid job combinations`);
|
|
137
|
+
console.log(
|
|
138
|
+
`\nRun 'npx pathway job --list' for all combinations with titles`,
|
|
139
|
+
);
|
|
97
140
|
console.log(
|
|
98
141
|
`Run 'npx pathway job <discipline> <level> [--track=<track>]' for details\n`,
|
|
99
142
|
);
|
|
@@ -102,14 +145,28 @@ export async function runJobCommand({ data, args, options, dataDir }) {
|
|
|
102
145
|
|
|
103
146
|
// Handle job detail view - requires discipline and level
|
|
104
147
|
if (args.length < 2) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
148
|
+
// Check if the single arg is a level or track, hinting at what's missing
|
|
149
|
+
const arg = args[0];
|
|
150
|
+
const isLevel = data.levels.some((g) => g.id === arg);
|
|
151
|
+
const isTrack = data.tracks.some((t) => t.id === arg);
|
|
152
|
+
if (isLevel) {
|
|
153
|
+
console.error(
|
|
154
|
+
`Missing discipline. Usage: npx pathway job <discipline> ${arg} [--track=<track>]`,
|
|
155
|
+
);
|
|
156
|
+
console.error(
|
|
157
|
+
`Disciplines: ${data.disciplines.map((d) => d.id).join(", ")}`,
|
|
158
|
+
);
|
|
159
|
+
} else if (isTrack) {
|
|
160
|
+
console.error(`Track must be passed as a flag: --track=${arg}`);
|
|
161
|
+
console.error(
|
|
162
|
+
`Usage: npx pathway job <discipline> <level> --track=${arg}`,
|
|
163
|
+
);
|
|
164
|
+
} else {
|
|
165
|
+
console.error(
|
|
166
|
+
"Usage: npx pathway job <discipline> <level> [--track=<track>]",
|
|
167
|
+
);
|
|
168
|
+
console.error(" npx pathway job --list");
|
|
169
|
+
}
|
|
113
170
|
process.exit(1);
|
|
114
171
|
}
|
|
115
172
|
|
|
@@ -120,14 +177,36 @@ export async function runJobCommand({ data, args, options, dataDir }) {
|
|
|
120
177
|
: null;
|
|
121
178
|
|
|
122
179
|
if (!discipline) {
|
|
123
|
-
|
|
124
|
-
|
|
180
|
+
// Check if args are swapped (level first, discipline second)
|
|
181
|
+
const maybeLevel = data.levels.find((g) => g.id === args[0]);
|
|
182
|
+
const maybeDiscipline = data.disciplines.find((d) => d.id === args[1]);
|
|
183
|
+
if (maybeLevel && maybeDiscipline) {
|
|
184
|
+
console.error(`Arguments are in the wrong order. Try:`);
|
|
185
|
+
console.error(
|
|
186
|
+
` npx pathway job ${args[1]} ${args[0]}${options.track ? ` --track=${options.track}` : ""}`,
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
console.error(`Discipline not found: ${args[0]}`);
|
|
190
|
+
console.error(
|
|
191
|
+
`Available: ${data.disciplines.map((d) => d.id).join(", ")}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
125
194
|
process.exit(1);
|
|
126
195
|
}
|
|
127
196
|
|
|
128
197
|
if (!level) {
|
|
129
|
-
|
|
130
|
-
|
|
198
|
+
// Check if the second arg is a track ID passed as positional
|
|
199
|
+
const isTrack = data.tracks.some((t) => t.id === args[1]);
|
|
200
|
+
if (isTrack) {
|
|
201
|
+
console.error(
|
|
202
|
+
`Track must be passed as a flag, not a positional argument:`,
|
|
203
|
+
);
|
|
204
|
+
console.error(` npx pathway job ${args[0]} <level> --track=${args[1]}`);
|
|
205
|
+
console.error(`Levels: ${data.levels.map((g) => g.id).join(", ")}`);
|
|
206
|
+
} else {
|
|
207
|
+
console.error(`Level not found: ${args[1]}`);
|
|
208
|
+
console.error(`Available: ${data.levels.map((g) => g.id).join(", ")}`);
|
|
209
|
+
}
|
|
131
210
|
process.exit(1);
|
|
132
211
|
}
|
|
133
212
|
|
|
@@ -164,6 +243,18 @@ export async function runJobCommand({ data, args, options, dataDir }) {
|
|
|
164
243
|
console.error(`${discipline.id} does not support tracks`);
|
|
165
244
|
}
|
|
166
245
|
}
|
|
246
|
+
// Check if it's a minLevel issue
|
|
247
|
+
if (discipline.minLevel) {
|
|
248
|
+
const levelIndex = data.levels.findIndex((g) => g.id === level.id);
|
|
249
|
+
const minIndex = data.levels.findIndex(
|
|
250
|
+
(g) => g.id === discipline.minLevel,
|
|
251
|
+
);
|
|
252
|
+
if (levelIndex >= 0 && minIndex >= 0 && levelIndex < minIndex) {
|
|
253
|
+
console.error(
|
|
254
|
+
`${discipline.id} requires minimum level: ${discipline.minLevel}`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
167
258
|
process.exit(1);
|
|
168
259
|
}
|
|
169
260
|
|
|
@@ -224,6 +315,6 @@ export async function runJobCommand({ data, args, options, dataDir }) {
|
|
|
224
315
|
}
|
|
225
316
|
|
|
226
317
|
// Load job template for description formatting
|
|
227
|
-
const jobTemplate =
|
|
318
|
+
const jobTemplate = templateLoader.load("job.template.md", dataDir);
|
|
228
319
|
formatJob(view, options, { discipline, level, track }, jobTemplate);
|
|
229
320
|
}
|
package/src/commands/level.js
CHANGED
|
@@ -46,7 +46,13 @@ function formatSummary(levels, data) {
|
|
|
46
46
|
|
|
47
47
|
console.log(
|
|
48
48
|
formatTable(
|
|
49
|
-
[
|
|
49
|
+
[
|
|
50
|
+
"ID",
|
|
51
|
+
"Professional Title",
|
|
52
|
+
"Management Title",
|
|
53
|
+
"Experience",
|
|
54
|
+
"Primary Level",
|
|
55
|
+
],
|
|
50
56
|
rows,
|
|
51
57
|
),
|
|
52
58
|
);
|
package/src/commands/skill.js
CHANGED
|
@@ -18,7 +18,6 @@ import { getConceptEmoji } from "@forwardimpact/map/levels";
|
|
|
18
18
|
import { formatTable, formatError } from "../lib/cli-output.js";
|
|
19
19
|
import { generateSkillMarkdown } from "@forwardimpact/libskill/agent";
|
|
20
20
|
import { formatAgentSkill } from "../formatters/agent/skill.js";
|
|
21
|
-
import { loadSkillTemplate } from "../lib/template-loader.js";
|
|
22
21
|
|
|
23
22
|
/**
|
|
24
23
|
* Format skill summary output
|
|
@@ -69,7 +68,7 @@ function formatDetail(viewAndContext, framework) {
|
|
|
69
68
|
* @param {Array} stages - All stage entities
|
|
70
69
|
* @param {string} dataDir - Path to data directory for template loading
|
|
71
70
|
*/
|
|
72
|
-
async function formatAgentDetail(skill, stages, dataDir) {
|
|
71
|
+
async function formatAgentDetail(skill, stages, templateLoader, dataDir) {
|
|
73
72
|
if (!skill.agent) {
|
|
74
73
|
console.error(formatError(`Skill '${skill.id}' has no agent section`));
|
|
75
74
|
console.error(`\nSkills with agent support:`);
|
|
@@ -79,7 +78,7 @@ async function formatAgentDetail(skill, stages, dataDir) {
|
|
|
79
78
|
process.exit(1);
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
const template =
|
|
81
|
+
const template = templateLoader.load("skill.template.md", dataDir);
|
|
83
82
|
const skillMd = generateSkillMarkdown(skill, stages);
|
|
84
83
|
const output = formatAgentSkill(skillMd, template);
|
|
85
84
|
console.log(output);
|
|
@@ -119,7 +118,13 @@ const baseSkillCommand = createEntityCommand({
|
|
|
119
118
|
* @param {Object} params.options - Command options
|
|
120
119
|
* @param {string} params.dataDir - Path to data directory
|
|
121
120
|
*/
|
|
122
|
-
export async function runSkillCommand({
|
|
121
|
+
export async function runSkillCommand({
|
|
122
|
+
data,
|
|
123
|
+
args,
|
|
124
|
+
options,
|
|
125
|
+
dataDir,
|
|
126
|
+
templateLoader,
|
|
127
|
+
}) {
|
|
123
128
|
// Handle --agent flag for detail view
|
|
124
129
|
if (options.agent && args.length > 0) {
|
|
125
130
|
const [id] = args;
|
|
@@ -131,7 +136,7 @@ export async function runSkillCommand({ data, args, options, dataDir }) {
|
|
|
131
136
|
process.exit(1);
|
|
132
137
|
}
|
|
133
138
|
|
|
134
|
-
await formatAgentDetail(skill, data.stages, dataDir);
|
|
139
|
+
await formatAgentDetail(skill, data.stages, templateLoader, dataDir);
|
|
135
140
|
return;
|
|
136
141
|
}
|
|
137
142
|
|
package/src/commands/tool.js
CHANGED
|
@@ -88,7 +88,9 @@ function formatSummary(tools, totalCount) {
|
|
|
88
88
|
if (sorted.length > 15) {
|
|
89
89
|
console.log(`(showing top 15 by usage)`);
|
|
90
90
|
}
|
|
91
|
-
console.log(
|
|
91
|
+
console.log(
|
|
92
|
+
`\nRun 'npx pathway tool --list' for all tool names and descriptions`,
|
|
93
|
+
);
|
|
92
94
|
console.log(`Run 'npx pathway tool <name>' for details\n`);
|
|
93
95
|
}
|
|
94
96
|
|
package/src/commands/track.js
CHANGED
|
@@ -31,17 +31,23 @@ function formatListItem(track) {
|
|
|
31
31
|
* @param {Object} data - Full data context
|
|
32
32
|
*/
|
|
33
33
|
function formatSummary(tracks, data) {
|
|
34
|
-
const { framework } = data;
|
|
34
|
+
const { framework, disciplines } = data;
|
|
35
35
|
const emoji = framework ? getConceptEmoji(framework, "track") : "🛤️";
|
|
36
36
|
|
|
37
37
|
console.log(`\n${emoji} Tracks\n`);
|
|
38
38
|
|
|
39
39
|
const rows = tracks.map((t) => {
|
|
40
40
|
const modCount = Object.keys(t.skillModifiers || {}).length;
|
|
41
|
-
|
|
41
|
+
const usedBy = disciplines
|
|
42
|
+
? disciplines.filter((d) => d.validTracks && d.validTracks.includes(t.id))
|
|
43
|
+
: [];
|
|
44
|
+
const disciplineNames = usedBy
|
|
45
|
+
.map((d) => d.specialization || d.id)
|
|
46
|
+
.join(", ");
|
|
47
|
+
return [t.id, t.name, modCount, disciplineNames || "—"];
|
|
42
48
|
});
|
|
43
49
|
|
|
44
|
-
console.log(formatTable(["ID", "Name", "Modifiers"], rows));
|
|
50
|
+
console.log(formatTable(["ID", "Name", "Modifiers", "Disciplines"], rows));
|
|
45
51
|
console.log(`\nTotal: ${tracks.length} tracks`);
|
|
46
52
|
console.log(`\nRun 'npx pathway track --list' for IDs and names`);
|
|
47
53
|
console.log(`Run 'npx pathway track <id>' for details\n`);
|
package/src/commands/update.js
CHANGED
|
@@ -10,7 +10,7 @@ import { cp, mkdir, rm, readFile, writeFile, access } from "fs/promises";
|
|
|
10
10
|
import { join } from "path";
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
import { execFileSync, execSync } from "child_process";
|
|
13
|
-
import {
|
|
13
|
+
import { createDataLoader } from "@forwardimpact/map/loader";
|
|
14
14
|
|
|
15
15
|
const INSTALL_DIR = join(homedir(), ".fit", "pathway");
|
|
16
16
|
|
|
@@ -38,7 +38,8 @@ export async function runUpdateCommand({ dataDir: _dataDir, options }) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// Load framework config to get siteUrl
|
|
41
|
-
const
|
|
41
|
+
const loader = createDataLoader();
|
|
42
|
+
const framework = await loader.loadFrameworkConfig(installDataDir);
|
|
42
43
|
const siteUrl = options.url || framework.distribution?.siteUrl;
|
|
43
44
|
|
|
44
45
|
if (!siteUrl) {
|
|
@@ -35,16 +35,31 @@ export function disciplineListToMarkdown(disciplines) {
|
|
|
35
35
|
* @param {Object} context - Additional context
|
|
36
36
|
* @param {Array} context.skills - All skills
|
|
37
37
|
* @param {Array} context.behaviours - All behaviours
|
|
38
|
+
* @param {Array} [context.tracks] - All tracks (for showing valid track names)
|
|
38
39
|
* @param {boolean} [context.showBehaviourModifiers=true] - Whether to show behaviour modifiers section
|
|
39
40
|
* @returns {string}
|
|
40
41
|
*/
|
|
41
42
|
export function disciplineToMarkdown(
|
|
42
43
|
discipline,
|
|
43
|
-
{ skills, behaviours, showBehaviourModifiers = true } = {},
|
|
44
|
+
{ skills, behaviours, tracks, showBehaviourModifiers = true } = {},
|
|
44
45
|
) {
|
|
45
46
|
const view = prepareDisciplineDetail(discipline, { skills, behaviours });
|
|
47
|
+
const type = discipline.isProfessional ? "Professional" : "Management";
|
|
46
48
|
const lines = [`# 📋 ${view.name}`, "", view.description, ""];
|
|
47
49
|
|
|
50
|
+
// Type and valid tracks
|
|
51
|
+
const validTracks = (discipline.validTracks || []).filter((t) => t !== null);
|
|
52
|
+
if (validTracks.length > 0) {
|
|
53
|
+
const trackNames = validTracks.map((tid) => {
|
|
54
|
+
const track = tracks?.find((t) => t.id === tid);
|
|
55
|
+
return track ? track.name : tid;
|
|
56
|
+
});
|
|
57
|
+
lines.push(`**Type:** ${type} `);
|
|
58
|
+
lines.push(`**Valid Tracks:** ${trackNames.join(", ")}`, "");
|
|
59
|
+
} else {
|
|
60
|
+
lines.push(`**Type:** ${type}`, "");
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
// Core skills
|
|
49
64
|
if (view.coreSkills.length > 0) {
|
|
50
65
|
lines.push("## Core Skills", "");
|
|
@@ -23,14 +23,9 @@ import { trimValue, trimFields } from "../shared.js";
|
|
|
23
23
|
* @returns {Object} Data object ready for Mustache template
|
|
24
24
|
*/
|
|
25
25
|
function prepareJobDescriptionData({ job, discipline, level, track }) {
|
|
26
|
-
// Build role summary from discipline
|
|
27
|
-
const isManagement = discipline.isManagement === true;
|
|
28
|
-
let roleSummary =
|
|
29
|
-
isManagement && discipline.managementRoleSummary
|
|
30
|
-
? discipline.managementRoleSummary
|
|
31
|
-
: discipline.professionalRoleSummary || discipline.description;
|
|
32
|
-
// Replace placeholders
|
|
26
|
+
// Build role summary from discipline
|
|
33
27
|
const { roleTitle, specialization } = discipline;
|
|
28
|
+
let roleSummary = discipline.roleSummary || discipline.description;
|
|
34
29
|
roleSummary = roleSummary.replace(/\{roleTitle\}/g, roleTitle);
|
|
35
30
|
roleSummary = roleSummary.replace(/\{specialization\}/g, specialization);
|
|
36
31
|
|
|
@@ -44,6 +44,20 @@ export function trackToMarkdown(
|
|
|
44
44
|
const emoji = framework ? getConceptEmoji(framework, "track") : "🛤️";
|
|
45
45
|
const lines = [`# ${emoji} ${view.name}`, "", view.description, ""];
|
|
46
46
|
|
|
47
|
+
// Show which disciplines use this track
|
|
48
|
+
if (disciplines) {
|
|
49
|
+
const usedBy = disciplines.filter(
|
|
50
|
+
(d) => d.validTracks && d.validTracks.includes(track.id),
|
|
51
|
+
);
|
|
52
|
+
if (usedBy.length > 0) {
|
|
53
|
+
const names = usedBy.map(
|
|
54
|
+
(d) =>
|
|
55
|
+
`${d.specialization || d.id} (${d.isProfessional ? "professional" : "management"})`,
|
|
56
|
+
);
|
|
57
|
+
lines.push(`**Used by:** ${names.join(", ")}`, "");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
// Skill modifiers - show expanded skills for capabilities
|
|
48
62
|
if (view.skillModifiers.length > 0) {
|
|
49
63
|
lines.push("## Skill Modifiers", "");
|
package/src/lib/yaml-loader.js
CHANGED
|
@@ -298,9 +298,15 @@ export async function loadAgentDataBrowser(dataDir = "./data") {
|
|
|
298
298
|
loadDisciplinesFromDir(`${dataDir}/disciplines`),
|
|
299
299
|
loadTracksFromDir(`${dataDir}/tracks`),
|
|
300
300
|
loadBehavioursFromDir(`${dataDir}/behaviours`),
|
|
301
|
-
tryLoadYamlFile(`${dataDir}/vscode-settings.yaml`)
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
tryLoadYamlFile(`${dataDir}/repository/vscode-settings.yaml`).then(
|
|
302
|
+
(r) => r ?? tryLoadYamlFile(`${dataDir}/vscode-settings.yaml`),
|
|
303
|
+
),
|
|
304
|
+
tryLoadYamlFile(`${dataDir}/repository/devcontainer.yaml`).then(
|
|
305
|
+
(r) => r ?? tryLoadYamlFile(`${dataDir}/devcontainer.yaml`),
|
|
306
|
+
),
|
|
307
|
+
tryLoadYamlFile(`${dataDir}/repository/copilot-setup-steps.yaml`).then(
|
|
308
|
+
(r) => r ?? tryLoadYamlFile(`${dataDir}/copilot-setup-steps.yaml`),
|
|
309
|
+
),
|
|
304
310
|
]);
|
|
305
311
|
|
|
306
312
|
return {
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Template Loader
|
|
3
|
-
*
|
|
4
|
-
* Loads Mustache templates from the data directory with fallback to the
|
|
5
|
-
* top-level templates directory. This allows users to customize agent
|
|
6
|
-
* and skill templates by placing them in their data directory.
|
|
7
|
-
*
|
|
8
|
-
* Resolution order:
|
|
9
|
-
* 1. {dataDir}/templates/{name} (user customization)
|
|
10
|
-
* 2. {codebaseDir}/templates/{name} (fallback)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { readFile } from "fs/promises";
|
|
14
|
-
import { join, dirname } from "path";
|
|
15
|
-
import { fileURLToPath } from "url";
|
|
16
|
-
import { existsSync } from "fs";
|
|
17
|
-
|
|
18
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const CODEBASE_TEMPLATES_DIR = join(__dirname, "..", "..", "templates");
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Load a template file with fallback to codebase templates
|
|
23
|
-
* @param {string} templateName - Template filename (e.g., 'agent.template.md')
|
|
24
|
-
* @param {string} dataDir - Path to data directory
|
|
25
|
-
* @returns {Promise<string>} Template content
|
|
26
|
-
* @throws {Error} If template not found in either location
|
|
27
|
-
*/
|
|
28
|
-
export async function loadTemplate(templateName, dataDir) {
|
|
29
|
-
// Build list of paths to try
|
|
30
|
-
const paths = [];
|
|
31
|
-
if (dataDir) {
|
|
32
|
-
paths.push(join(dataDir, "templates", templateName));
|
|
33
|
-
}
|
|
34
|
-
paths.push(join(CODEBASE_TEMPLATES_DIR, templateName));
|
|
35
|
-
|
|
36
|
-
// Try each path in order
|
|
37
|
-
for (const path of paths) {
|
|
38
|
-
if (existsSync(path)) {
|
|
39
|
-
return await readFile(path, "utf-8");
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Not found
|
|
44
|
-
throw new Error(
|
|
45
|
-
`Template '${templateName}' not found. Checked:\n` +
|
|
46
|
-
paths.map((p) => ` - ${p}`).join("\n"),
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Load agent profile template
|
|
52
|
-
* @param {string} dataDir - Path to data directory
|
|
53
|
-
* @returns {Promise<string>} Agent template content
|
|
54
|
-
*/
|
|
55
|
-
export async function loadAgentTemplate(dataDir) {
|
|
56
|
-
return loadTemplate("agent.template.md", dataDir);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Load agent skill template
|
|
61
|
-
* @param {string} dataDir - Path to data directory
|
|
62
|
-
* @returns {Promise<string>} Skill template content
|
|
63
|
-
*/
|
|
64
|
-
export async function loadSkillTemplate(dataDir) {
|
|
65
|
-
return loadTemplate("skill.template.md", dataDir);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Load skill install script template
|
|
70
|
-
* @param {string} dataDir - Path to data directory
|
|
71
|
-
* @returns {Promise<string>} Install script template content
|
|
72
|
-
*/
|
|
73
|
-
export async function loadSkillInstallTemplate(dataDir) {
|
|
74
|
-
return loadTemplate("skill-install.template.sh", dataDir);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Load skill reference template
|
|
79
|
-
* @param {string} dataDir - Path to data directory
|
|
80
|
-
* @returns {Promise<string>} Reference template content
|
|
81
|
-
*/
|
|
82
|
-
export async function loadSkillReferenceTemplate(dataDir) {
|
|
83
|
-
return loadTemplate("skill-reference.template.md", dataDir);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Load job description template
|
|
88
|
-
* @param {string} dataDir - Path to data directory
|
|
89
|
-
* @returns {Promise<string>} Job template content
|
|
90
|
-
*/
|
|
91
|
-
export async function loadJobTemplate(dataDir) {
|
|
92
|
-
return loadTemplate("job.template.md", dataDir);
|
|
93
|
-
}
|