legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
406 lines (393 loc) ⢠16.8 kB
JavaScript
/**
* First Time User Experience (FTUX) handler for Interactive CLI
*
* This module provides an improved onboarding experience for new users
* when no files are found in the default directory, offering setup
* assistance, demo examples, and guided configuration.
*
* @module
*/
import { select, input, confirm } from '@inquirer/prompts';
import * as path from 'path';
import * as fs from 'fs';
import chalk from 'chalk';
// import { RESOLVED_PATHS } from '../../../constants/index.js';
import { scanDirectory } from '../utils/file-scanner.js';
import { formatWarningMessage } from '../utils/format-helpers.js';
import { handleBrowseFolder, handleManualInput } from '../utils/file-input-helpers.js';
import { getEnvFilePath, getInstallationDescription } from '../utils/installation-detector.js';
/**
* Get current directory path in a way that works for both ESM and CJS
* Since we're building from dist, we can use relative paths from the script location
*/
function getCurrentDir() {
// Use process.argv[1] to determine script location - works for both ESM and CJS
const scriptPath = process.argv[1] || process.cwd();
return path.dirname(scriptPath);
}
/** Option for setting up configuration */
const SETUP_CONFIG_OPTION = 'š ļø Setup configuration (.env)';
/** Option for trying a demo example */
const TRY_DEMO_OPTION = 'šÆ Try a demo example';
/** Option for browsing different folder */
const BROWSE_FOLDER_OPTION = 'š Browse different folder';
/** Option for manual file input */
const MANUAL_INPUT_OPTION = 'š Enter file path manually';
/** Option for getting help/tutorial */
const GET_HELP_OPTION = 'š Show help & tutorial';
/** Option for exiting */
const EXIT_OPTION = 'ā Exit';
/**
* Handle first-time user experience when no files are found
*
* Provides a friendly onboarding flow with multiple options to help
* new users get started with Legal Markdown processing.
*
* @returns Promise resolving to the selected input file path
* @throws Error when user cancels or no valid option is selected
*/
export async function handleFirstTimeUserExperience() {
console.log(chalk.yellow('\nš Welcome to Legal Markdown!'));
console.log(chalk.gray("It looks like you're getting started. Let me help you with that.\n"));
const choice = await select({
message: 'What would you like to do?',
choices: [
{ name: SETUP_CONFIG_OPTION, value: 'setup' },
{ name: TRY_DEMO_OPTION, value: 'demo' },
{ name: BROWSE_FOLDER_OPTION, value: 'browse' },
{ name: MANUAL_INPUT_OPTION, value: 'manual' },
{ name: GET_HELP_OPTION, value: 'help' },
{ name: EXIT_OPTION, value: 'exit' },
],
});
switch (choice) {
case 'setup':
return await handleSetupConfiguration();
case 'demo':
return await handleDemoExample();
case 'browse':
return await handleBrowseFolder();
case 'manual':
return await handleManualInput();
case 'help':
await showHelpAndTutorial();
// After showing help, ask again
return await handleFirstTimeUserExperience();
case 'exit':
console.log(chalk.yellow('\nš Thanks for trying Legal Markdown!'));
console.log(chalk.gray('Run ') + chalk.cyan('legal-md-ui') + chalk.gray(" again when you're ready."));
process.exit(0);
break;
default:
throw new Error('Invalid option selected');
}
}
/**
* Handle setup configuration workflow
*/
async function handleSetupConfiguration() {
console.log(chalk.cyan('\nš ļø Configuration Setup'));
console.log(chalk.gray("Let's set up your preferred directories for Legal Markdown processing."));
console.log(chalk.gray(getInstallationDescription() + '\n'));
// Ask for input directory
const inputDir = await input({
message: 'Where do you keep your markdown files? (input directory)',
default: process.cwd(),
validate: value => {
const resolvedPath = path.resolve(value);
if (!fs.existsSync(resolvedPath)) {
return 'Directory does not exist. Please enter a valid path.';
}
if (!fs.statSync(resolvedPath).isDirectory()) {
return 'Path is not a directory.';
}
return true;
},
});
// Ask for output directory
const outputDir = await input({
message: 'Where should processed files be saved? (output directory)',
default: path.join(path.resolve(inputDir), 'output'),
validate: value => {
const resolvedPath = path.resolve(value);
const parentDir = path.dirname(resolvedPath);
if (!fs.existsSync(parentDir)) {
return 'Parent directory does not exist.';
}
return true;
},
});
// Ask for styles directory
const stylesDir = await input({
message: 'Where are your CSS stylesheets stored? (styles directory)',
default: path.join(path.resolve(inputDir), 'styles'),
validate: value => {
const resolvedPath = path.resolve(value);
const parentDir = path.dirname(resolvedPath);
if (!fs.existsSync(parentDir)) {
return 'Parent directory does not exist.';
}
return true;
},
});
// Ask for archive directory
const archiveDir = await input({
message: 'Where should processed input files be archived? (archive directory)',
default: path.join(path.resolve(outputDir), 'archive'),
validate: value => {
const resolvedPath = path.resolve(value);
const parentDir = path.dirname(resolvedPath);
if (!fs.existsSync(parentDir)) {
return 'Parent directory does not exist.';
}
return true;
},
});
// Create .env configuration using installation-aware path
const envPath = getEnvFilePath();
const envContent = `# Legal Markdown Configuration
# Generated by interactive setup
# Directory containing image assets (logos, diagrams, etc.)
IMAGES_DIR=src/assets/images
# Directory containing CSS stylesheets
STYLES_DIR="${path.resolve(stylesDir)}"
# Default directory for input documents
DEFAULT_INPUT_DIR="${path.resolve(inputDir)}"
# Default directory for output documents
DEFAULT_OUTPUT_DIR="${path.resolve(outputDir)}"
# Directory for archiving processed input files
ARCHIVE_DIR="${path.resolve(archiveDir)}"
`;
// Check if .env already exists
if (fs.existsSync(envPath)) {
const overwrite = await confirm({
message: 'A .env file already exists. Overwrite it?',
default: false,
});
if (!overwrite) {
console.log(chalk.yellow('ā ļø Setup cancelled. Existing configuration unchanged.'));
console.log(chalk.gray('Returning to main menu...\n'));
// Return to FTUX main menu instead of exiting
return await handleFirstTimeUserExperience();
}
else {
fs.writeFileSync(envPath, envContent);
console.log(chalk.green(`ā
Configuration saved to ${envPath}`));
}
}
else {
fs.writeFileSync(envPath, envContent);
console.log(chalk.green(`ā
Configuration saved to ${envPath}`));
}
// Create directories if they don't exist
const resolvedOutputDir = path.resolve(outputDir);
const resolvedStylesDir = path.resolve(stylesDir);
const resolvedArchiveDir = path.resolve(archiveDir);
if (!fs.existsSync(resolvedOutputDir)) {
fs.mkdirSync(resolvedOutputDir, { recursive: true });
console.log(chalk.green(`ā
Created output directory: ${resolvedOutputDir}`));
}
if (!fs.existsSync(resolvedStylesDir)) {
fs.mkdirSync(resolvedStylesDir, { recursive: true });
console.log(chalk.green(`ā
Created styles directory: ${resolvedStylesDir}`));
}
if (!fs.existsSync(resolvedArchiveDir)) {
fs.mkdirSync(resolvedArchiveDir, { recursive: true });
console.log(chalk.green(`ā
Created archive directory: ${resolvedArchiveDir}`));
}
// Now scan for files in the configured input directory
console.log(chalk.cyan(`\nš Scanning for files in: ${path.resolve(inputDir)}\n`));
const resolvedInputDir = path.resolve(inputDir);
const files = scanDirectory(resolvedInputDir, resolvedInputDir);
if (files.length === 0) {
console.log(formatWarningMessage('No supported files found in the configured directory.'));
console.log(chalk.gray('You can add .md, .markdown, .rst, .tex, or .txt files to get started.\n'));
const nextAction = await select({
message: 'What would you like to do next?',
choices: [
{ name: TRY_DEMO_OPTION, value: 'demo' },
{ name: MANUAL_INPUT_OPTION, value: 'manual' },
{ name: EXIT_OPTION, value: 'exit' },
],
});
switch (nextAction) {
case 'demo':
return await handleDemoExample();
case 'manual':
return await handleManualInput();
case 'exit':
process.exit(0);
}
}
// Show available files
const choices = [
...files.map(file => ({
name: file.name,
value: file.path,
})),
{ name: TRY_DEMO_OPTION, value: 'demo' },
{ name: EXIT_OPTION, value: 'exit' },
];
const selectedFile = await select({
message: 'Select a file to process:',
choices,
});
if (selectedFile === 'demo') {
return await handleDemoExample();
}
if (selectedFile === 'exit') {
process.exit(0);
}
return selectedFile;
}
/**
* Handle demo example selection
*/
async function handleDemoExample() {
console.log(chalk.cyan('\nšÆ Demo Examples'));
console.log(chalk.gray('Try Legal Markdown with these built-in examples.\n'));
// Get examples from the package - try multiple locations
const currentDir = getCurrentDir();
const possibleExamplesDirs = [
// In dist for development or when running from source
path.join(currentDir, '../../..', 'dist', 'examples'),
// In node_modules for installed package
path.join(process.cwd(), 'node_modules', 'legal-markdown-js', 'dist', 'examples'),
path.join(process.cwd(), 'node_modules', 'legal-markdown-js', 'examples'),
// Global installation paths
path.join(path.dirname(process.argv[1] || ''), '..', 'examples'),
path.join(path.dirname(process.argv[1] || ''), '..', 'dist', 'examples'),
];
const availableExamples = [];
let examplesDir = '';
// Find the first existing examples directory
for (const dir of possibleExamplesDirs) {
if (fs.existsSync(dir)) {
examplesDir = dir;
break;
}
}
if (examplesDir) {
const basicExample = path.join(examplesDir, 'basic-processing', 'simple-document', 'example.md');
const advancedExample = path.join(examplesDir, 'advanced', 'complex-nda', 'nda-with-data.md');
if (fs.existsSync(basicExample)) {
availableExamples.push({
name: 'š Basic Document Processing',
path: basicExample,
description: 'Simple markdown with template fields',
});
}
if (fs.existsSync(advancedExample)) {
availableExamples.push({
name: 'š Advanced NDA Contract',
path: advancedExample,
description: 'Complex contract with conditional clauses',
});
}
}
// Fallback: create a simple demo file in current directory
if (availableExamples.length === 0) {
const demoPath = path.join(process.cwd(), 'legal-markdown-demo.md');
const demoContent = `---
title: "Sample Legal Document"
party1: "Acme Corporation"
party2: "[Enter Client Name]"
date: "{{formatDate(today, 'MMMM dd, yyyy')}}"
jurisdiction: "California"
---
# {{title}}
This agreement is entered into on {{date}} between {{party1}} and {{party2}}.
## 1. Terms and Conditions
l. **Effective Date**: This agreement becomes effective on {{date}}.
l. **Jurisdiction**: This agreement shall be governed by the laws of {{jurisdiction}}.
{{#if party2}}
l. **Parties**: The contracting parties are {{party1}} and {{party2}}.
{{else}}
l. **Party**: The contracting party is {{party1}}.
{{/if}}
---
*This is a demo document showing Legal Markdown features:*
- *Template fields: {{party1}}, {{party2}}*
- *Date formatting: {{date}}*
- *Conditional clauses: {{#if party2}}*
- *Automatic numbering: l.*
`;
try {
fs.writeFileSync(demoPath, demoContent);
availableExamples.push({
name: 'šÆ Generated Demo Document',
path: demoPath,
description: 'A sample document created just for you',
});
console.log(chalk.green(`ā
Created demo file: ${demoPath}`));
}
catch (error) {
console.log(chalk.yellow(`ā ļø Couldn't create demo file: ${error instanceof Error ? error.message : String(error)}`));
}
}
if (availableExamples.length === 0) {
console.log(chalk.red('ā No demo examples available.'));
return await handleManualInput();
}
const choices = [
...availableExamples.map(example => ({
name: `${example.name}\n ${chalk.gray(example.description)}`,
value: example.path,
})),
{ name: 'ā Back to main menu', value: 'back' },
];
const selectedExample = await select({
message: 'Choose a demo example:',
choices,
});
if (selectedExample === 'back') {
return await handleFirstTimeUserExperience();
}
console.log(chalk.green(`\nā
Selected demo: ${selectedExample}`));
console.log(chalk.gray('You can examine this file after processing to see how Legal Markdown works.\n'));
return selectedExample;
}
/**
* Show help and tutorial information
*/
async function showHelpAndTutorial() {
console.log(chalk.cyan('\nš Legal Markdown Help & Tutorial\n'));
console.log(chalk.bold('What is Legal Markdown?'));
console.log(chalk.gray('Legal Markdown is a tool for processing legal documents written in Markdown'));
console.log(chalk.gray('with YAML metadata and template fields.\n'));
console.log(chalk.bold('Key Features:'));
console.log(chalk.gray('⢠š Template fields: {{party1}}, {{date}}, etc.'));
console.log(chalk.gray('⢠š¢ Auto-numbering: l. for numbered clauses'));
console.log(chalk.gray('⢠ā Conditional clauses: {{#if condition}}...{{/if}}'));
console.log(chalk.gray('⢠š
Date formatting: {{formatDate(date, "MMM dd, yyyy")}}'));
console.log(chalk.gray('⢠š Multiple output formats: HTML, PDF, Markdown'));
console.log(chalk.gray('⢠šØ Custom styling with CSS\n'));
console.log(chalk.bold('Example Document Structure:'));
console.log(chalk.gray('```'));
console.log(chalk.gray('---'));
console.log(chalk.gray('title: "My Contract"'));
console.log(chalk.gray('party1: "Company A"'));
console.log(chalk.gray('date: "2024-01-15"'));
console.log(chalk.gray('---'));
console.log(chalk.gray(''));
console.log(chalk.gray('# {{title}}'));
console.log(chalk.gray(''));
console.log(chalk.gray('Agreement between {{party1}} dated {{formatDate(date, "MMMM dd, yyyy")}}.'));
console.log(chalk.gray(''));
console.log(chalk.gray('l. First clause'));
console.log(chalk.gray('l. Second clause'));
console.log(chalk.gray('```\n'));
console.log(chalk.bold('Getting Started:'));
console.log(chalk.gray('1. š ļø Setup: Configure your input/output directories'));
console.log(chalk.gray('2. š Files: Create .md files with YAML frontmatter'));
console.log(chalk.gray('3. šÆ Process: Use this interactive CLI to convert documents'));
console.log(chalk.gray('4. š Output: Get HTML, PDF, or processed Markdown\n'));
console.log(chalk.bold('Useful Commands:'));
console.log(chalk.gray('⢠') + chalk.cyan('legal-md-ui') + chalk.gray(' - Interactive processing'));
console.log(chalk.gray('⢠') + chalk.cyan('legal-md input.md') + chalk.gray(' - Direct processing'));
console.log(chalk.gray('⢠') + chalk.cyan('legal-md --help') + chalk.gray(' - Command line help\n'));
await input({
message: 'Press Enter to continue...',
});
}
//# sourceMappingURL=ftux-handler.js.map