@forwardimpact/pathway 0.3.0 → 0.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/app/commands/agent.js +1 -1
- package/app/commands/behaviour.js +1 -1
- package/app/commands/command-factory.js +2 -2
- package/app/commands/discipline.js +1 -1
- package/app/commands/driver.js +1 -1
- package/app/commands/grade.js +1 -1
- package/app/commands/index.js +4 -3
- package/app/commands/serve.js +2 -2
- package/app/commands/site.js +22 -2
- package/app/commands/skill.js +57 -3
- package/app/commands/stage.js +1 -1
- package/app/commands/tool.js +112 -0
- package/app/commands/track.js +1 -1
- package/app/components/card.js +11 -1
- package/app/components/checklist.js +6 -4
- package/app/components/code-display.js +153 -0
- package/app/components/markdown-textarea.js +153 -0
- package/app/css/bundles/app.css +14 -0
- package/app/css/components/badges.css +15 -8
- package/app/css/components/forms.css +55 -0
- package/app/css/components/layout.css +12 -0
- package/app/css/components/surfaces.css +71 -3
- package/app/css/components/typography.css +1 -2
- package/app/css/pages/agent-builder.css +11 -102
- package/app/css/pages/detail.css +60 -0
- package/app/css/pages/job-builder.css +0 -42
- package/app/css/tokens.css +3 -0
- package/app/formatters/agent/dom.js +26 -71
- package/app/formatters/agent/profile.js +67 -10
- package/app/formatters/agent/skill.js +48 -6
- package/app/formatters/grade/dom.js +6 -6
- package/app/formatters/job/description.js +21 -16
- package/app/formatters/job/dom.js +9 -70
- package/app/formatters/json-ld.js +1 -1
- package/app/formatters/shared.js +58 -0
- package/app/formatters/skill/dom.js +70 -3
- package/app/formatters/skill/markdown.js +18 -0
- package/app/formatters/skill/shared.js +14 -4
- package/app/formatters/stage/microdata.js +2 -2
- package/app/formatters/stage/shared.js +3 -3
- package/app/formatters/tool/shared.js +78 -0
- package/app/handout-main.js +19 -18
- package/app/index.html +16 -3
- package/app/lib/card-mappers.js +91 -17
- package/app/lib/render.js +4 -0
- package/app/lib/yaml-loader.js +12 -1
- package/app/main.js +4 -0
- package/app/model/agent.js +47 -23
- package/app/model/checklist.js +2 -2
- package/app/model/derivation.js +5 -5
- package/app/model/levels.js +4 -2
- package/app/model/loader.js +12 -1
- package/app/model/validation.js +77 -11
- package/app/pages/agent-builder.js +121 -77
- package/app/pages/landing.js +35 -15
- package/app/pages/self-assessment.js +7 -5
- package/app/pages/skill.js +5 -17
- package/app/pages/stage.js +12 -8
- package/app/pages/tool.js +50 -0
- package/app/slide-main.js +1 -1
- package/app/slides/chapter.js +8 -8
- package/app/slides/index.js +26 -26
- package/app/slides/overview.js +8 -8
- package/app/slides/skill.js +1 -0
- package/bin/pathway.js +31 -16
- package/examples/capabilities/business.yaml +18 -18
- package/examples/capabilities/delivery.yaml +54 -37
- package/examples/capabilities/people.yaml +1 -1
- package/examples/capabilities/reliability.yaml +130 -115
- package/examples/capabilities/scale.yaml +39 -37
- package/examples/disciplines/engineering_management.yaml +1 -1
- package/examples/framework.yaml +21 -9
- package/examples/grades.yaml +5 -7
- package/examples/self-assessments.yaml +1 -1
- package/examples/stages.yaml +18 -10
- package/package.json +2 -1
- package/templates/agent.template.md +47 -17
- package/templates/job.template.md +8 -8
- package/templates/skill.template.md +33 -11
- package/examples/agents/.claude/skills/architecture-design/SKILL.md +0 -130
- package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +0 -131
- package/examples/agents/.claude/skills/code-quality-review/SKILL.md +0 -108
- package/examples/agents/.claude/skills/devops-cicd/SKILL.md +0 -142
- package/examples/agents/.claude/skills/full-stack-development/SKILL.md +0 -134
- package/examples/agents/.claude/skills/sre-practices/SKILL.md +0 -163
- package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +0 -164
- package/examples/agents/.github/agents/se-platform-code.agent.md +0 -132
- package/examples/agents/.github/agents/se-platform-plan.agent.md +0 -131
- package/examples/agents/.github/agents/se-platform-review.agent.md +0 -136
- package/examples/agents/.vscode/settings.json +0 -8
package/app/commands/agent.js
CHANGED
|
@@ -454,7 +454,7 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
|
|
|
454
454
|
const skillFiles = derivedSkills
|
|
455
455
|
.map((derived) => skillsWithAgent.find((s) => s.id === derived.skillId))
|
|
456
456
|
.filter((skill) => skill?.agent)
|
|
457
|
-
.map((skill) => generateSkillMd(skill));
|
|
457
|
+
.map((skill) => generateSkillMd(skill, data.stages));
|
|
458
458
|
|
|
459
459
|
// Validate all profiles
|
|
460
460
|
for (const profile of profiles) {
|
|
@@ -24,7 +24,7 @@ import { capitalize } from "../formatters/shared.js";
|
|
|
24
24
|
* @param {Function} config.formatDetail - Function to format detail output: (view, framework) => void
|
|
25
25
|
* @param {Function} [config.sortItems] - Optional function to sort items: (items) => sortedItems
|
|
26
26
|
* @param {Function} [config.validate] - Optional validation function: (data) => {errors: [], warnings: []}
|
|
27
|
-
* @param {string} [config.
|
|
27
|
+
* @param {string} [config.emojiIcon] - Optional emoji for the entity
|
|
28
28
|
* @returns {Function} Command handler
|
|
29
29
|
*/
|
|
30
30
|
export function createEntityCommand({
|
|
@@ -36,7 +36,7 @@ export function createEntityCommand({
|
|
|
36
36
|
formatDetail,
|
|
37
37
|
sortItems,
|
|
38
38
|
validate,
|
|
39
|
-
|
|
39
|
+
_emojiIcon = "",
|
|
40
40
|
}) {
|
|
41
41
|
return async function runCommand({ data, args, options }) {
|
|
42
42
|
const [id] = args;
|
package/app/commands/driver.js
CHANGED
package/app/commands/grade.js
CHANGED
package/app/commands/index.js
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* Re-exports all command handlers for convenient importing.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export { runSkillCommand } from "./skill.js";
|
|
8
|
-
export { runBehaviourCommand } from "./behaviour.js";
|
|
9
|
-
export { runDriverCommand } from "./driver.js";
|
|
10
7
|
export { runDisciplineCommand } from "./discipline.js";
|
|
11
8
|
export { runGradeCommand } from "./grade.js";
|
|
12
9
|
export { runTrackCommand } from "./track.js";
|
|
10
|
+
export { runBehaviourCommand } from "./behaviour.js";
|
|
11
|
+
export { runSkillCommand } from "./skill.js";
|
|
12
|
+
export { runDriverCommand } from "./driver.js";
|
|
13
13
|
export { runStageCommand } from "./stage.js";
|
|
14
|
+
export { runToolCommand } from "./tool.js";
|
|
14
15
|
export { runJobCommand } from "./job.js";
|
|
15
16
|
export { runInterviewCommand } from "./interview.js";
|
|
16
17
|
export { runProgressCommand } from "./progress.js";
|
package/app/commands/serve.js
CHANGED
|
@@ -94,7 +94,7 @@ export async function runServeCommand({ dataDir, options }) {
|
|
|
94
94
|
framework = await loadFrameworkConfig(dataDir);
|
|
95
95
|
} catch {
|
|
96
96
|
// Fallback if framework config fails
|
|
97
|
-
framework = {
|
|
97
|
+
framework = { emojiIcon: "🚀", title: "Engineering Pathway" };
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// Generate _index.yaml files before serving
|
|
@@ -136,7 +136,7 @@ export async function runServeCommand({ dataDir, options }) {
|
|
|
136
136
|
|
|
137
137
|
server.listen(port, () => {
|
|
138
138
|
console.log(`
|
|
139
|
-
${framework.
|
|
139
|
+
${framework.emojiIcon} ${framework.title} running at http://localhost:${port}
|
|
140
140
|
📁 Data directory: ${dataDir}
|
|
141
141
|
|
|
142
142
|
Press Ctrl+C to stop the server.
|
package/app/commands/site.js
CHANGED
|
@@ -38,6 +38,11 @@ const PUBLIC_ASSETS = [
|
|
|
38
38
|
"formatters",
|
|
39
39
|
];
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Files and directories to copy from project root
|
|
43
|
+
*/
|
|
44
|
+
const ROOT_ASSETS = ["templates"];
|
|
45
|
+
|
|
41
46
|
/**
|
|
42
47
|
* Run the site command
|
|
43
48
|
* @param {Object} params - Command parameters
|
|
@@ -53,11 +58,11 @@ export async function runSiteCommand({ dataDir, options }) {
|
|
|
53
58
|
try {
|
|
54
59
|
framework = await loadFrameworkConfig(dataDir);
|
|
55
60
|
} catch {
|
|
56
|
-
framework = {
|
|
61
|
+
framework = { emojiIcon: "🚀", title: "Engineering Pathway" };
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
console.log(`
|
|
60
|
-
${framework.
|
|
65
|
+
${framework.emojiIcon} Generating ${framework.title} static site...
|
|
61
66
|
`);
|
|
62
67
|
|
|
63
68
|
// Clean output directory if requested
|
|
@@ -93,6 +98,21 @@ ${framework.emoji} Generating ${framework.title} static site...
|
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
100
|
|
|
101
|
+
// Copy root assets (templates, etc.)
|
|
102
|
+
const rootDir = join(appDir, "..");
|
|
103
|
+
for (const asset of ROOT_ASSETS) {
|
|
104
|
+
const src = join(rootDir, asset);
|
|
105
|
+
const dest = join(outputDir, asset);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await access(src);
|
|
109
|
+
await cp(src, dest, { recursive: true });
|
|
110
|
+
console.log(` ✓ ${asset}`);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.log(` ⚠️ Skipped ${asset}: ${err.message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
96
116
|
// Copy data directory (dereference symlinks to copy actual content)
|
|
97
117
|
console.log("📁 Copying data files...");
|
|
98
118
|
const dataOutputDir = join(outputDir, "data");
|
package/app/commands/skill.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* npx pathway skill # Summary with stats
|
|
8
8
|
* npx pathway skill --list # IDs only (for piping)
|
|
9
9
|
* npx pathway skill <id> # Detail view
|
|
10
|
+
* npx pathway skill <id> --agent # Agent SKILL.md output
|
|
10
11
|
* npx pathway skill --validate # Validation checks
|
|
11
12
|
*/
|
|
12
13
|
|
|
@@ -14,7 +15,10 @@ import { createEntityCommand } from "./command-factory.js";
|
|
|
14
15
|
import { skillToMarkdown } from "../formatters/skill/markdown.js";
|
|
15
16
|
import { prepareSkillsList } from "../formatters/skill/shared.js";
|
|
16
17
|
import { getConceptEmoji } from "../model/levels.js";
|
|
17
|
-
import { formatTable } from "../lib/cli-output.js";
|
|
18
|
+
import { formatTable, formatError } from "../lib/cli-output.js";
|
|
19
|
+
import { generateSkillMd } from "../model/agent.js";
|
|
20
|
+
import { formatAgentSkill } from "../formatters/agent/skill.js";
|
|
21
|
+
import { loadSkillTemplate } from "../lib/template-loader.js";
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* Format skill summary output
|
|
@@ -59,7 +63,29 @@ function formatDetail(viewAndContext, framework) {
|
|
|
59
63
|
);
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Format skill as agent SKILL.md output
|
|
68
|
+
* @param {Object} skill - Skill entity with agent section
|
|
69
|
+
* @param {Array} stages - All stage entities
|
|
70
|
+
* @param {string} dataDir - Path to data directory for template loading
|
|
71
|
+
*/
|
|
72
|
+
async function formatAgentDetail(skill, stages, dataDir) {
|
|
73
|
+
if (!skill.agent) {
|
|
74
|
+
console.error(formatError(`Skill '${skill.id}' has no agent section`));
|
|
75
|
+
console.error(`\nSkills with agent support:`);
|
|
76
|
+
console.error(
|
|
77
|
+
` npx pathway skill --list | xargs -I{} sh -c 'npx pathway skill {} --json | jq -e .skill.agent > /dev/null && echo {}'`,
|
|
78
|
+
);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const template = await loadSkillTemplate(dataDir);
|
|
83
|
+
const skillMd = generateSkillMd(skill, stages);
|
|
84
|
+
const output = formatAgentSkill(skillMd, template);
|
|
85
|
+
console.log(output);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const baseSkillCommand = createEntityCommand({
|
|
63
89
|
entityName: "skill",
|
|
64
90
|
pluralName: "skills",
|
|
65
91
|
findEntity: (data, id) => data.skills.find((s) => s.id === id),
|
|
@@ -72,5 +98,33 @@ export const runSkillCommand = createEntityCommand({
|
|
|
72
98
|
}),
|
|
73
99
|
formatSummary,
|
|
74
100
|
formatDetail,
|
|
75
|
-
|
|
101
|
+
emojiIcon: "📚",
|
|
76
102
|
});
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Run skill command with --agent support
|
|
106
|
+
* @param {Object} params - Command parameters
|
|
107
|
+
* @param {Object} params.data - Loaded pathway data
|
|
108
|
+
* @param {string[]} params.args - Command arguments
|
|
109
|
+
* @param {Object} params.options - Command options
|
|
110
|
+
* @param {string} params.dataDir - Path to data directory
|
|
111
|
+
*/
|
|
112
|
+
export async function runSkillCommand({ data, args, options, dataDir }) {
|
|
113
|
+
// Handle --agent flag for detail view
|
|
114
|
+
if (options.agent && args.length > 0) {
|
|
115
|
+
const [id] = args;
|
|
116
|
+
const skill = data.skills.find((s) => s.id === id);
|
|
117
|
+
|
|
118
|
+
if (!skill) {
|
|
119
|
+
console.error(formatError(`Skill not found: ${id}`));
|
|
120
|
+
console.error(`Available: ${data.skills.map((s) => s.id).join(", ")}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await formatAgentDetail(skill, data.stages, dataDir);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Delegate to base command for all other cases
|
|
129
|
+
return baseSkillCommand({ data, args, options, dataDir });
|
|
130
|
+
}
|
package/app/commands/stage.js
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool CLI Command
|
|
3
|
+
*
|
|
4
|
+
* Handles tool summary, listing, and detail display in the terminal.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx pathway tool # Summary with stats
|
|
8
|
+
* npx pathway tool --list # Tool names only (for piping)
|
|
9
|
+
* npx pathway tool <name> # Detail view for specific tool
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { prepareToolsList } from "../formatters/tool/shared.js";
|
|
13
|
+
import {
|
|
14
|
+
formatTable,
|
|
15
|
+
formatHeader,
|
|
16
|
+
formatSubheader,
|
|
17
|
+
} from "../lib/cli-output.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run tool command
|
|
21
|
+
* @param {Object} params - Command parameters
|
|
22
|
+
* @param {Object} params.data - Loaded pathway data
|
|
23
|
+
* @param {string[]} params.args - Command arguments
|
|
24
|
+
* @param {Object} params.options - Command options
|
|
25
|
+
*/
|
|
26
|
+
export async function runToolCommand({ data, args, options }) {
|
|
27
|
+
const [name] = args;
|
|
28
|
+
const { tools, totalCount } = prepareToolsList(data.skills);
|
|
29
|
+
|
|
30
|
+
// --list: Output clean newline-separated tool names for piping
|
|
31
|
+
if (options.list) {
|
|
32
|
+
for (const tool of tools) {
|
|
33
|
+
console.log(tool.name);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// No args: Show summary
|
|
39
|
+
if (!name) {
|
|
40
|
+
if (options.json) {
|
|
41
|
+
console.log(JSON.stringify(tools, null, 2));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
formatSummary(tools, totalCount);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// With name: Show detail
|
|
49
|
+
const tool = tools.find((t) => t.name.toLowerCase() === name.toLowerCase());
|
|
50
|
+
|
|
51
|
+
if (!tool) {
|
|
52
|
+
console.error(`Tool not found: ${name}`);
|
|
53
|
+
console.error(`Available: ${tools.map((t) => t.name).join(", ")}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (options.json) {
|
|
58
|
+
console.log(JSON.stringify(tool, null, 2));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
formatDetail(tool);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Format tool summary output
|
|
67
|
+
* @param {Array} tools - Aggregated tools
|
|
68
|
+
* @param {number} totalCount - Total tool count
|
|
69
|
+
*/
|
|
70
|
+
function formatSummary(tools, totalCount) {
|
|
71
|
+
console.log(`\n🔧 Tools\n`);
|
|
72
|
+
|
|
73
|
+
// Show tools sorted by usage count
|
|
74
|
+
const sorted = [...tools].sort((a, b) => b.usages.length - a.usages.length);
|
|
75
|
+
const rows = sorted
|
|
76
|
+
.slice(0, 15)
|
|
77
|
+
.map((t) => [
|
|
78
|
+
t.name,
|
|
79
|
+
t.usages.length,
|
|
80
|
+
t.description.length > 50
|
|
81
|
+
? t.description.slice(0, 47) + "..."
|
|
82
|
+
: t.description,
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
console.log(formatTable(["Tool", "Skills", "Description"], rows));
|
|
86
|
+
console.log(`\nTotal: ${totalCount} tools`);
|
|
87
|
+
if (sorted.length > 15) {
|
|
88
|
+
console.log(`(showing top 15 by usage)`);
|
|
89
|
+
}
|
|
90
|
+
console.log(`\nRun 'npx pathway tool --list' for all tool names`);
|
|
91
|
+
console.log(`Run 'npx pathway tool <name>' for details\n`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Format tool detail output
|
|
96
|
+
* @param {Object} tool - Aggregated tool with usages
|
|
97
|
+
*/
|
|
98
|
+
function formatDetail(tool) {
|
|
99
|
+
console.log(formatHeader(`\n🔧 ${tool.name}\n`));
|
|
100
|
+
console.log(`${tool.description}\n`);
|
|
101
|
+
|
|
102
|
+
if (tool.url) {
|
|
103
|
+
console.log(`Documentation: ${tool.url}\n`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (tool.usages.length > 0) {
|
|
107
|
+
console.log(formatSubheader("Used in Skills\n"));
|
|
108
|
+
const rows = tool.usages.map((u) => [u.skillName, u.useWhen]);
|
|
109
|
+
console.log(formatTable(["Skill", "Use When"], rows));
|
|
110
|
+
console.log();
|
|
111
|
+
}
|
|
112
|
+
}
|
package/app/commands/track.js
CHANGED
package/app/components/card.js
CHANGED
|
@@ -13,6 +13,7 @@ import { div, h3, p, span } from "../lib/render.js";
|
|
|
13
13
|
* @param {HTMLElement[]} [options.badges] - Badges to display
|
|
14
14
|
* @param {HTMLElement[]} [options.meta] - Meta information
|
|
15
15
|
* @param {HTMLElement} [options.content] - Additional content
|
|
16
|
+
* @param {HTMLElement} [options.icon] - Icon element to display
|
|
16
17
|
* @param {string} [options.className] - Additional CSS class
|
|
17
18
|
* @returns {HTMLElement}
|
|
18
19
|
*/
|
|
@@ -23,13 +24,22 @@ export function createCard({
|
|
|
23
24
|
badges = [],
|
|
24
25
|
meta = [],
|
|
25
26
|
content,
|
|
27
|
+
icon,
|
|
26
28
|
className = "",
|
|
27
29
|
}) {
|
|
28
30
|
const isClickable = !!href;
|
|
29
31
|
|
|
32
|
+
const titleContent = icon
|
|
33
|
+
? div(
|
|
34
|
+
{ className: "card-title-with-icon" },
|
|
35
|
+
icon,
|
|
36
|
+
h3({ className: "card-title" }, title),
|
|
37
|
+
)
|
|
38
|
+
: h3({ className: "card-title" }, title);
|
|
39
|
+
|
|
30
40
|
const cardHeader = div(
|
|
31
41
|
{ className: "card-header" },
|
|
32
|
-
|
|
42
|
+
titleContent,
|
|
33
43
|
badges.length > 0 ? div({ className: "card-badges" }, ...badges) : null,
|
|
34
44
|
);
|
|
35
45
|
|
|
@@ -43,7 +43,7 @@ export function createChecklist(checklist, options = {}) {
|
|
|
43
43
|
function createChecklistGroup(group, options) {
|
|
44
44
|
const { interactive, capabilities } = options;
|
|
45
45
|
const emoji = getCapabilityEmoji(capabilities, group.capability);
|
|
46
|
-
const capabilityName = formatCapabilityName(group.capability);
|
|
46
|
+
const capabilityName = formatCapabilityName(group.capability, capabilities);
|
|
47
47
|
|
|
48
48
|
return div(
|
|
49
49
|
{ className: "checklist-group" },
|
|
@@ -90,11 +90,13 @@ function createInteractiveCheckbox() {
|
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* Format capability name for display
|
|
93
|
-
* @param {string}
|
|
93
|
+
* @param {string} capabilityId - Capability ID
|
|
94
|
+
* @param {Array} capabilities - Capabilities array
|
|
94
95
|
* @returns {string}
|
|
95
96
|
*/
|
|
96
|
-
function formatCapabilityName(
|
|
97
|
-
|
|
97
|
+
function formatCapabilityName(capabilityId, capabilities) {
|
|
98
|
+
const capability = capabilities.find((c) => c.id === capabilityId);
|
|
99
|
+
return capability?.name || capabilityId;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
/**
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Display Component
|
|
3
|
+
*
|
|
4
|
+
* Reusable read-only code block with copy buttons and syntax highlighting.
|
|
5
|
+
* Used for markdown content, agent profiles, skills, and code snippets.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* global Prism */
|
|
9
|
+
import { div, p, span, button } from "../lib/render.js";
|
|
10
|
+
|
|
11
|
+
const COPY_LABEL = "📋 Copy";
|
|
12
|
+
const COPY_HTML_LABEL = "Copy as HTML";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a copy button that copies content to clipboard
|
|
16
|
+
* @param {string} content - The text content to copy
|
|
17
|
+
* @returns {HTMLElement}
|
|
18
|
+
*/
|
|
19
|
+
export function createCopyButton(content) {
|
|
20
|
+
const btn = button(
|
|
21
|
+
{
|
|
22
|
+
className: "btn btn-sm copy-btn",
|
|
23
|
+
onClick: async () => {
|
|
24
|
+
try {
|
|
25
|
+
await navigator.clipboard.writeText(content);
|
|
26
|
+
btn.textContent = "✓ Copied!";
|
|
27
|
+
btn.classList.add("copied");
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
btn.textContent = COPY_LABEL;
|
|
30
|
+
btn.classList.remove("copied");
|
|
31
|
+
}, 2000);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error("Failed to copy:", err);
|
|
34
|
+
btn.textContent = "Copy failed";
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
btn.textContent = COPY_LABEL;
|
|
37
|
+
}, 2000);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
COPY_LABEL,
|
|
42
|
+
);
|
|
43
|
+
return btn;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a copy button that copies HTML to clipboard (for rich text pasting)
|
|
48
|
+
* @param {string} html - The HTML content to copy
|
|
49
|
+
* @returns {HTMLElement}
|
|
50
|
+
*/
|
|
51
|
+
function createCopyHtmlButton(html) {
|
|
52
|
+
const btn = button(
|
|
53
|
+
{
|
|
54
|
+
className: "btn btn-sm btn-secondary copy-btn",
|
|
55
|
+
onClick: async () => {
|
|
56
|
+
try {
|
|
57
|
+
const blob = new Blob([html], { type: "text/html" });
|
|
58
|
+
const clipboardItem = new ClipboardItem({ "text/html": blob });
|
|
59
|
+
await navigator.clipboard.write([clipboardItem]);
|
|
60
|
+
btn.textContent = "✓ Copied!";
|
|
61
|
+
btn.classList.add("copied");
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
btn.textContent = COPY_HTML_LABEL;
|
|
64
|
+
btn.classList.remove("copied");
|
|
65
|
+
}, 2000);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error("Failed to copy:", err);
|
|
68
|
+
btn.textContent = "Copy failed";
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
btn.textContent = COPY_HTML_LABEL;
|
|
71
|
+
}, 2000);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
COPY_HTML_LABEL,
|
|
76
|
+
);
|
|
77
|
+
return btn;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a code display component with syntax highlighting and copy button
|
|
82
|
+
* @param {Object} options
|
|
83
|
+
* @param {string} options.content - The code content to display
|
|
84
|
+
* @param {string} [options.language="markdown"] - Language for syntax highlighting
|
|
85
|
+
* @param {string} [options.filename] - Optional filename to display in header
|
|
86
|
+
* @param {string} [options.description] - Optional description text
|
|
87
|
+
* @param {Function} [options.toHtml] - Function to convert content to HTML (enables "Copy as HTML" button)
|
|
88
|
+
* @param {number} [options.minHeight] - Optional minimum height in pixels
|
|
89
|
+
* @param {number} [options.maxHeight] - Optional maximum height in pixels
|
|
90
|
+
* @returns {HTMLElement}
|
|
91
|
+
*/
|
|
92
|
+
export function createCodeDisplay({
|
|
93
|
+
content,
|
|
94
|
+
language = "markdown",
|
|
95
|
+
filename,
|
|
96
|
+
description,
|
|
97
|
+
toHtml,
|
|
98
|
+
minHeight,
|
|
99
|
+
maxHeight,
|
|
100
|
+
}) {
|
|
101
|
+
// Create highlighted code block
|
|
102
|
+
const pre = document.createElement("pre");
|
|
103
|
+
pre.className = "code-display";
|
|
104
|
+
if (minHeight) pre.style.minHeight = `${minHeight}px`;
|
|
105
|
+
if (maxHeight) {
|
|
106
|
+
pre.style.maxHeight = `${maxHeight}px`;
|
|
107
|
+
pre.style.overflowY = "auto";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const code = document.createElement("code");
|
|
111
|
+
if (language) {
|
|
112
|
+
code.className = `language-${language}`;
|
|
113
|
+
}
|
|
114
|
+
code.textContent = content;
|
|
115
|
+
pre.appendChild(code);
|
|
116
|
+
|
|
117
|
+
// Apply Prism highlighting if available and language specified
|
|
118
|
+
if (language && typeof Prism !== "undefined") {
|
|
119
|
+
Prism.highlightElement(code);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Build header content
|
|
123
|
+
const headerLeft = [];
|
|
124
|
+
if (filename) {
|
|
125
|
+
headerLeft.push(span({ className: "code-display-filename" }, filename));
|
|
126
|
+
}
|
|
127
|
+
if (description) {
|
|
128
|
+
headerLeft.push(p({ className: "text-muted" }, description));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Build buttons
|
|
132
|
+
const buttons = [createCopyButton(content)];
|
|
133
|
+
if (toHtml) {
|
|
134
|
+
buttons.push(createCopyHtmlButton(toHtml(content)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Only show header if there's content for it
|
|
138
|
+
const hasHeader = headerLeft.length > 0 || buttons.length > 0;
|
|
139
|
+
|
|
140
|
+
return div(
|
|
141
|
+
{ className: "code-display-container" },
|
|
142
|
+
hasHeader
|
|
143
|
+
? div(
|
|
144
|
+
{ className: "code-display-header" },
|
|
145
|
+
headerLeft.length > 0
|
|
146
|
+
? div({ className: "code-display-info" }, ...headerLeft)
|
|
147
|
+
: null,
|
|
148
|
+
div({ className: "button-group" }, ...buttons),
|
|
149
|
+
)
|
|
150
|
+
: null,
|
|
151
|
+
pre,
|
|
152
|
+
);
|
|
153
|
+
}
|