kogral/scripts/kogral-import-logseq.nu
Jesús Pérez 9ea04852a8
Some checks failed
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
chore: add schemas and just recipes
2026-01-23 16:12:50 +00:00

389 lines
11 KiB
Plaintext

#!/usr/bin/env nu
# Import from Logseq graph to KOGRAL
#
# Usage: nu kogral-import-logseq.nu <logseq-path> [--kogral-dir <path>] [--dry-run]
def main [
logseq_path: string # Path to Logseq graph directory
--kogral-dir: string = ".kogral" # KOGRAL directory
--dry-run # Show what would be imported without making changes
--skip-journals # Skip importing journal entries
--skip-pages # Skip importing pages
] {
print $"(ansi green_bold)Logseq Import(ansi reset)"
print $"Source: ($logseq_path)"
print $"Target: ($kogral_dir)"
if $dry_run {
print $"(ansi yellow)DRY RUN MODE - No changes will be made(ansi reset)"
}
# Validate Logseq directory
if not ($logseq_path | path exists) {
print $"(ansi red)Error: Logseq path not found: ($logseq_path)(ansi reset)"
exit 1
}
let pages_dir = $"($logseq_path)/pages"
let journals_dir = $"($logseq_path)/journals"
if not ($pages_dir | path exists) and not ($journals_dir | path exists) {
print $"(ansi red)Error: Not a valid Logseq graph (missing pages or journals directory)(ansi reset)"
exit 1
}
# Validate KOGRAL directory
if not ($kogral_dir | path exists) {
print $"(ansi yellow)KOGRAL directory doesn't exist. Creating...(ansi reset)"
if not $dry_run {
mkdir $kogral_dir
mkdir $"($kogral_dir)/notes"
mkdir $"($kogral_dir)/journal"
}
}
# Count files to import
let pages_count = if ($pages_dir | path exists) and not $skip_pages {
glob $"($pages_dir)/**/*.md" | length
} else { 0 }
let journals_count = if ($journals_dir | path exists) and not $skip_journals {
glob $"($journals_dir)/**/*.md" | length
} else { 0 }
print $"\n(ansi cyan_bold)Files to import:(ansi reset)"
print $" Pages: ($pages_count)"
print $" Journals: ($journals_count)"
print $" Total: ($pages_count + $journals_count)"
if ($pages_count + $journals_count) == 0 {
print $"\n(ansi yellow)No files to import(ansi reset)"
exit 0
}
if $dry_run {
print $"\n(ansi yellow)[DRY RUN] Would import ($pages_count + $journals_count) files(ansi reset)"
exit 0
}
# Import pages
if not $skip_pages and $pages_count > 0 {
print $"\n(ansi cyan_bold)Importing pages...(ansi reset)"
import_pages $pages_dir $kogral_dir
}
# Import journals
if not $skip_journals and $journals_count > 0 {
print $"\n(ansi cyan_bold)Importing journals...(ansi reset)"
import_journals $journals_dir $kogral_dir
}
print $"\n(ansi green_bold)✓ Import completed(ansi reset)"
print $"Imported ($pages_count + $journals_count) files"
}
def import_pages [pages_dir: string, kogral_dir: string] {
let files = glob $"($pages_dir)/**/*.md"
let total = $files | length
mut imported = 0
mut decisions = 0
mut guidelines = 0
mut patterns = 0
mut notes = 0
for file in $files {
let filename = $file | path basename
print $" Importing ($filename)..."
# Phase 1: Read and parse Logseq format
let content = open $file
# Phase 2: Detect node type from properties/content
let node_type = detect_node_type $content
# Phase 3: Convert to KOGRAL format
let kogral_content = convert_logseq_to_kogral $content $node_type
# Phase 4: Determine target directory
let target_dir = match $node_type {
"decision" => {
$decisions = $decisions + 1
$"($kogral_dir)/decisions"
},
"guideline" => {
$guidelines = $guidelines + 1
$"($kogral_dir)/guidelines"
},
"pattern" => {
$patterns = $patterns + 1
$"($kogral_dir)/patterns"
},
_ => {
$notes = $notes + 1
$"($kogral_dir)/notes"
}
}
# Phase 5: Save to KOGRAL
mkdir $target_dir
$kogral_content | save $"($target_dir)/($filename)"
$imported = $imported + 1
print $" (ansi green)✓ Imported as ($node_type)(ansi reset)"
}
print $"\n(ansi green)Pages summary:(ansi reset)"
print $" Notes: ($notes)"
print $" Decisions: ($decisions)"
print $" Guidelines: ($guidelines)"
print $" Patterns: ($patterns)"
print $" Total: ($imported)/($total) imported"
}
def import_journals [journals_dir: string, kogral_dir: string] {
let files = glob $"($journals_dir)/**/*.md"
let total = $files | length
mkdir $"($kogral_dir)/journal"
mut imported = 0
for file in $files {
let filename = $file | path basename
print $" Importing ($filename)..."
# Phase 1: Read Logseq journal format
let content = open $file
# Phase 2: Convert to KOGRAL journal format
let kogral_content = convert_logseq_to_kogral $content "journal"
# Phase 3: Save to journal directory
$kogral_content | save $"($kogral_dir)/journal/($filename)"
$imported = $imported + 1
print $" (ansi green)✓ Imported(ansi reset)"
}
print $"\n(ansi green)Journals imported: ($imported)/($total)(ansi reset)"
}
def detect_node_type [content: string] {
# Check for type hints in properties or content
if ($content | str contains "type:: decision") or ($content | str contains "# Decision") {
"decision"
} else if ($content | str contains "type:: guideline") or ($content | str contains "# Guideline") {
"guideline"
} else if ($content | str contains "type:: pattern") or ($content | str contains "# Pattern") {
"pattern"
} else {
"note"
}
}
def convert_logseq_to_kogral [content: string, node_type: string] {
let lines = $content | lines
# Phase 1: Parse Logseq properties (key:: value format)
let props = parse_logseq_properties $lines
let body_start = get_body_start_index $lines
# Phase 2: Extract metadata from properties
let title = $props.title? | default (extract_title_from_lines $lines)
let created = $props.created? | default (date now | format date "%Y-%m-%dT%H:%M:%SZ")
let modified = $props.modified? | default (date now | format date "%Y-%m-%dT%H:%M:%SZ")
let status = match ($node_type) {
"journal" => "draft",
_ => "active"
}
# Phase 3: Extract tags and relationships from properties
let tags = parse_tags_from_properties $props
let relates_to = parse_references_from_property ($props.relates_to? | default "")
let depends_on = parse_references_from_property ($props.depends_on? | default "")
let implements = parse_references_from_property ($props.implements? | default "")
let extends = parse_references_from_property ($props.extends? | default "")
# Phase 4: Build YAML frontmatter
let frontmatter = build_yaml_frontmatter {
type: $node_type,
title: $title,
created: $created,
modified: $modified,
status: $status,
tags: $tags,
relates_to: $relates_to,
depends_on: $depends_on,
implements: $implements,
extends: $extends
}
# Phase 5: Extract body and preserve wikilinks
let body = if $body_start < ($lines | length) {
$lines | skip $body_start | str join "\n"
} else {
""
}
# Phase 6: Convert Logseq-specific syntax
let converted_body = convert_logseq_syntax $body
$"$frontmatter\n$converted_body"
}
def parse_logseq_properties [lines: list] {
mut props = {}
# Parse properties until blank line or content starts
for line in $lines {
if ($line | str trim | is-empty) {
break
}
# Match pattern: key:: value
if ($line =~ '^[\w-]+::') {
let key = $line | str replace '^(\w[\w-]*)::.*' '$1'
let value = $line | str replace '^[\w-]+::\s*' '' | str trim
$props = ($props | insert $key $value)
}
}
$props
}
def get_body_start_index [lines: list] {
# Find where properties end (first blank line or non-property line)
mut idx = 0
for line in $lines {
if ($line | str trim | is-empty) {
return ($idx + 1)
}
if not ($line =~ '^[\w-]+::') and ($line | str trim | length) > 0 {
return $idx
}
$idx = $idx + 1
}
$idx
}
def extract_title_from_lines [lines: list] {
# Extract from first heading or property
for line in $lines {
if ($line =~ '^#+ ') {
return ($line | str replace '^#+\s+' '')
}
if ($line =~ '^title::') {
return ($line | str replace '^title::\s*' '')
}
}
"Untitled"
}
def parse_tags_from_properties [props: record] {
mut tags = []
# Check tags property
if ($props.tags? | is-empty) {
return $tags
}
let tags_str = $props.tags?
# Extract [[tag]] format using split
if ($tags_str | str contains "[[") {
let parts = $tags_str | split row "[[" | skip 1
for part in $parts {
let tag = $part | split row "]]" | get 0
if ($tag | str length) > 0 {
$tags = ($tags | append $tag)
}
}
} else {
# Extract comma-separated format
$tags = ($tags_str | split row ',' | each { |t| $t | str trim })
}
$tags
}
def parse_references_from_property [prop: string] {
if ($prop | str length) == 0 {
return []
}
# Extract [[ref]] format using split
mut refs = []
if ($prop | str contains "[[") {
let parts = $prop | split row "[[" | skip 1
for part in $parts {
let ref = $part | split row "]]" | get 0
if ($ref | str length) > 0 {
$refs = ($refs | append $ref)
}
}
}
$refs
}
def build_yaml_frontmatter [data: record] {
mut fm = "---\n"
$fm = $fm + $"type: ($data.type)\n"
$fm = $fm + $"title: ($data.title)\n"
$fm = $fm + $"created: ($data.created)\n"
$fm = $fm + $"modified: ($data.modified)\n"
$fm = $fm + $"status: ($data.status)\n"
if ($data.tags | length) > 0 {
let quoted_tags = $data.tags | each { |t| $'"($t)"' }
let tags_str = $quoted_tags | str join ", "
$fm = $fm + $"tags: [$tags_str]\n"
}
if ($data.relates_to | length) > 0 {
$fm = $fm + "relates_to:\n"
for ref in $data.relates_to {
$fm = $fm + $" - $ref\n"
}
}
if ($data.depends_on | length) > 0 {
$fm = $fm + "depends_on:\n"
for ref in $data.depends_on {
$fm = $fm + $" - $ref\n"
}
}
if ($data.implements | length) > 0 {
$fm = $fm + "implements:\n"
for ref in $data.implements {
$fm = $fm + $" - $ref\n"
}
}
if ($data.extends | length) > 0 {
$fm = $fm + "extends:\n"
for ref in $data.extends {
$fm = $fm + $" - $ref\n"
}
}
$fm + "---"
}
def convert_logseq_syntax [body: string] {
# Phase 1: Convert Logseq task markers to standard markdown
mut converted = $body | str replace -a 'LATER ' ''
$converted = $converted | str replace -a 'NOW ' ''
$converted = $converted | str replace -a 'WAITING ' ''
$converted = $converted | str replace -a 'CANCELLED ' ''
$converted
}