UNPKG

@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
#!/usr/bin/env node 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();