@zanozbot/8d-tools
Version:
A command-line tool for generating Eight Disciplines (8D) problem-solving reports from customizable templates.
1,152 lines (1,066 loc) • 37.1 kB
JavaScript
import { Command } from "commander";
import chalk from "chalk";
import fs from "fs-extra";
import { resolve, join, basename, relative } from "path";
function helpCommand() {
console.log(
chalk.blue.bold("\n8D Tools - Eight Disciplines Problem-Solving CLI\n")
);
console.log(chalk.yellow("USAGE:"));
console.log(" 8d <command> [options]\n");
console.log(chalk.yellow("COMMANDS:"));
console.log(" help Show this help information");
console.log(
" init [directory] Initialize 8D directory structure"
);
console.log(" new <title> [options] Create a new 8D report");
console.log(
" link <source> <target> [linkType] Link two existing 8D reports"
);
console.log(" template [action] [name] Manage 8D report templates\n");
console.log(chalk.yellow("EXAMPLES:"));
console.log(
" 8d help # Show help"
);
console.log(
" 8d init # Initialize in docs/8d"
);
console.log(
" 8d init reports/8d # Initialize in custom directory"
);
console.log(
' 8d new "Product quality issue" # Create new 8D report'
);
console.log(
' 8d new "Updated process" -s 3 # Create report superseding #3'
);
console.log(
' 8d new "Related issue" -l "2:Related to:Related to" # Create linked report'
);
console.log(
' 8d new "Simple incident" -t simple # Create report using simple template'
);
console.log(
" 8d link 1 2 # Link reports (default: Supersedes)"
);
console.log(
' 8d link 1 2 "Related to" # Link with custom type'
);
console.log(
" 8d template list # List available templates"
);
console.log(
" 8d template create incident # Create custom template\n"
);
console.log(chalk.yellow("OPTIONS for new command:"));
console.log(" -s, --supersede <number> Supersede an existing 8D report");
console.log(
' -l, --link <link> Link format: "number:LinkType:ReverseLink"'
);
console.log(
' -t, --template <name> Template to use (default: "default")\n'
);
console.log(chalk.yellow("AVAILABLE TEMPLATES:"));
console.log(
" • default Comprehensive 8D template with all disciplines (D0-D8)"
);
console.log(" • simple Streamlined template for quick problem-solving");
console.log(
' • custom Create your own templates with "8d template create"\n'
);
console.log(chalk.yellow("AVAILABLE LINK TYPES:"));
console.log(
" The following link types are supported with automatic reverse linking:"
);
console.log(
" • Supersedes ↔ Superseded by (when one report replaces another)"
);
console.log(" • Related to ↔ Related to (for general relationships)");
console.log(
" • Amends ↔ Amended by (when one report modifies another)"
);
console.log(
" • Clarifies ↔ Clarified by (when one report explains another)"
);
console.log(
" • Extends ↔ Extended by (when one report builds upon another)\n"
);
console.log(chalk.yellow("LINK EXAMPLES:"));
console.log(
' 8d link 1 2 # Uses default "Supersedes"'
);
console.log(
' 8d new "Follow-up" -l "1:Supersedes:Superseded by" # New report supersedes #1'
);
console.log(
' 8d new "Amendment" -l "2:Amends:Amended by" # New report amends #2'
);
console.log(
' 8d link 3 4 "Clarifies" # Report #3 clarifies #4'
);
console.log(
' 8d link 5 6 "Related to" # Reports #5 and #6 are related\n'
);
console.log(
chalk.green(
"For more information, visit: https://github.com/zanozbot/8d-tools"
)
);
}
async function initCommand(directory = "docs/8d") {
try {
const absoluteDir = resolve(directory);
const rootDir = process.cwd();
const adrDirFile = join(rootDir, ".8d-dir");
const sequenceLockFile = join(absoluteDir, ".8d-sequence.lock");
const tocFile = join(absoluteDir, "README.md");
if (await fs.pathExists(adrDirFile)) {
console.log(chalk.yellow("8D directory already initialized."));
const existingDir = await fs.readFile(adrDirFile, "utf8");
console.log(chalk.blue(`Current 8D directory: ${existingDir.trim()}`));
return;
}
await fs.ensureDir(absoluteDir);
console.log(chalk.green(`Created directory: ${directory}`));
await fs.writeFile(adrDirFile, directory);
console.log(chalk.green(`Created .8d-dir file pointing to: ${directory}`));
await fs.writeFile(sequenceLockFile, "0");
console.log(chalk.green("Created .8d-sequence.lock file"));
const tocContent = `# 8D Problem-Solving Reports
This directory contains Eight Disciplines (8D) problem-solving reports.
## Reports
No reports have been created yet. Use \`8d new "Problem Title"\` to create your first report.
## About 8D
The Eight Disciplines (8D) is a problem-solving methodology designed to find the root cause of a problem, devise a short-term fix and implement a long-term solution to prevent recurring problems.
The eight disciplines are:
- **D0**: Plan and prepare
- **D1**: Form a team
- **D2**: Identify the problem
- **D3**: Develop interim containment plan
- **D4**: Verify root causes and escape points
- **D5**: Choose permanent corrective actions
- **D6**: Implement corrective actions
- **D7**: Take preventive measures
- **D8**: Celebrate with your team
`;
await fs.writeFile(tocFile, tocContent);
console.log(chalk.green("Created table of contents (README.md)"));
const templatesDir = join(absoluteDir, ".templates");
await fs.ensureDir(templatesDir);
console.log(chalk.green("Created .templates directory"));
console.log(
chalk.blue.bold("\n✅ 8D directory structure initialized successfully!")
);
console.log(chalk.blue(`Directory: ${directory}`));
console.log(chalk.yellow("\nNext steps:"));
console.log(
chalk.yellow('1. Create your first 8D report: 8d new "Problem Title"')
);
console.log(
chalk.yellow(
'2. Try the simple template: 8d new "Quick Report" -t simple'
)
);
console.log(chalk.yellow("3. List available templates: 8d template list"));
console.log(chalk.yellow('4. Use "8d help" for more commands\n'));
} catch (error) {
console.error(chalk.red("Error initializing 8D directory:"), error);
process.exit(1);
}
}
async function getEightDConfig() {
const rootDir = process.cwd();
const adrDirFile = join(rootDir, ".8d-dir");
if (!await fs.pathExists(adrDirFile)) {
console.error(chalk.red("Error: 8D directory not initialized."));
console.error(
chalk.yellow('Run "8d init" to initialize the 8D directory structure.')
);
process.exit(1);
}
const directory = (await fs.readFile(adrDirFile, "utf8")).trim();
const absoluteDir = resolve(directory);
const sequenceFile = join(absoluteDir, ".8d-sequence.lock");
const tocFile = join(absoluteDir, "README.md");
return {
directory: absoluteDir,
sequenceFile,
tocFile
};
}
async function getCurrentSequenceNumber() {
const config = await getEightDConfig();
if (!await fs.pathExists(config.sequenceFile)) {
console.error(chalk.red("Error: Sequence file not found."));
console.error(
chalk.yellow('Run "8d init" to initialize the 8D directory structure.')
);
process.exit(1);
}
const currentSequence = parseInt(
await fs.readFile(config.sequenceFile, "utf8"),
10
);
return currentSequence;
}
async function incrementSequenceNumber() {
const config = await getEightDConfig();
const currentSequence = await getCurrentSequenceNumber();
const nextSequence = currentSequence + 1;
await fs.writeFile(config.sequenceFile, nextSequence.toString());
return nextSequence;
}
async function getNextSequenceNumber() {
const currentSequence = await getCurrentSequenceNumber();
return currentSequence + 1;
}
function formatSequenceNumber(num) {
return num.toString();
}
function formatSequenceNumberForFilename(num) {
return num.toString().padStart(4, "0");
}
function generateFileName(sequence, title) {
const formattedSequence = formatSequenceNumberForFilename(sequence);
const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
return `${formattedSequence}-${slug}.md`;
}
async function updateTableOfContents(reportPath, title, sequence) {
const config = await getEightDConfig();
if (!await fs.pathExists(config.tocFile)) {
return;
}
const tocContent = await fs.readFile(config.tocFile, "utf8");
const fileName = basename(reportPath);
const formattedSequence = formatSequenceNumber(sequence);
const lines = tocContent.split("\n");
const reportsIndex = lines.findIndex((line) => line.trim() === "## Reports");
if (reportsIndex === -1) {
return;
}
let nextSectionIndex = lines.length;
for (let i = reportsIndex + 1; i < lines.length; i++) {
if (lines[i].startsWith("## ") && lines[i].trim() !== "## Reports") {
nextSectionIndex = i;
break;
}
}
const beforeReports = lines.slice(0, reportsIndex + 1);
const afterReports = lines.slice(nextSectionIndex);
const reportEntries = [];
for (let i = reportsIndex + 1; i < nextSectionIndex; i++) {
const line = lines[i];
if (line.includes("No reports have been created yet") || line.trim() === "") {
continue;
}
if (line.startsWith("- [")) {
const match = line.match(/^\- \[(\d+):/);
if (match) {
reportEntries.push({
line,
sequence: parseInt(match[1], 10)
});
}
}
}
reportEntries.push({
line: `- [${formattedSequence}: ${title}](./${fileName})`,
sequence
});
reportEntries.sort((a, b) => a.sequence - b.sequence);
const newLines = [
...beforeReports,
"",
// Empty line after "## Reports"
...reportEntries.map((entry) => entry.line),
"",
// Empty line before next section
...afterReports
];
await fs.writeFile(config.tocFile, newLines.join("\n"));
}
function generateEightDTemplate(data) {
const { title, sequence, date, links = [] } = data;
const linksSection = links.length > 0 ? `
## Links
${links.join("\n")}` : "";
return `# ${sequence}: ${title}
- **Date:** ${date}
- **Status:** Draft
${linksSection}
## D0: Plan and prepare
### Problem background
<!-- Describe the background of the problem and why it needs to be solved -->
### Prerequisites
<!-- List what is needed before starting the 8D process -->
### Initial assessment
<!-- Provide initial assessment of the problem scope and impact -->
---
## D1: Form a team
### Team members
<!-- List team members and their roles -->
| Name | Role | Department | Responsibilities |
|------|------|------------|------------------|
| | | | |
### Team leader
<!-- Identify the team leader -->
### Team charter
<!-- Define team goals, scope, and operating principles -->
---
## D2: Identify the problem
### Problem statement
<!-- Clear, specific description of the problem -->
### 5W2H analysis
- **Who:**
- **What:**
- **When:**
- **Where:**
- **Why:**
- **How:**
- **How many:**
### Problem quantification
<!-- Quantify the problem with data, metrics, or measurements -->
### Supporting evidence
<!-- Include photos, charts, data, or other evidence -->
---
## D3: Develop interim containment plan
### Immediate actions
<!-- List immediate actions to contain the problem -->
### Containment verification
<!-- How will you verify the containment is effective? -->
### Customer protection
<!-- How are customers protected while permanent solution is developed? -->
### Implementation date
<!-- When was/will the containment be implemented? -->
---
## D4: Verify root causes and escape points
### Root cause analysis
<!-- Use tools like 5 Whys, Fishbone diagram, etc. -->
#### Potential root causes
1.
2.
3.
#### Root cause verification
<!-- How were root causes verified? Include data/evidence -->
### Escape points
<!-- Where in the process did the problem escape detection? -->
#### Why wasn't it caught?
<!-- Analysis of why existing controls failed -->
---
## D5: Choose permanent corrective actions
### Proposed solutions
<!-- List potential permanent corrective actions -->
1. **Solution 1:**
- Description:
- Pros:
- Cons:
- Risk assessment:
2. **Solution 2:**
- Description:
- Pros:
- Cons:
- Risk assessment:
### Selected solution
<!-- Which solution was chosen and why? -->
### Verification plan
<!-- How will you verify the solution works? -->
---
## D6: Implement corrective actions
### Implementation plan
<!-- Detailed plan for implementing the permanent corrective action -->
| Action | Responsible | Target Date | Status |
|--------|-------------|-------------|--------|
| | | | |
### Implementation verification
<!-- Evidence that implementation was successful -->
### Monitoring plan
<!-- How will ongoing effectiveness be monitored? -->
---
## D7: Take preventive measures
### System improvements
<!-- What system changes prevent similar problems? -->
### Process updates
<!-- Document any process changes -->
### Training requirements
<!-- What training is needed to prevent recurrence? -->
### Standard work updates
<!-- How are standard procedures updated? -->
---
## D8: Celebrate with your team
### Team recognition
<!-- How was the team recognized for their work? -->
### Lessons learned
<!-- Key lessons learned from this 8D process -->
### Knowledge sharing
<!-- How will lessons be shared with the organization? -->
### Celebration activities
<!-- How did the team celebrate success? -->
---
## Summary
### Problem resolution
<!-- Brief summary of how the problem was resolved -->
### Key metrics
<!-- Before/after metrics showing improvement -->
### Next steps
<!-- Any follow-up actions or monitoring required -->
---
*This 8D report was generated using [8d-tools](https://github.com/zanozbot/8d-tools).*
`;
}
function generateSimpleTemplate(data) {
const { title, sequence, date, links = [] } = data;
const linksSection = links.length > 0 ? `
## Links
${links.join("\n")}` : "";
return `# ${sequence}: ${title}
- **Date:** ${date}
- **Status:** Draft
${linksSection}
## D0: Planning
<!-- Gather data, feedback, and prerequisites required to solve the problem. -->
## D1: Team members
<!-- List team members and their roles -->
| Name | Role | Department | Responsibilities |
|------|------|------------|------------------|
| | | | |
## D2: Problem statement & description
<!-- Describe the problem clearly and concisely -->
## D3: Interim containment action
<!-- Describe any temporary actions or plans to put in place while determining a permanent corrective action. -->
## D4: Root cause & escape points
<!-- Identify all possible root causes and escape points for the problem. -->
## D5: Permanent corrective action
<!-- Compose a list of corrective actions to solve the problem and prevent similar issues from reoccurring. -->
## D6: Implementation plan
<!-- Develop a plan to implement your corrective actions, including who is responsible for each step and the completion deadline. -->
## D7: Preventive measures
<!-- Describe any measure to implement to avoid similar problems in the future. -->
## D8: Review & closure
<!-- Review the 8D process and close the report. -->
---
*This 8D report was generated using [8d-tools](https://github.com/zanozbot/8d-tools).*
`;
}
async function getAvailableTemplates() {
const templates = ["default", "simple"];
try {
const config = await getEightDConfig();
const templatesDir = join(config.directory, ".templates");
if (await fs.pathExists(templatesDir)) {
const files = await fs.readdir(templatesDir);
const customTemplates = files.filter((file) => file.endsWith(".md")).map((file) => basename(file, ".md"));
templates.push(...customTemplates);
}
} catch (error) {
}
return templates;
}
async function templateExists(templateName) {
if (templateName === "default" || templateName === "simple") {
return true;
}
try {
const config = await getEightDConfig();
const templatesDir = join(config.directory, ".templates");
const templatePath = join(templatesDir, `${templateName}.md`);
return await fs.pathExists(templatePath);
} catch (error) {
return false;
}
}
async function generateReportFromTemplate(templateName, data) {
if (templateName === "default") {
return generateEightDTemplate(data);
}
if (templateName === "simple") {
return generateSimpleTemplate(data);
}
try {
const config = await getEightDConfig();
const templatesDir = join(config.directory, ".templates");
const templatePath = join(templatesDir, `${templateName}.md`);
if (!await fs.pathExists(templatePath)) {
throw new Error(`Template "${templateName}" not found`);
}
let content = await fs.readFile(templatePath, "utf8");
content = content.replace(/\{\{title\}\}/g, data.title);
content = content.replace(/\{\{sequence\}\}/g, data.sequence);
content = content.replace(/\{\{date\}\}/g, data.date);
if (data.links && data.links.length > 0) {
const linksSection = `
## Links
${data.links.join("\n")}
`;
content = content.replace(/\{\{links\}\}/g, linksSection);
} else {
content = content.replace(/\{\{links\}\}/g, "");
}
return content;
} catch (error) {
throw new Error(
`Failed to generate report from template "${templateName}": ${error}`
);
}
}
async function getTemplateInfo(templateName) {
if (templateName === "default") {
return {
name: "default",
title: "Standard 8D problem-solving template",
isBuiltIn: true
};
}
if (templateName === "simple") {
return {
name: "simple",
title: "Simple problem-solving template",
isBuiltIn: true
};
}
try {
const config = await getEightDConfig();
const templatesDir = join(config.directory, ".templates");
const templatePath = join(templatesDir, `${templateName}.md`);
if (!await fs.pathExists(templatePath)) {
throw new Error(`Template "${templateName}" not found`);
}
const content = await fs.readFile(templatePath, "utf8");
const titleMatch = content.match(/^# (.+)$/m);
const title = titleMatch ? titleMatch[1] : "No title";
return {
name: templateName,
title,
isBuiltIn: false,
path: templatePath
};
} catch (error) {
throw new Error(
`Failed to get template info for "${templateName}": ${error}`
);
}
}
function validateTemplateName(name) {
const validNameRegex = /^[a-zA-Z0-9_-]+$/;
return validNameRegex.test(name) && name.length > 0 && name.length <= 50;
}
async function ensureTemplatesDirectory() {
const config = await getEightDConfig();
const templatesDir = join(config.directory, ".templates");
await fs.ensureDir(templatesDir);
return templatesDir;
}
async function addLinkToReport(filePath, linkText) {
const content = await fs.readFile(filePath, "utf8");
const lines = content.split("\n");
if (content.includes(linkText)) {
return;
}
const linksIndex = lines.findIndex((line) => line.trim() === "## Links");
if (linksIndex !== -1) {
let insertIndex = linksIndex + 1;
while (insertIndex < lines.length && lines[insertIndex].trim() === "") {
insertIndex++;
}
lines.splice(insertIndex, 0, `- ${linkText}`);
} else {
let insertIndex = -1;
const statusIndex = lines.findIndex((line) => line.includes("**Status:**"));
if (statusIndex !== -1) {
insertIndex = statusIndex + 1;
} else {
const dateIndex = lines.findIndex((line) => line.includes("**Date:**"));
if (dateIndex !== -1) {
insertIndex = dateIndex + 1;
} else {
const titleIndex = lines.findIndex(
(line) => line.trim().startsWith("#")
);
if (titleIndex !== -1) {
insertIndex = titleIndex + 1;
} else {
insertIndex = 0;
}
}
}
if (insertIndex !== -1) {
lines.splice(insertIndex, 0, "", "## Links", "", `- ${linkText}`);
}
}
await fs.writeFile(filePath, lines.join("\n"));
}
function getReverseLink(linkType) {
const reverseMappings = {
Supersedes: "Superseded by",
"Superseded by": "Supersedes",
"Related to": "Related to",
Amends: "Amended by",
"Amended by": "Amends",
Clarifies: "Clarified by",
"Clarified by": "Clarifies",
Extends: "Extended by",
"Extended by": "Extends"
};
return reverseMappings[linkType] || "Related to";
}
async function newCommand(title, options = {}) {
try {
const config = await getEightDConfig();
const predictedSequence = await getNextSequenceNumber();
const fileName = generateFileName(predictedSequence, title);
const filePath = join(config.directory, fileName);
const templateName = options.template || "default";
if (!await templateExists(templateName)) {
console.error(chalk.red(`Error: Template "${templateName}" not found.`));
console.log(
chalk.yellow('Use "8d template list" to see available templates.')
);
process.exit(1);
}
if (await fs.pathExists(filePath)) {
console.error(chalk.red(`Error: File ${fileName} already exists.`));
process.exit(1);
}
const actualSequence = await incrementSequenceNumber();
const actualFormattedSequence = formatSequenceNumber(actualSequence);
const links = [];
if (options.supersede) {
const supersedeNum = parseInt(options.supersede, 10);
if (isNaN(supersedeNum)) {
console.error(chalk.red("Error: Supersede option must be a number."));
process.exit(1);
}
const supersedeFileName = await findReportByNumber$1(
config.directory,
supersedeNum
);
if (!supersedeFileName) {
console.error(
chalk.red(
`Error: Report ${formatSequenceNumber(supersedeNum)} not found.`
)
);
process.exit(1);
}
links.push(
`- Supersedes: [${formatSequenceNumber(
supersedeNum
)}: ${await getReportTitle$1(
join(config.directory, supersedeFileName)
)}](./${supersedeFileName})`
);
await addLinkToReport(
join(config.directory, supersedeFileName),
`Superseded by: [${actualFormattedSequence}: ${title}](./${fileName})`
);
console.log(
chalk.green(
`Updated report ${formatSequenceNumber(
supersedeNum
)} with superseded link.`
)
);
}
if (options.link) {
const linkParts = options.link.split(":");
if (linkParts.length !== 3) {
console.error(
chalk.red(
'Error: Link format should be "number:LinkType:ReverseLink"'
)
);
console.error(chalk.yellow('Example: "2:Related to:Related to"'));
process.exit(1);
}
const [linkNumStr, linkType, reverseLinkType] = linkParts;
const linkNum = parseInt(linkNumStr, 10);
const actualReverseLinkType = reverseLinkType || getReverseLink(linkType);
if (isNaN(linkNum)) {
console.error(chalk.red("Error: Link number must be a valid number."));
process.exit(1);
}
const linkFileName = await findReportByNumber$1(config.directory, linkNum);
if (!linkFileName) {
console.error(
chalk.red(`Error: Report ${formatSequenceNumber(linkNum)} not found.`)
);
process.exit(1);
}
links.push(
`- ${linkType}: [${formatSequenceNumber(
linkNum
)}: ${await getReportTitle$1(
join(config.directory, linkFileName)
)}](./${linkFileName})`
);
await addLinkToReport(
join(config.directory, linkFileName),
`${actualReverseLinkType}: [${actualFormattedSequence}: ${title}](./${fileName})`
);
console.log(
chalk.green(
`Added link between reports ${formatSequenceNumber(
linkNum
)} and ${actualFormattedSequence}.`
)
);
}
const templateData = {
title,
sequence: actualFormattedSequence,
date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
links: links.length > 0 ? links : void 0
};
const content = await generateReportFromTemplate(
templateName,
templateData
);
await fs.writeFile(filePath, content);
console.log(chalk.green(`Created 8D report: ${fileName}`));
await updateTableOfContents(filePath, title, actualSequence);
console.log(chalk.green("Updated table of contents."));
console.log(
chalk.blue.bold(
`
✅ 8D Report ${actualFormattedSequence} created successfully!`
)
);
console.log(chalk.blue(`File: ${relative(process.cwd(), filePath)}`));
} catch (error) {
console.error(chalk.red("Error creating 8D report:"), error);
process.exit(1);
}
}
async function findReportByNumber$1(directory, number) {
const formattedNumber = formatSequenceNumberForFilename(number);
const files = await fs.readdir(directory);
for (const file of files) {
if (file.startsWith(formattedNumber + "-") && file.endsWith(".md")) {
return file;
}
}
return null;
}
async function getReportTitle$1(filePath) {
const content = await fs.readFile(filePath, "utf8");
const lines = content.split("\n");
const titleLine = lines.find((line) => line.startsWith("# "));
if (titleLine) {
const match = titleLine.match(/^# \d+: (.+)$/);
return match ? match[1] : titleLine.substring(2);
}
return "Unknown Title";
}
async function linkCommand(source, target, linkType = "Supersedes") {
try {
const config = await getEightDConfig();
const sourceNum = parseInt(source, 10);
const targetNum = parseInt(target, 10);
if (isNaN(sourceNum) || isNaN(targetNum)) {
console.error(
chalk.red("Error: Source and target must be valid numbers.")
);
process.exit(1);
}
if (sourceNum === targetNum) {
console.error(
chalk.red("Error: Source and target cannot be the same report.")
);
process.exit(1);
}
const sourceFile = await findReportByNumber(config.directory, sourceNum);
const targetFile = await findReportByNumber(config.directory, targetNum);
if (!sourceFile) {
console.error(
chalk.red(
`Error: Source report ${formatSequenceNumber(sourceNum)} not found.`
)
);
process.exit(1);
}
if (!targetFile) {
console.error(
chalk.red(
`Error: Target report ${formatSequenceNumber(targetNum)} not found.`
)
);
process.exit(1);
}
const sourceFilePath = join(config.directory, sourceFile);
const targetFilePath = join(config.directory, targetFile);
const sourceTitle = await getReportTitle(sourceFilePath);
const targetTitle = await getReportTitle(targetFilePath);
const sourceLink = `${linkType}: [${formatSequenceNumber(
targetNum
)}: ${targetTitle}](./${targetFile})`;
await addLinkToReport(sourceFilePath, sourceLink);
const reverseLinkType = getReverseLink(linkType);
const targetLink = `${reverseLinkType}: [${formatSequenceNumber(
sourceNum
)}: ${sourceTitle}](./${sourceFile})`;
await addLinkToReport(targetFilePath, targetLink);
console.log(chalk.green(`✅ Successfully linked reports:`));
console.log(
chalk.blue(
` ${formatSequenceNumber(
sourceNum
)} ${linkType.toLowerCase()} ${formatSequenceNumber(targetNum)}`
)
);
console.log(
chalk.blue(
` ${formatSequenceNumber(
targetNum
)} ${reverseLinkType.toLowerCase()} ${formatSequenceNumber(sourceNum)}`
)
);
} catch (error) {
console.error(chalk.red("Error linking reports:"), error);
process.exit(1);
}
}
async function findReportByNumber(directory, number) {
const formattedNumber = formatSequenceNumberForFilename(number);
const files = await fs.readdir(directory);
for (const file of files) {
if (file.startsWith(formattedNumber + "-") && file.endsWith(".md")) {
return file;
}
}
return null;
}
async function getReportTitle(filePath) {
const content = await fs.readFile(filePath, "utf8");
const lines = content.split("\n");
const titleLine = lines.find((line) => line.startsWith("# "));
if (titleLine) {
const match = titleLine.match(/^# \d+: (.+)$/);
return match ? match[1] : titleLine.substring(2);
}
return "Unknown Title";
}
async function templateCommand(action, templateName, options = {}) {
try {
switch (action) {
case "list":
await listTemplates();
break;
case "create":
if (!templateName) {
console.error(
chalk.red("Error: Template name is required for create action.")
);
console.log(
chalk.yellow(
'Usage: 8d template create <name> [--title "Custom Title"]'
)
);
process.exit(1);
}
await createTemplate(templateName, options.title);
break;
case "show":
if (!templateName) {
console.error(
chalk.red("Error: Template name is required for show action.")
);
console.log(chalk.yellow("Usage: 8d template show <name>"));
process.exit(1);
}
await showTemplate(templateName);
break;
case "delete":
if (!templateName) {
console.error(
chalk.red("Error: Template name is required for delete action.")
);
console.log(chalk.yellow("Usage: 8d template delete <name>"));
process.exit(1);
}
await deleteTemplate(templateName);
break;
default:
showTemplateHelp();
break;
}
} catch (error) {
console.error(chalk.red("Error managing templates:"), error);
process.exit(1);
}
}
async function listTemplates() {
console.log(chalk.blue.bold("\nAvailable Templates:\n"));
const templates = await getAvailableTemplates();
for (const templateName of templates) {
try {
const info = await getTemplateInfo(templateName);
const builtInLabel = info.isBuiltIn ? chalk.gray(" (built-in)") : "";
console.log(
chalk.green(`• ${info.name}`) + builtInLabel + chalk.gray(` - ${info.title}`)
);
} catch (error) {
console.log(
chalk.green(`• ${templateName}`) + chalk.red(" (error reading template)")
);
}
}
if (templates.length === 1) {
console.log(
chalk.yellow(
'\nNo custom templates found. Use "8d template create <name>" to create one.'
)
);
}
console.log();
}
async function createTemplate(templateName, customTitle) {
if (!validateTemplateName(templateName)) {
console.error(
chalk.red(
"Error: Template name must contain only letters, numbers, hyphens, and underscores."
)
);
console.error(chalk.red("Template name must be 1-50 characters long."));
process.exit(1);
}
if (await templateExists(templateName)) {
console.error(
chalk.red(`Error: Template "${templateName}" already exists.`)
);
process.exit(1);
}
const templatesDir = await ensureTemplatesDirectory();
const templatePath = join(templatesDir, `${templateName}.md`);
const title = customTitle || `{{title}}`;
const templateContent = generateCustomTemplate(title);
await fs.writeFile(templatePath, templateContent);
console.log(
chalk.green(`✅ Template "${templateName}" created successfully!`)
);
console.log(chalk.blue(`Location: ${relative(process.cwd(), templatePath)}`));
}
async function showTemplate(templateName) {
if (!await templateExists(templateName)) {
console.error(chalk.red(`Error: Template "${templateName}" not found.`));
process.exit(1);
}
try {
const info = await getTemplateInfo(templateName);
if (info.isBuiltIn) {
console.log(chalk.blue.bold("\nDefault Template (built-in):\n"));
console.log(
chalk.gray(
"This is the standard 8D problem-solving template with all disciplines."
)
);
console.log(
chalk.gray(
"It includes: D0-D8 sections, team formation, root cause analysis, etc."
)
);
console.log(
chalk.yellow(
'\nTo see the full template, create a new report: 8d new "Test Report"\n'
)
);
return;
}
if (info.path) {
const content = await fs.readFile(info.path, "utf8");
console.log(chalk.blue.bold(`
Template: ${templateName}
`));
console.log(content);
}
} catch (error) {
console.error(
chalk.red(`Error reading template "${templateName}":`, error)
);
process.exit(1);
}
}
async function deleteTemplate(templateName) {
if (templateName === "default") {
console.error(
chalk.red("Error: Cannot delete the built-in default template.")
);
process.exit(1);
}
if (!await templateExists(templateName)) {
console.error(chalk.red(`Error: Template "${templateName}" not found.`));
process.exit(1);
}
try {
const info = await getTemplateInfo(templateName);
if (info.path) {
await fs.remove(info.path);
console.log(
chalk.green(`✅ Template "${templateName}" deleted successfully.`)
);
}
} catch (error) {
console.error(
chalk.red(`Error deleting template "${templateName}":`, error)
);
process.exit(1);
}
}
function showTemplateHelp() {
console.log(chalk.blue.bold("\n8D Template Management\n"));
console.log(chalk.yellow("USAGE:"));
console.log(" 8d template <action> [options]\n");
console.log(chalk.yellow("ACTIONS:"));
console.log(" list List all available templates");
console.log(" create <name> Create a new custom template");
console.log(" show <name> Display template content");
console.log(" delete <name> Delete a custom template\n");
console.log(chalk.yellow("OPTIONS for create:"));
console.log(
" --title <title> Set a custom title (default: uses {{title}} placeholder)\n"
);
console.log(chalk.yellow("EXAMPLES:"));
console.log(
" 8d template list # List all templates"
);
console.log(
" 8d template create simple # Create template with {{title}} placeholder"
);
console.log(
' 8d template create incident --title "Security Incident Report" # Create with fixed title'
);
console.log(
" 8d template show simple # Display template content"
);
console.log(
" 8d template delete simple # Delete custom template\n"
);
}
function generateCustomTemplate(title) {
return `# ${title}
**Date:** {{date}}
**Status:** Draft
{{links}}
<!-- Add your custom template content here -->
---
*This 8D report was generated using [8d-tools](https://github.com/zanozbot/8d-tools).*
`;
}
const program = new Command();
program.name("8d").description(
"CLI tool for generating Eight Disciplines (8D) problem-solving documents"
).version("1.0.0");
program.command("help").description("Show help information").action(helpCommand);
program.command("init").argument("[directory]", "Directory name for 8D reports", "docs/8d").description("Initialize 8D directory structure").action(initCommand);
program.command("new").argument("<title>", "Title of the 8D report").option(
"-s, --supersede <number>",
"Supersede an existing 8D report by number"
).option(
"-l, --link <link>",
'Link to existing 8D report (format: "number:LinkType:ReverseLink")'
).option("-t, --template <name>", 'Template to use (default: "default")').description("Create a new 8D report").action(newCommand);
program.command("link").argument("<source>", "Source 8D report number").argument("<target>", "Target 8D report number").argument("[linkType]", 'Type of link (default: "Supersedes")').description("Link two existing 8D reports").action(linkCommand);
program.command("template").argument("[action]", "Action to perform (list, create, show, delete)").argument("[name]", "Template name").option("--title <title>", "Custom title for template creation").description("Manage 8D report templates").action(templateCommand);
program.action(() => {
helpCommand();
});
program.parse();