338 lines
10 KiB
Plaintext
338 lines
10 KiB
Plaintext
#!/usr/bin/env nu
|
|
# Export KOGRAL to Logseq format
|
|
#
|
|
# Usage: nu kogral-export-logseq.nu <output-path> [--kogral-dir <path>] [--dry-run]
|
|
|
|
def main [
|
|
output_path: string # Path for Logseq graph output
|
|
--kogral-dir: string = ".kogral" # KOGRAL directory
|
|
--dry-run # Show what would be exported without making changes
|
|
--skip-journals # Skip exporting journal entries
|
|
] {
|
|
print $"(ansi green_bold)Logseq Export(ansi reset)"
|
|
print $"Source: ($kogral_dir)"
|
|
print $"Target: ($output_path)"
|
|
|
|
if $dry_run {
|
|
print $"(ansi yellow)DRY RUN MODE - No changes will be made(ansi reset)"
|
|
}
|
|
|
|
# Check if .kogral directory exists
|
|
if not ($kogral_dir | path exists) {
|
|
print $"(ansi red)Error: KOGRAL directory not found: ($kogral_dir)(ansi reset)"
|
|
exit 1
|
|
}
|
|
|
|
# Count files to export
|
|
let stats = get_export_stats $kogral_dir $skip_journals
|
|
|
|
print $"\n(ansi cyan_bold)Files to export:(ansi reset)"
|
|
print $" Notes: ($stats.notes)"
|
|
print $" Decisions: ($stats.decisions)"
|
|
print $" Guidelines: ($stats.guidelines)"
|
|
print $" Patterns: ($stats.patterns)"
|
|
print $" Journals: ($stats.journals)"
|
|
print $" Total: ($stats.total)"
|
|
|
|
if $stats.total == 0 {
|
|
print $"\n(ansi yellow)No files to export(ansi reset)"
|
|
exit 0
|
|
}
|
|
|
|
if $dry_run {
|
|
print $"\n(ansi yellow)[DRY RUN] Would export ($stats.total) files(ansi reset)"
|
|
exit 0
|
|
}
|
|
|
|
# Create Logseq directory structure
|
|
print $"\n(ansi cyan_bold)Creating Logseq directory structure...(ansi reset)"
|
|
create_logseq_structure $output_path
|
|
|
|
# Export files
|
|
print $"\n(ansi cyan_bold)Exporting files...(ansi reset)"
|
|
|
|
export_nodes $"($kogral_dir)/notes" $"($output_path)/pages" "note"
|
|
export_nodes $"($kogral_dir)/decisions" $"($output_path)/pages" "decision"
|
|
export_nodes $"($kogral_dir)/guidelines" $"($output_path)/pages" "guideline"
|
|
export_nodes $"($kogral_dir)/patterns" $"($output_path)/pages" "pattern"
|
|
|
|
if not $skip_journals {
|
|
export_journals $"($kogral_dir)/journal" $"($output_path)/journals"
|
|
}
|
|
|
|
# Create Logseq config
|
|
print $"\n(ansi cyan_bold)Creating Logseq configuration...(ansi reset)"
|
|
create_logseq_config $output_path
|
|
|
|
print $"\n(ansi green_bold)✓ Export completed(ansi reset)"
|
|
print $"Exported ($stats.total) files to ($output_path)"
|
|
}
|
|
|
|
def get_export_stats [kogral_dir: string, skip_journals: bool] {
|
|
let notes = if ($"($kogral_dir)/notes" | path exists) {
|
|
glob $"($kogral_dir)/notes/**/*.md" | length
|
|
} else { 0 }
|
|
|
|
let decisions = if ($"($kogral_dir)/decisions" | path exists) {
|
|
glob $"($kogral_dir)/decisions/**/*.md" | length
|
|
} else { 0 }
|
|
|
|
let guidelines = if ($"($kogral_dir)/guidelines" | path exists) {
|
|
glob $"($kogral_dir)/guidelines/**/*.md" | length
|
|
} else { 0 }
|
|
|
|
let patterns = if ($"($kogral_dir)/patterns" | path exists) {
|
|
glob $"($kogral_dir)/patterns/**/*.md" | length
|
|
} else { 0 }
|
|
|
|
let journals = if not $skip_journals and ($"($kogral_dir)/journal" | path exists) {
|
|
glob $"($kogral_dir)/journal/**/*.md" | length
|
|
} else { 0 }
|
|
|
|
{
|
|
notes: $notes,
|
|
decisions: $decisions,
|
|
guidelines: $guidelines,
|
|
patterns: $patterns,
|
|
journals: $journals,
|
|
total: ($notes + $decisions + $guidelines + $patterns + $journals)
|
|
}
|
|
}
|
|
|
|
def create_logseq_structure [output_path: string] {
|
|
mkdir $output_path
|
|
mkdir $"($output_path)/pages"
|
|
mkdir $"($output_path)/journals"
|
|
mkdir $"($output_path)/assets"
|
|
mkdir $"($output_path)/logseq"
|
|
|
|
print $" (ansi green)✓ Directory structure created(ansi reset)"
|
|
}
|
|
|
|
def export_nodes [source_dir: string, target_dir: string, node_type: string] {
|
|
if not ($source_dir | path exists) {
|
|
return
|
|
}
|
|
|
|
let files = glob $"($source_dir)/**/*.md"
|
|
if ($files | length) == 0 {
|
|
return
|
|
}
|
|
|
|
print $"\n Exporting ($node_type)s..."
|
|
|
|
mut exported = 0
|
|
let total = $files | length
|
|
|
|
for file in $files {
|
|
let filename = $file | path basename
|
|
|
|
# Phase 1: Read KOGRAL markdown file
|
|
let content = open $file
|
|
|
|
# Phase 2: Convert to Logseq format
|
|
let logseq_content = convert_kogral_to_logseq $content $node_type
|
|
|
|
# Phase 3: Save to Logseq pages directory
|
|
$logseq_content | save $"($target_dir)/($filename)"
|
|
|
|
$exported = $exported + 1
|
|
}
|
|
|
|
print $" (ansi green)✓ Exported ($exported)/($total) ($node_type)s(ansi reset)"
|
|
}
|
|
|
|
def export_journals [source_dir: string, target_dir: string] {
|
|
if not ($source_dir | path exists) {
|
|
return
|
|
}
|
|
|
|
let files = glob $"($source_dir)/**/*.md"
|
|
if ($files | length) == 0 {
|
|
return
|
|
}
|
|
|
|
print $"\n Exporting journals..."
|
|
|
|
mut exported = 0
|
|
let total = $files | length
|
|
|
|
for file in $files {
|
|
let filename = $file | path basename
|
|
|
|
# Phase 1: Read KOGRAL journal file
|
|
let content = open $file
|
|
|
|
# Phase 2: Convert to Logseq format
|
|
let logseq_content = convert_kogral_to_logseq $content "journal"
|
|
|
|
# Phase 3: Save to Logseq journals directory
|
|
$logseq_content | save $"($target_dir)/($filename)"
|
|
|
|
$exported = $exported + 1
|
|
}
|
|
|
|
print $" (ansi green)✓ Exported ($exported)/($total) journals(ansi reset)"
|
|
}
|
|
|
|
def convert_kogral_to_logseq [content: string, node_type: string] {
|
|
let lines = $content | lines
|
|
|
|
# Phase 1: Check for and parse YAML frontmatter
|
|
let has_frontmatter = ($lines | get 0 | str trim) == "---"
|
|
|
|
if not $has_frontmatter {
|
|
# No frontmatter, return content as-is with minimal properties
|
|
return $"type:: ($node_type)\n\n$content"
|
|
}
|
|
|
|
# Phase 2: Extract frontmatter and body
|
|
let frontmatter_end = get_frontmatter_end_index $lines
|
|
let frontmatter_lines = $lines | take $frontmatter_end
|
|
let body_lines = $lines | skip $frontmatter_end
|
|
|
|
# Phase 3: Parse YAML fields
|
|
let fm = parse_yaml_frontmatter $frontmatter_lines
|
|
|
|
# Phase 4: Convert to Logseq properties format
|
|
mut logseq_props = ""
|
|
|
|
# Add type property
|
|
$logseq_props = $logseq_props + $"type:: ($node_type)\n"
|
|
|
|
# Add title if present
|
|
if not ($fm.title? | is-empty) {
|
|
$logseq_props = $logseq_props + $"title:: ($fm.title?)\n"
|
|
}
|
|
|
|
# Add created date if present
|
|
if not ($fm.created? | is-empty) {
|
|
let created_date = convert_date_to_logseq $fm.created?
|
|
$logseq_props = $logseq_props + $"created:: [[$created_date]]\n"
|
|
}
|
|
|
|
# Add tags if present
|
|
if not ($fm.tags? | is-empty) {
|
|
$logseq_props = $logseq_props + "tags:: "
|
|
let tags_list = $fm.tags? | str replace '\[' '' | str replace '\]' '' | split row ','
|
|
for tag in $tags_list {
|
|
let trimmed = $tag | str trim | str replace '"' ''
|
|
$logseq_props = $logseq_props + $"[[($trimmed)]] "
|
|
}
|
|
$logseq_props = $logseq_props + "\n"
|
|
}
|
|
|
|
# Add status if present
|
|
if not ($fm.status? | is-empty) {
|
|
$logseq_props = $logseq_props + $"status:: ($fm.status?)\n"
|
|
}
|
|
|
|
# Add relationships if present
|
|
if not ($fm.relates_to? | is-empty) {
|
|
$logseq_props = $logseq_props + "relates-to:: "
|
|
let refs = parse_yaml_list $fm.relates_to?
|
|
let refs_formatted = $refs | each { |r| $'[[($r)]]' }
|
|
$logseq_props = $logseq_props + ($refs_formatted | str join ", ") + "\n"
|
|
}
|
|
|
|
if not ($fm.depends_on? | is-empty) {
|
|
$logseq_props = $logseq_props + "depends-on:: "
|
|
let refs = parse_yaml_list $fm.depends_on?
|
|
let refs_formatted = $refs | each { |r| $'[[($r)]]' }
|
|
$logseq_props = $logseq_props + ($refs_formatted | str join ", ") + "\n"
|
|
}
|
|
|
|
# Phase 5: Build final output
|
|
let body = $body_lines | str join "\n"
|
|
$"$logseq_props\n$body"
|
|
}
|
|
|
|
def get_frontmatter_end_index [lines: list] {
|
|
mut idx = 1 # Skip first "---"
|
|
|
|
for line in ($lines | skip 1) {
|
|
if ($line | str trim) == "---" {
|
|
return ($idx + 1)
|
|
}
|
|
$idx = $idx + 1
|
|
}
|
|
|
|
$idx
|
|
}
|
|
|
|
def parse_yaml_frontmatter [lines: list] {
|
|
mut fm = {}
|
|
|
|
for line in $lines {
|
|
if ($line | str trim) == "---" {
|
|
continue
|
|
}
|
|
|
|
# Match YAML key: value format
|
|
if ($line =~ '^[\w]+:') {
|
|
let key = $line | str replace '^(\w+):.*' '$1'
|
|
let value = $line | str replace '^[\w]+:\s*' '' | str trim
|
|
$fm = ($fm | insert $key $value)
|
|
}
|
|
}
|
|
|
|
$fm
|
|
}
|
|
|
|
def convert_date_to_logseq [date_str: string] {
|
|
# Convert ISO 8601 (2026-01-17T10:30:00Z) to Logseq format (Jan 17th, 2026)
|
|
# For simplicity, extract date part and format
|
|
let date_part = $date_str | str substring 0..10
|
|
let year = $date_part | str substring 0..4
|
|
let month = $date_part | str substring 5..7
|
|
let day = $date_part | str substring 8..10 | str replace '^0+' ''
|
|
|
|
let month_name = match $month {
|
|
"01" => "Jan",
|
|
"02" => "Feb",
|
|
"03" => "Mar",
|
|
"04" => "Apr",
|
|
"05" => "May",
|
|
"06" => "Jun",
|
|
"07" => "Jul",
|
|
"08" => "Aug",
|
|
"09" => "Sep",
|
|
"10" => "Oct",
|
|
"11" => "Nov",
|
|
"12" => "Dec",
|
|
_ => "Unknown"
|
|
}
|
|
|
|
let day_suffix = match ($day | into int) {
|
|
1 | 21 | 31 => "st",
|
|
2 | 22 => "nd",
|
|
3 | 23 => "rd",
|
|
_ => "th"
|
|
}
|
|
|
|
$"($month_name) ($day)($day_suffix), ($year)"
|
|
}
|
|
|
|
def parse_yaml_list [yaml_str: string] {
|
|
# Parse YAML list format: [item1, item2] or list format with dashes
|
|
# For now, handle bracket format
|
|
let cleaned = $yaml_str | str replace '\[' '' | str replace '\]' ''
|
|
let items = $cleaned | split row ',' | map { |i| $i | str trim | str replace '"' '' }
|
|
$items
|
|
}
|
|
|
|
def create_logseq_config [output_path: string] {
|
|
let config = {
|
|
"preferred-format": "markdown",
|
|
"preferred-workflow": ":now",
|
|
"hidden": [".git"],
|
|
"journal/page-title-format": "yyyy-MM-dd",
|
|
"start-of-week": 1,
|
|
"feature/enable-block-timestamps": false,
|
|
"feature/enable-search-remove-accents": true
|
|
}
|
|
|
|
$config | to json | save $"($output_path)/logseq/config.edn"
|
|
print $" (ansi green)✓ Logseq configuration created(ansi reset)"
|
|
}
|