feat(cron): add lore cron command for automated sync scheduling
Add lore cron {install,uninstall,status} to manage a crontab entry that
runs lore sync on a configurable interval. Supports both human and robot
output modes.
Core implementation (src/core/cron.rs):
- install_cron: appends a tagged crontab entry, detects existing entries
- uninstall_cron: removes the tagged entry
- cron_status: reads crontab + checks last-sync time from the database
- Unix-only (#[cfg(unix)]) — compiles out on Windows
CLI wiring:
- CronAction enum and CronArgs in cli/mod.rs with after_help examples
- Robot JSON envelope with RobotMeta timing for all 3 sub-actions
- Dispatch in main.rs
Also in this commit:
- Add after_help example blocks to Status, Auth, Doctor, Init, Migrate,
Health commands for better discoverability
- Add LORE_ICONS env var documentation to CLI help text
- Simplify notes format dispatch in main.rs (removed csv/jsonl paths)
- Update commands/mod.rs re-exports for cron + notes cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,9 @@ use std::io::IsTerminal;
|
||||
GITLAB_TOKEN GitLab personal access token (or name set in config)
|
||||
LORE_ROBOT Enable robot/JSON mode (non-empty, non-zero value)
|
||||
LORE_CONFIG_PATH Override config file location
|
||||
NO_COLOR Disable color output (any non-empty value)")]
|
||||
NO_COLOR Disable color output (any non-empty value)
|
||||
LORE_ICONS Override icon set: nerd, unicode, or ascii
|
||||
NERD_FONTS Enable Nerd Font icons when set to a non-empty value")]
|
||||
pub struct Cli {
|
||||
/// Path to config file
|
||||
#[arg(short = 'c', long, global = true, help = "Path to config file")]
|
||||
@@ -135,19 +137,35 @@ pub enum Commands {
|
||||
Count(CountArgs),
|
||||
|
||||
/// Show sync state
|
||||
#[command(visible_alias = "st")]
|
||||
#[command(
|
||||
visible_alias = "st",
|
||||
after_help = "\x1b[1mExamples:\x1b[0m
|
||||
lore status # Show last sync times per project
|
||||
lore --robot status # JSON output for automation"
|
||||
)]
|
||||
Status,
|
||||
|
||||
/// Verify GitLab authentication
|
||||
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
||||
lore auth # Verify token and show user info
|
||||
lore --robot auth # JSON output for automation")]
|
||||
Auth,
|
||||
|
||||
/// Check environment health
|
||||
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
||||
lore doctor # Check config, token, database, Ollama
|
||||
lore --robot doctor # JSON output for automation")]
|
||||
Doctor,
|
||||
|
||||
/// Show version information
|
||||
Version,
|
||||
|
||||
/// Initialize configuration and database
|
||||
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
||||
lore init # Interactive setup
|
||||
lore init --force # Overwrite existing config
|
||||
lore --robot init --gitlab-url https://gitlab.com \\
|
||||
--token-env-var GITLAB_TOKEN --projects group/repo # Non-interactive setup")]
|
||||
Init {
|
||||
/// Skip overwrite confirmation
|
||||
#[arg(short = 'f', long)]
|
||||
@@ -174,11 +192,14 @@ pub enum Commands {
|
||||
default_project: Option<String>,
|
||||
},
|
||||
|
||||
/// Back up local database (not yet implemented)
|
||||
#[command(hide = true)]
|
||||
Backup,
|
||||
|
||||
/// Reset local database (not yet implemented)
|
||||
#[command(hide = true)]
|
||||
Reset {
|
||||
/// Skip confirmation prompt
|
||||
#[arg(short = 'y', long)]
|
||||
yes: bool,
|
||||
},
|
||||
@@ -202,9 +223,15 @@ pub enum Commands {
|
||||
Sync(SyncArgs),
|
||||
|
||||
/// Run pending database migrations
|
||||
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
||||
lore migrate # Apply pending migrations
|
||||
lore --robot migrate # JSON output for automation")]
|
||||
Migrate,
|
||||
|
||||
/// Quick health check: config, database, schema version
|
||||
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
||||
lore health # Quick pre-flight check (exit 0 = healthy)
|
||||
lore --robot health # JSON output for automation")]
|
||||
Health,
|
||||
|
||||
/// Machine-readable command manifest for agent self-discovery
|
||||
@@ -242,6 +269,10 @@ pub enum Commands {
|
||||
Trace(TraceArgs),
|
||||
|
||||
/// Detect discussion divergence from original intent
|
||||
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
||||
lore drift issues 42 # Check drift on issue #42
|
||||
lore drift issues 42 --threshold 0.3 # Custom similarity threshold
|
||||
lore --robot drift issues 42 -p group/repo # JSON output, scoped to project")]
|
||||
Drift {
|
||||
/// Entity type (currently only "issues" supported)
|
||||
#[arg(value_parser = ["issues"])]
|
||||
@@ -259,6 +290,14 @@ pub enum Commands {
|
||||
project: Option<String>,
|
||||
},
|
||||
|
||||
/// Manage cron-based automatic syncing
|
||||
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
||||
lore cron install # Install cron job (every 8 minutes)
|
||||
lore cron install --interval 15 # Custom interval
|
||||
lore cron status # Check if cron is installed
|
||||
lore cron uninstall # Remove cron job")]
|
||||
Cron(CronArgs),
|
||||
|
||||
#[command(hide = true)]
|
||||
List {
|
||||
#[arg(value_parser = ["issues", "mrs"])]
|
||||
@@ -344,7 +383,7 @@ pub struct IssuesArgs {
|
||||
pub fields: Option<Vec<String>>,
|
||||
|
||||
/// Filter by state (opened, closed, all)
|
||||
#[arg(short = 's', long, help_heading = "Filters")]
|
||||
#[arg(short = 's', long, help_heading = "Filters", value_parser = ["opened", "closed", "all"])]
|
||||
pub state: Option<String>,
|
||||
|
||||
/// Filter by project path
|
||||
@@ -438,7 +477,7 @@ pub struct MrsArgs {
|
||||
pub fields: Option<Vec<String>>,
|
||||
|
||||
/// Filter by state (opened, merged, closed, locked, all)
|
||||
#[arg(short = 's', long, help_heading = "Filters")]
|
||||
#[arg(short = 's', long, help_heading = "Filters", value_parser = ["opened", "merged", "closed", "locked", "all"])]
|
||||
pub state: Option<String>,
|
||||
|
||||
/// Filter by project path
|
||||
@@ -535,15 +574,6 @@ pub struct NotesArgs {
|
||||
#[arg(long, help_heading = "Output", value_delimiter = ',')]
|
||||
pub fields: Option<Vec<String>>,
|
||||
|
||||
/// Output format (table, json, jsonl, csv)
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "table",
|
||||
value_parser = ["table", "json", "jsonl", "csv"],
|
||||
help_heading = "Output"
|
||||
)]
|
||||
pub format: String,
|
||||
|
||||
/// Filter by author username
|
||||
#[arg(short = 'a', long, help_heading = "Filters")]
|
||||
pub author: Option<String>,
|
||||
@@ -655,6 +685,11 @@ pub struct IngestArgs {
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
||||
lore stats # Show document and index statistics
|
||||
lore stats --check # Run integrity checks
|
||||
lore stats --repair --dry-run # Preview what repair would fix
|
||||
lore --robot stats # JSON output for automation")]
|
||||
pub struct StatsArgs {
|
||||
/// Run integrity checks
|
||||
#[arg(long, overrides_with = "no_check")]
|
||||
@@ -743,6 +778,10 @@ pub struct SearchArgs {
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
||||
lore generate-docs # Generate docs for dirty entities
|
||||
lore generate-docs --full # Full rebuild of all documents
|
||||
lore generate-docs --full -p group/repo # Full rebuild for one project")]
|
||||
pub struct GenerateDocsArgs {
|
||||
/// Full rebuild: seed all entities into dirty queue, then drain
|
||||
#[arg(long)]
|
||||
@@ -805,9 +844,17 @@ pub struct SyncArgs {
|
||||
/// Show detailed timing breakdown for sync stages
|
||||
#[arg(short = 't', long = "timings")]
|
||||
pub timings: bool,
|
||||
|
||||
/// Acquire file lock before syncing (skip if another sync is running)
|
||||
#[arg(long)]
|
||||
pub lock: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
||||
lore embed # Embed new/changed documents
|
||||
lore embed --full # Re-embed all documents from scratch
|
||||
lore embed --retry-failed # Retry previously failed embeddings")]
|
||||
pub struct EmbedArgs {
|
||||
/// Re-embed all documents (clears existing embeddings first)
|
||||
#[arg(long, overrides_with = "no_full")]
|
||||
@@ -1046,6 +1093,10 @@ pub struct TraceArgs {
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
||||
lore count issues # Total issues in local database
|
||||
lore count notes --for mr # Notes on merge requests only
|
||||
lore count discussions --for issue # Discussions on issues only")]
|
||||
pub struct CountArgs {
|
||||
/// Entity type to count (issues, mrs, discussions, notes, events)
|
||||
#[arg(value_parser = ["issues", "mrs", "discussions", "notes", "events"])]
|
||||
@@ -1055,3 +1106,25 @@ pub struct CountArgs {
|
||||
#[arg(short = 'f', long = "for", value_parser = ["issue", "mr"])]
|
||||
pub for_entity: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct CronArgs {
|
||||
#[command(subcommand)]
|
||||
pub action: CronAction,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum CronAction {
|
||||
/// Install cron job for automatic syncing
|
||||
Install {
|
||||
/// Sync interval in minutes (default: 8)
|
||||
#[arg(long, default_value = "8")]
|
||||
interval: u32,
|
||||
},
|
||||
|
||||
/// Remove cron job
|
||||
Uninstall,
|
||||
|
||||
/// Show current cron configuration
|
||||
Status,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user