diff --git a/README.md b/README.md index eed8146..f0392b8 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The project is under active development. Core blogging workflows are broadly ava - Post and translation authoring with draft/published lifecycle, metadata, tags, categories, links, media, and batch gallery-image import. - Media import, thumbnails, metadata translations, filters, validation, and post assignment. - WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping. -- Template and Lua script management with explicit syntax-check feedback, using a custom Ropey/Syntect/Cosmic Text editor and the documented, bDS2-signature-compatible project-scoped [`bds` host API](docs/scripting/API_REFERENCE.md) across utilities, rendered macros, and Blogmark transforms. +- Template and Lua script management with explicit syntax-check feedback, using a custom Ropey/Syntect/Cosmic Text editor and the documented, bDS2-signature-compatible project-scoped [`bds` host API](docs/scripting/API_REFERENCE.md) across utilities, rendered macros, and Blogmark transforms, including airplane-gated Git sync. - SQLite and filesystem persistence with frontmatter, sidecars, rebuild, metadata diff/repair, and FTS5 search. - Optional on-device multilingual semantic search and tag suggestions backed by a persistent USearch index, with duplicate-post review and dismissal in the desktop workspace. - Read-only in-app browsers in the Help menu for the bundled global `DOCUMENTATION.md`, the generated Lua API reference with public types and runnable examples, the [CLI/server/TUI documentation](CLI.md), and the [MCP server documentation](MCP.md), with safe GFM rendering and confirmed external links. diff --git a/crates/bds-cli/src/lib.rs b/crates/bds-cli/src/lib.rs index 91f898d..3f1f770 100644 --- a/crates/bds-cli/src/lib.rs +++ b/crates/bds-cli/src/lib.rs @@ -335,47 +335,20 @@ fn active_project(db: &Database) -> Result<(Project, PathBuf)> { fn rebuild(db: &Database, incremental: bool) -> Result { let (project, data_dir) = active_project(db)?; if incremental { - let (applied, imported, failed) = cli_sync::run_cli_mutation(db.conn(), || { - let report = - engine::metadata_diff::compute_metadata_diff(db.conn(), &data_dir, &project.id)?; - if !report.errors.is_empty() { - return Err(engine::EngineError::Validation(report.errors.join("; "))); - } - for item in &report.diffs { - engine::metadata_diff::repair_metadata_diff_item( - db.conn(), - &data_dir, - &project.id, - engine::metadata_diff::RepairDirection::FileToDatabase, - item, - )?; - } - let filesystem_orphans = report - .orphans - .iter() - .filter(|orphan| orphan.reason == "file_without_db_entry") - .collect::>(); - let mut imported = 0; - let mut failed = 0; - for orphan in filesystem_orphans { - match engine::metadata_diff::import_orphan_file( - db.conn(), - &data_dir, - &project.id, - orphan, - ) { - Ok(()) => imported += 1, - Err(_) => failed += 1, - } - } - if !report.diffs.is_empty() || imported > 0 { + let report = cli_sync::run_cli_mutation(db.conn(), || { + let report = engine::rebuild::rebuild_incremental(db.conn(), &data_dir, &project.id)?; + if report.differences_applied > 0 || report.orphans_imported > 0 { emit_bulk(&project.id); } - Ok((report.diffs.len(), imported, failed)) + Ok(report) })?; return Ok(output( "Applied incremental filesystem changes", - json!({"differences_applied": applied, "orphans_imported": imported, "orphans_failed": failed}), + json!({ + "differences_applied": report.differences_applied, + "orphans_imported": report.orphans_imported, + "orphans_failed": report.orphans_failed, + }), )); } diff --git a/crates/bds-core/src/db/migrations.rs b/crates/bds-core/src/db/migrations.rs index 5642f70..76f124a 100644 --- a/crates/bds-core/src/db/migrations.rs +++ b/crates/bds-core/src/db/migrations.rs @@ -16,11 +16,10 @@ mod tests { use crate::db::Database; use crate::db::schema::{ ai_catalog_meta, ai_endpoint_models, ai_model_modalities, ai_models, ai_providers, - chat_conversations, - chat_messages, db_notifications, dismissed_duplicate_pairs, embedding_keys, - generated_file_hashes, import_definitions, mcp_proposals, media, media_translations, - post_links, post_media, post_translations, posts, projects, scripts, settings, tags, - templates, + chat_conversations, chat_messages, db_notifications, dismissed_duplicate_pairs, + embedding_keys, generated_file_hashes, import_definitions, mcp_proposals, media, + media_translations, post_links, post_media, post_translations, posts, projects, scripts, + settings, tags, templates, }; use diesel::prelude::*; use diesel_migrations::MigrationHarness; diff --git a/crates/bds-core/src/engine/rebuild.rs b/crates/bds-core/src/engine/rebuild.rs index b62bb97..91fc291 100644 --- a/crates/bds-core/src/engine/rebuild.rs +++ b/crates/bds-core/src/engine/rebuild.rs @@ -29,6 +29,51 @@ pub struct FullRebuildReport { pub errors: Vec, } +#[derive(Debug, Default, PartialEq, Eq)] +pub struct IncrementalRebuildReport { + pub differences_applied: usize, + pub orphans_imported: usize, + pub orphans_failed: usize, +} + +/// Apply portable filesystem changes to one project's cache database. +pub fn rebuild_incremental( + conn: &Connection, + data_dir: &Path, + project_id: &str, +) -> EngineResult { + let report = super::metadata_diff::compute_metadata_diff(conn, data_dir, project_id)?; + if !report.errors.is_empty() { + return Err(crate::engine::EngineError::Validation( + report.errors.join("; "), + )); + } + for item in &report.diffs { + super::metadata_diff::repair_metadata_diff_item( + conn, + data_dir, + project_id, + super::metadata_diff::RepairDirection::FileToDatabase, + item, + )?; + } + let mut result = IncrementalRebuildReport { + differences_applied: report.diffs.len(), + ..Default::default() + }; + for orphan in report + .orphans + .iter() + .filter(|orphan| orphan.reason == "file_without_db_entry") + { + match super::metadata_diff::import_orphan_file(conn, data_dir, project_id, orphan) { + Ok(()) => result.orphans_imported += 1, + Err(_) => result.orphans_failed += 1, + } + } + Ok(result) +} + /// Progress callback: (percent 0.0..1.0, phase description). pub type ProgressFn = Arc; diff --git a/crates/bds-core/src/scripting/core_host.rs b/crates/bds-core/src/scripting/core_host.rs index 1ab7780..689c82c 100644 --- a/crates/bds-core/src/scripting/core_host.rs +++ b/crates/bds-core/src/scripting/core_host.rs @@ -1239,6 +1239,64 @@ impl CoreHost { Ok(Value::Bool(true)) } + fn sync(&self, method: &str, args: &[Value]) -> HostResult { + if method == "check_availability" { + return Ok(std::process::Command::new("git") + .arg("--version") + .output() + .is_ok() + .into()); + } + if self.offline_mode && matches!(method, "fetch" | "pull" | "push") { + return Err(format!("Git {method} is unavailable in airplane mode").into()); + } + let git = engine::git::GitEngine::new(&self.data_dir); + let cancelled = || { + self.task_manager + .as_ref() + .zip(self.task_id) + .is_some_and(|(manager, id)| manager.is_cancelled(id)) + }; + match method { + "get_repo_state" | "get_remote_state" => { + Ok(public_git_repository(git.repository().map_err(text)?)) + } + "get_status" => Ok(json!({ + "files": git.status().map_err(text)?.into_iter().map(public_git_status).collect::>(), + })), + "get_history" => { + let repository = git.repository().map_err(text)?; + let commits = match repository.current_branch { + Some(branch) => git.history(&branch).map_err(text)?, + None => Vec::new(), + }; + Ok(json!({ + "commits": commits.into_iter().map(public_git_commit).collect::>(), + })) + } + "fetch" => { + let result = git.fetch(cancelled, |_| {}).map_err(text)?; + Ok(json!({"updated": true, "output": result.output})) + } + "pull" => { + let result = git.pull(cancelled, |_| {}).map_err(text)?; + let db = self.database()?; + engine::rebuild::rebuild_incremental(db.conn(), &self.data_dir, &self.project_id)?; + Ok(json!({"updated": true, "output": result.output})) + } + "push" => { + let result = git.push(cancelled, |_| {}).map_err(text)?; + Ok(json!({"updated": true, "output": result.output})) + } + "commit_all" => { + let message = string_arg(args, 0)?; + let result = git.commit_all(message).map_err(text)?; + Ok(json!({"message": message, "output": result.output})) + } + _ => Err(format!("unknown sync capability: {method}").into()), + } + } + fn publish(&self, method: &str, args: &[Value]) -> HostResult { if method != "upload_site" { return Err(format!("unknown publish capability: {method}").into()); @@ -1492,6 +1550,7 @@ impl HostApi for CoreHost { "templates" => self.templates(method, &arguments), "tags" => self.tags(method, &arguments), "tasks" => self.tasks(method, &arguments), + "sync" => self.sync(method, &arguments), "publish" => self.publish(method, &arguments), "chat" => self.chat(method, &arguments), "embeddings" => self.embeddings(method, &arguments), @@ -1587,6 +1646,54 @@ fn public_tasks(values: Vec) -> HostResult { .map(Value::Array) } +fn public_git_repository(value: engine::git::GitRepository) -> Value { + json!({ + "is_initialized": value.is_initialized, + "remote_url": value.remote_url, + "provider": value.provider.map(|provider| json!({"kind": match provider { + engine::git::GitProvider::GitHub => "github", + engine::git::GitProvider::GitLab => "gitlab", + engine::git::GitProvider::GiteaForgejo => "gitea_forgejo", + }})), + "current_branch": value.current_branch, + "has_lfs": value.has_lfs, + }) +} + +fn public_git_status(value: engine::git::GitFileStatus) -> Value { + let mut result = Map::new(); + result.insert("path".into(), value.path.into()); + if let Some(old_path) = value.old_path { + result.insert("old_path".into(), old_path.into()); + } + result.insert( + "status".into(), + match value.kind { + engine::git::FileStatusKind::Added => "added", + engine::git::FileStatusKind::Modified => "modified", + engine::git::FileStatusKind::Deleted => "deleted", + engine::git::FileStatusKind::Renamed => "renamed", + engine::git::FileStatusKind::Untracked => "untracked", + } + .into(), + ); + Value::Object(result) +} + +fn public_git_commit(value: engine::git::GitCommit) -> Value { + json!({ + "hash": value.hash, + "subject": value.subject, + "author": value.author, + "date": value.date, + "sync_status": {"kind": match value.sync_status { + engine::git::SyncStatus::LocalOnly => "local_only", + engine::git::SyncStatus::RemoteOnly => "remote_only", + engine::git::SyncStatus::Both => "both", + }}, + }) +} + fn public_metadata(data_dir: &Path) -> HostResult { let metadata = engine::meta::read_project_json(data_dir)?; Ok(json!({ @@ -1862,6 +1969,255 @@ fn one_shot_json(value: engine::ai::OneShotResponse) -> HostResult { mod tests { use super::*; use crate::scripting::{ExecutionControl, ExecutionKind, execute_with_host}; + use std::fs; + use std::process::Command; + + struct SyncFixture { + _temp: tempfile::TempDir, + db_path: PathBuf, + data_dir: PathBuf, + remote_dir: PathBuf, + project_id: String, + } + + impl SyncFixture { + fn new() -> Self { + let temp = tempfile::tempdir().unwrap(); + let db_path = temp.path().join("ruds.db"); + let data_dir = temp.path().join("project"); + let remote_dir = temp.path().join("remote.git"); + let upstream_dir = temp.path().join("upstream"); + let db = Database::open(&db_path).unwrap(); + db.migrate().unwrap(); + crate::db::fts::ensure_fts_tables(db.conn()).unwrap(); + let project = engine::project::create_project( + db.conn(), + "Active", + Some(data_dir.to_str().unwrap()), + ) + .unwrap(); + engine::project::set_active_project(db.conn(), &project.id).unwrap(); + drop(db); + + git(&data_dir, &["init", "-b", "master"]); + configure_git(&data_dir); + git(&data_dir, &["add", "-A"]); + git(&data_dir, &["commit", "-m", "Initial project"]); + git( + temp.path(), + &[ + "init", + "--bare", + "-b", + "master", + remote_dir.to_str().unwrap(), + ], + ); + git( + &data_dir, + &["remote", "add", "origin", remote_dir.to_str().unwrap()], + ); + git(&data_dir, &["push", "-u", "origin", "master"]); + git( + temp.path(), + &[ + "clone", + remote_dir.to_str().unwrap(), + upstream_dir.to_str().unwrap(), + ], + ); + configure_git(&upstream_dir); + let mut metadata = engine::meta::read_project_json(&upstream_dir).unwrap(); + metadata.name = "Pulled Name".into(); + engine::meta::write_project_json(&upstream_dir, &metadata).unwrap(); + git(&upstream_dir, &["add", "-A"]); + git(&upstream_dir, &["commit", "-m", "Update project name"]); + git(&upstream_dir, &["push", "origin", "master"]); + fs::write(data_dir.join("pending.txt"), "pending").unwrap(); + + Self { + _temp: temp, + db_path, + data_dir, + remote_dir, + project_id: project.id, + } + } + + fn host(&self, offline: bool) -> CoreHost { + CoreHost::new(&self.db_path, &self.project_id, &self.data_dir) + .with_offline_mode(offline) + } + } + + fn configure_git(dir: &Path) { + git(dir, &["config", "user.name", "Lua Test"]); + git(dir, &["config", "user.email", "lua@example.invalid"]); + } + + fn git(dir: &Path, args: &[&str]) -> String { + let output = Command::new("git") + .args(args) + .current_dir(dir) + .env("GIT_TERMINAL_PROMPT", "0") + .output() + .unwrap(); + assert!( + output.status.success(), + "git {} failed: {}", + args.join(" "), + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8(output.stdout).unwrap().trim().to_owned() + } + + #[test] + fn sync_namespace_runs_every_bds2_method_and_reconciles_pull() { + let fixture = SyncFixture::new(); + let result = execute_with_host( + r#" + function main() + local status = bds.sync.get_status() + local history = bds.sync.get_history() + return { + available = bds.sync.check_availability(), + repo = bds.sync.get_repo_state(), + remote = bds.sync.get_remote_state(), + status = status, + history = history, + fetched = bds.sync.fetch(), + pulled = bds.sync.pull(), + committed = bds.sync.commit_all("Lua commit"), + pushed = bds.sync.push(), + } + end + "#, + "main", + &Value::Null, + ExecutionKind::Utility, + &ExecutionControl::default(), + Arc::new(fixture.host(false)), + ) + .unwrap(); + + assert_eq!(result.value["available"], true); + assert_eq!(result.value["repo"]["is_initialized"], true); + assert_eq!(result.value["remote"], result.value["repo"]); + assert!( + result.value["status"]["files"] + .as_array() + .unwrap() + .iter() + .any(|file| file == &json!({"path":"pending.txt","status":"untracked"})) + ); + let commit = &result.value["history"]["commits"][0]; + assert!(commit["date"].as_str().is_some_and(|date| date.len() == 10)); + assert!(commit["sync_status"]["kind"].is_string()); + assert_eq!(result.value["fetched"]["updated"], true); + assert_eq!(result.value["pulled"]["updated"], true); + assert_eq!(result.value["committed"]["message"], "Lua commit"); + assert_eq!(result.value["pushed"]["updated"], true); + + let db = Database::open(&fixture.db_path).unwrap(); + assert_eq!( + project::get_project_by_id(db.conn(), &fixture.project_id) + .unwrap() + .name, + "Pulled Name" + ); + assert_eq!( + git(&fixture.remote_dir, &["log", "-1", "--format=%s"]), + "Lua commit" + ); + } + + #[test] + fn sync_namespace_uses_nil_for_non_repository_failures() { + let temp = tempfile::tempdir().unwrap(); + let data_dir = temp.path().join("not-a-repository"); + fs::create_dir(&data_dir).unwrap(); + let result = execute_with_host( + r#" + function main() + return { + repo = bds.sync.get_repo_state(), + remote = bds.sync.get_remote_state(), + status = bds.sync.get_status(), + history = bds.sync.get_history(), + fetched = bds.sync.fetch(), + pulled = bds.sync.pull(), + pushed = bds.sync.push(), + committed = bds.sync.commit_all("no repository"), + } + end + "#, + "main", + &Value::Null, + ExecutionKind::Utility, + &ExecutionControl::default(), + Arc::new(CoreHost::new( + temp.path().join("missing.db"), + "p1", + &data_dir, + )), + ) + .unwrap(); + + assert_eq!(result.value["repo"]["is_initialized"], false); + assert_eq!(result.value["remote"], result.value["repo"]); + assert_eq!(result.value["history"], json!({"commits": []})); + for key in ["status", "fetched", "pulled", "pushed", "committed"] { + assert!(result.value[key].is_null(), "{key} was not nil"); + } + } + + #[test] + fn sync_namespace_airplane_gate_prevents_network_commands_and_rejects_blank_commit() { + let fixture = SyncFixture::new(); + git(&fixture.data_dir, &["add", "pending.txt"]); + git(&fixture.data_dir, &["commit", "-m", "Local only"]); + let local_head = git(&fixture.data_dir, &["rev-parse", "HEAD"]); + let tracking_head = git( + &fixture.data_dir, + &["rev-parse", "refs/remotes/origin/master"], + ); + let remote_head = git(&fixture.remote_dir, &["rev-parse", "refs/heads/master"]); + + let result = execute_with_host( + r#" + function main() + return { + fetched = bds.sync.fetch(), + pulled = bds.sync.pull(), + pushed = bds.sync.push(), + committed = bds.sync.commit_all(" "), + } + end + "#, + "main", + &Value::Null, + ExecutionKind::Utility, + &ExecutionControl::default(), + Arc::new(fixture.host(true)), + ) + .unwrap(); + + for key in ["fetched", "pulled", "pushed", "committed"] { + assert!(result.value[key].is_null(), "{key} was not nil"); + } + assert_eq!(git(&fixture.data_dir, &["rev-parse", "HEAD"]), local_head); + assert_eq!( + git( + &fixture.data_dir, + &["rev-parse", "refs/remotes/origin/master"], + ), + tracking_head + ); + assert_eq!( + git(&fixture.remote_dir, &["rev-parse", "refs/heads/master"]), + remote_head + ); + } #[test] fn project_host_round_trips_engines_and_enforces_scope_and_failure_values() { diff --git a/crates/bds-core/src/scripting/manifest.rs b/crates/bds-core/src/scripting/manifest.rs index bcb06cd..b81b292 100644 --- a/crates/bds-core/src/scripting/manifest.rs +++ b/crates/bds-core/src/scripting/manifest.rs @@ -376,6 +376,23 @@ fn field_description(field: &str) -> &'static str { "slug" => "URL-safe record identifier.", "description" => "Human-readable description.", "status" => "Current lifecycle state.", + "kind" => "Public enum value.", + "path" => "Current repository-relative path.", + "old_path" => "Previous repository-relative path for a rename.", + "files" => "Changed repository paths.", + "commits" => "Commit history entries.", + "hash" => "Git object identifier.", + "subject" => "Commit subject.", + "author" => "Commit author.", + "date" => "ISO-8601 commit date.", + "sync_status" => "Local/remote presence classification.", + "current_branch" => "Checked-out branch name.", + "has_lfs" => "Whether Git LFS tracking is configured.", + "is_initialized" => "Whether the project folder is a Git repository.", + "provider" => "Hosting provider inferred from the remote URL.", + "remote_url" => "Configured origin URL.", + "output" => "Git command output.", + "updated" => "Whether the Git network command completed successfully.", "progress" => "Completion value reported by the task.", "message" => "Latest user-facing task message.", "created_at" => "Creation timestamp.", @@ -403,7 +420,6 @@ fn field_description(field: &str) -> &'static str { "file_path" => "Stored media file path.", "alt" => "Alternative text for the media.", "caption" => "Media caption.", - "kind" => "Script or template kind.", "entrypoint" => "Lua function invoked by the runtime.", "color" => "Optional display color.", "post_template_slug" => "Template selected for tagged posts.", diff --git a/crates/bds-core/src/scripting/mod.rs b/crates/bds-core/src/scripting/mod.rs index 7fffbac..3914db2 100644 --- a/crates/bds-core/src/scripting/mod.rs +++ b/crates/bds-core/src/scripting/mod.rs @@ -463,6 +463,7 @@ mod tests { "projects", "publish", "scripts", + "sync", "tags", "tasks", "templates", @@ -473,7 +474,7 @@ mod tests { r#" function main() return { - sync = bds.sync, + sync = type(bds.sync), embeddings = type(bds.embeddings), report_progress = type(bds.report_progress), post_search = type(bds.posts.search), @@ -494,6 +495,7 @@ mod tests { "post_search": "function", "app_toast": "function", "embeddings": "table", + "sync": "table", }) ); } @@ -515,6 +517,7 @@ mod tests { bds.tags.get_all(), bds.tasks.status_snapshot(), bds.publish.upload_site({}), + bds.sync.check_availability(), bds.chat.detect_post_language("title", "body"), bds.embeddings.get_progress(), } @@ -528,8 +531,8 @@ mod tests { ) .unwrap(); - assert_eq!(execution.value.as_array().unwrap().len(), 12); - assert_eq!(host.0.lock().unwrap().len(), 12); + assert_eq!(execution.value.as_array().unwrap().len(), 13); + assert_eq!(host.0.lock().unwrap().len(), 13); } #[test] diff --git a/crates/bds-ui/src/views/documentation.rs b/crates/bds-ui/src/views/documentation.rs index 2abce7d..d80e6f2 100644 --- a/crates/bds-ui/src/views/documentation.rs +++ b/crates/bds-ui/src/views/documentation.rs @@ -352,7 +352,9 @@ fn user_guide_path() -> PathBuf { } fn root_document_path(name: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").join(name) + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(name) } pub fn parse_document(source: &str) -> ParsedDocument { diff --git a/docs/scripting/API_REFERENCE.md b/docs/scripting/API_REFERENCE.md index 79a49c2..e17994b 100644 --- a/docs/scripting/API_REFERENCE.md +++ b/docs/scripting/API_REFERENCE.md @@ -39,6 +39,7 @@ Macro scripts expose `render(input, context)` and transform scripts expose `main - [`bds.projects`](#bdsprojects) - [`bds.publish`](#bdspublish) - [`bds.scripts`](#bdsscripts) +- [`bds.sync`](#bdssync) - [`bds.tags`](#bdstags) - [`bds.tasks`](#bdstasks) - [`bds.templates`](#bdstemplates) @@ -3987,6 +3988,280 @@ local result = bds.scripts.rebuild_from_files() } ``` +## `bds.sync` + +### `bds.sync.check_availability` + +Return whether Git is available on the current machine. + +**Signature** + +```text +bds.sync.check_availability() -> boolean +``` + +**Parameters** + +None. + +**Returns** + +`boolean`. `false` means the operation was rejected or failed. + +**Example call** + +```lua +local result = bds.sync.check_availability() +``` + +**Example response** + +```lua +true +``` + +### `bds.sync.get_repo_state` + +Return repository state for the active project using the GitRepositoryState shape. + +**Signature** + +```text +bds.sync.get_repo_state() -> table | nil +``` + +**Parameters** + +None. + +**Returns** + +`table | nil`. `nil` means no value was available or the host operation failed. + +**Example call** + +```lua +local result = bds.sync.get_repo_state() +``` + +**Example response** + +```lua +{ key = "value" } +``` + +### `bds.sync.get_status` + +Return Git status for the active project using the GitStatusResult shape. + +**Signature** + +```text +bds.sync.get_status() -> table | nil +``` + +**Parameters** + +None. + +**Returns** + +`table | nil`. `nil` means no value was available or the host operation failed. + +**Example call** + +```lua +local result = bds.sync.get_status() +``` + +**Example response** + +```lua +{ key = "value" } +``` + +### `bds.sync.get_history` + +Return commit history for the active project using the GitHistoryResult shape. + +**Signature** + +```text +bds.sync.get_history() -> table | nil +``` + +**Parameters** + +None. + +**Returns** + +`table | nil`. `nil` means no value was available or the host operation failed. + +**Example call** + +```lua +local result = bds.sync.get_history() +``` + +**Example response** + +```lua +{ key = "value" } +``` + +### `bds.sync.get_remote_state` + +Return the GitRepositoryState for the active project, matching the bDS2 remote-state alias. + +**Signature** + +```text +bds.sync.get_remote_state() -> table | nil +``` + +**Parameters** + +None. + +**Returns** + +`table | nil`. `nil` means no value was available or the host operation failed. + +**Example call** + +```lua +local result = bds.sync.get_remote_state() +``` + +**Example response** + +```lua +{ key = "value" } +``` + +### `bds.sync.fetch` + +Fetch and prune remote Git refs for the active project, returning GitNetworkResult; unavailable in airplane mode. + +**Signature** + +```text +bds.sync.fetch() -> table | nil +``` + +**Parameters** + +None. + +**Returns** + +`table | nil`. `nil` means no value was available or the host operation failed. + +**Example call** + +```lua +local result = bds.sync.fetch() +``` + +**Example response** + +```lua +{ key = "value" } +``` + +### `bds.sync.pull` + +Fast-forward pull the active project, reconcile its cache database, and return GitNetworkResult; unavailable in airplane mode. + +**Signature** + +```text +bds.sync.pull() -> table | nil +``` + +**Parameters** + +None. + +**Returns** + +`table | nil`. `nil` means no value was available or the host operation failed. + +**Example call** + +```lua +local result = bds.sync.pull() +``` + +**Example response** + +```lua +{ key = "value" } +``` + +### `bds.sync.push` + +Push the active project to its configured remote and return GitNetworkResult; unavailable in airplane mode. + +**Signature** + +```text +bds.sync.push() -> table | nil +``` + +**Parameters** + +None. + +**Returns** + +`table | nil`. `nil` means no value was available or the host operation failed. + +**Example call** + +```lua +local result = bds.sync.push() +``` + +**Example response** + +```lua +{ key = "value" } +``` + +### `bds.sync.commit_all` + +Stage and commit every pending change in the active project, returning GitCommitResult. + +**Signature** + +```text +bds.sync.commit_all(message: string) -> table | nil +``` + +**Parameters** + +| Name | Type | Required | Example | +| --- | --- | --- | --- | +| `message` | `string` | Yes | `"Working"` | + +**Returns** + +`table | nil`. `nil` means no value was available or the host operation failed. + +**Example call** + +```lua +local result = bds.sync.commit_all("Working") +``` + +**Example response** + +```lua +{ key = "value" } +``` + ## `bds.tags` ### `bds.tags.create` diff --git a/docs/scripting/TYPES.md b/docs/scripting/TYPES.md index c8b7eab..7fd0b39 100644 --- a/docs/scripting/TYPES.md +++ b/docs/scripting/TYPES.md @@ -22,6 +22,15 @@ These are the public, JSON-compatible records returned by the Lua host API. They - [`TagData`](#tagdata) - [`TaskData`](#taskdata) - [`TaskStatus`](#taskstatus) +- [`GitProviderData`](#gitproviderdata) +- [`GitRepositoryState`](#gitrepositorystate) +- [`GitFileStatus`](#gitfilestatus) +- [`GitStatusResult`](#gitstatusresult) +- [`GitSyncStatus`](#gitsyncstatus) +- [`GitCommitData`](#gitcommitdata) +- [`GitHistoryResult`](#githistoryresult) +- [`GitNetworkResult`](#gitnetworkresult) +- [`GitCommitResult`](#gitcommitresult) - [`ValidationResult`](#validationresult) ## `ProjectData` @@ -185,7 +194,7 @@ Lua script record. | `enabled` | `boolean` | Yes | Whether the record is enabled. | | `entrypoint` | `string` | Yes | Lua function invoked by the runtime. | | `id` | `string` | Yes | Stable record identifier. | -| `kind` | `string` | Yes | Script or template kind. | +| `kind` | `string` | Yes | Public enum value. | | `project_id` | `string` | Yes | Identifier of the owning project. | | `slug` | `string` | Yes | URL-safe record identifier. | | `status` | `string` | Yes | Current lifecycle state. | @@ -217,7 +226,7 @@ Template record for site rendering. | `created_at` | `ISO-8601 string` | Yes | Creation timestamp. | | `enabled` | `boolean` | Yes | Whether the record is enabled. | | `id` | `string` | Yes | Stable record identifier. | -| `kind` | `string` | Yes | Script or template kind. | +| `kind` | `string` | Yes | Public enum value. | | `project_id` | `string` | Yes | Identifier of the owning project. | | `slug` | `string` | Yes | URL-safe record identifier. | | `status` | `string` | Yes | Current lifecycle state. | @@ -306,6 +315,194 @@ Aggregate task status snapshot. | `running_count` | `integer` | Yes | Number of currently running tasks. | | `tasks` | `TaskData[]` | Yes | Tasks included in this status snapshot. | +## `GitProviderData` + +Git hosting provider classification used by bDS2 repository state. + +**Lua shape** + +```lua +{ + kind = "utility", +} +``` + +| Field | Type | Required | Meaning | +| --- | --- | --- | --- | +| `kind` | `string` | Yes | Public enum value. | + +## `GitRepositoryState` + +Repository state returned by bds.sync.get_repo_state and get_remote_state. + +**Lua shape** + +```lua +{ + current_branch = "example", + has_lfs = true, + is_initialized = true, + provider = { + kind = "utility", +}, + remote_url = "example", +} +``` + +| Field | Type | Required | Meaning | +| --- | --- | --- | --- | +| `current_branch` | `string \| nil` | No | Checked-out branch name. | +| `has_lfs` | `boolean` | Yes | Whether Git LFS tracking is configured. | +| `is_initialized` | `boolean` | Yes | Whether the project folder is a Git repository. | +| `provider` | `GitProviderData \| nil` | No | Hosting provider inferred from the remote URL. | +| `remote_url` | `string \| nil` | No | Configured origin URL. | + +## `GitFileStatus` + +One changed path in the active repository. + +**Lua shape** + +```lua +{ + old_path = "example", + path = "example", + status = "draft", +} +``` + +| Field | Type | Required | Meaning | +| --- | --- | --- | --- | +| `old_path` | `string \| nil` | No | Previous repository-relative path for a rename. | +| `path` | `string` | Yes | Current repository-relative path. | +| `status` | `string` | Yes | Current lifecycle state. | + +## `GitStatusResult` + +Repository working-tree status returned by bds.sync.get_status. + +**Lua shape** + +```lua +{ + files = { + { + old_path = "example", + path = "example", + status = "draft", + } +}, +} +``` + +| Field | Type | Required | Meaning | +| --- | --- | --- | --- | +| `files` | `GitFileStatus[]` | Yes | Changed repository paths. | + +## `GitSyncStatus` + +Whether a commit exists locally, remotely, or in both histories. + +**Lua shape** + +```lua +{ + kind = "utility", +} +``` + +| Field | Type | Required | Meaning | +| --- | --- | --- | --- | +| `kind` | `string` | Yes | Public enum value. | + +## `GitCommitData` + +Commit entry returned in bds.sync history. + +**Lua shape** + +```lua +{ + author = "example", + date = "2026-07-19T08:00:00Z", + hash = "example", + subject = "example", + sync_status = { + kind = "utility", +}, +} +``` + +| Field | Type | Required | Meaning | +| --- | --- | --- | --- | +| `author` | `string \| nil` | No | Commit author. | +| `date` | `ISO-8601 string \| nil` | No | ISO-8601 commit date. | +| `hash` | `string` | Yes | Git object identifier. | +| `subject` | `string \| nil` | No | Commit subject. | +| `sync_status` | `GitSyncStatus` | Yes | Local/remote presence classification. | + +## `GitHistoryResult` + +Commit history returned by bds.sync.get_history. + +**Lua shape** + +```lua +{ + commits = { + { + author = "example", + date = "2026-07-19T08:00:00Z", + hash = "example", + subject = "example", + sync_status = { + kind = "utility", + }, + } +}, +} +``` + +| Field | Type | Required | Meaning | +| --- | --- | --- | --- | +| `commits` | `GitCommitData[]` | Yes | Commit history entries. | + +## `GitNetworkResult` + +Result of a successful bds.sync network operation. + +**Lua shape** + +```lua +{ + output = "example", + updated = true, +} +``` + +| Field | Type | Required | Meaning | +| --- | --- | --- | --- | +| `output` | `string` | Yes | Git command output. | +| `updated` | `boolean` | Yes | Whether the Git network command completed successfully. | + +## `GitCommitResult` + +Result of successfully committing all pending repository changes. + +**Lua shape** + +```lua +{ + message = "Working", + output = "example", +} +``` + +| Field | Type | Required | Meaning | +| --- | --- | --- | --- | +| `message` | `string` | Yes | Latest user-facing task message. | +| `output` | `string` | Yes | Git command output. | + ## `ValidationResult` Template validation result. diff --git a/docs/scripting/api.json b/docs/scripting/api.json index e2ce122..598cfec 100644 --- a/docs/scripting/api.json +++ b/docs/scripting/api.json @@ -1428,6 +1428,75 @@ "params": [], "returns": "boolean" }, + { + "description": "Return whether Git is available on the current machine.", + "name": "check_availability", + "namespace": "sync", + "params": [], + "returns": "boolean" + }, + { + "description": "Return repository state for the active project using the GitRepositoryState shape.", + "name": "get_repo_state", + "namespace": "sync", + "params": [], + "returns": "table | nil" + }, + { + "description": "Return Git status for the active project using the GitStatusResult shape.", + "name": "get_status", + "namespace": "sync", + "params": [], + "returns": "table | nil" + }, + { + "description": "Return commit history for the active project using the GitHistoryResult shape.", + "name": "get_history", + "namespace": "sync", + "params": [], + "returns": "table | nil" + }, + { + "description": "Return the GitRepositoryState for the active project, matching the bDS2 remote-state alias.", + "name": "get_remote_state", + "namespace": "sync", + "params": [], + "returns": "table | nil" + }, + { + "description": "Fetch and prune remote Git refs for the active project, returning GitNetworkResult; unavailable in airplane mode.", + "name": "fetch", + "namespace": "sync", + "params": [], + "returns": "table | nil" + }, + { + "description": "Fast-forward pull the active project, reconcile its cache database, and return GitNetworkResult; unavailable in airplane mode.", + "name": "pull", + "namespace": "sync", + "params": [], + "returns": "table | nil" + }, + { + "description": "Push the active project to its configured remote and return GitNetworkResult; unavailable in airplane mode.", + "name": "push", + "namespace": "sync", + "params": [], + "returns": "table | nil" + }, + { + "description": "Stage and commit every pending change in the active project, returning GitCommitResult.", + "name": "commit_all", + "namespace": "sync", + "params": [ + { + "name": "message", + "required": true, + "type": "string" + } + ], + "returns": "table | nil" + }, { "description": "Upload the rendered site using the provided publishing credentials.", "name": "upload_site", @@ -1782,6 +1851,81 @@ }, "name": "TaskStatus" }, + { + "description": "Git hosting provider classification used by bDS2 repository state.", + "fields": { + "kind": "string" + }, + "name": "GitProviderData" + }, + { + "description": "Repository state returned by bds.sync.get_repo_state and get_remote_state.", + "fields": { + "current_branch": "string | nil", + "has_lfs": "boolean", + "is_initialized": "boolean", + "provider": "GitProviderData | nil", + "remote_url": "string | nil" + }, + "name": "GitRepositoryState" + }, + { + "description": "One changed path in the active repository.", + "fields": { + "old_path": "string | nil", + "path": "string", + "status": "string" + }, + "name": "GitFileStatus" + }, + { + "description": "Repository working-tree status returned by bds.sync.get_status.", + "fields": { + "files": "GitFileStatus[]" + }, + "name": "GitStatusResult" + }, + { + "description": "Whether a commit exists locally, remotely, or in both histories.", + "fields": { + "kind": "string" + }, + "name": "GitSyncStatus" + }, + { + "description": "Commit entry returned in bds.sync history.", + "fields": { + "author": "string | nil", + "date": "ISO-8601 string | nil", + "hash": "string", + "subject": "string | nil", + "sync_status": "GitSyncStatus" + }, + "name": "GitCommitData" + }, + { + "description": "Commit history returned by bds.sync.get_history.", + "fields": { + "commits": "GitCommitData[]" + }, + "name": "GitHistoryResult" + }, + { + "description": "Result of a successful bds.sync network operation.", + "fields": { + "output": "string", + "updated": "boolean" + }, + "name": "GitNetworkResult" + }, + { + "description": "Result of successfully committing all pending repository changes.", + "fields": { + "message": "string", + "output": "string" + }, + "name": "GitCommitResult" + }, { "description": "Template validation result.", "fields": { diff --git a/docs/scripting/bds2-core-signatures.json b/docs/scripting/bds2-core-signatures.json index 4c3d3b3..dae7f1d 100644 --- a/docs/scripting/bds2-core-signatures.json +++ b/docs/scripting/bds2-core-signatures.json @@ -1 +1 @@ -{"version":"0.4.0","methods":[{"namespace":"app","name":"copy_to_clipboard","params":[{"name":"text","type":"string","required":true}],"returns":"boolean"},{"namespace":"app","name":"get_blogmark_bookmarklet","params":[],"returns":"string"},{"namespace":"app","name":"get_data_paths","params":[],"returns":"table"},{"namespace":"app","name":"get_default_project_path","params":[],"returns":"string | nil"},{"namespace":"app","name":"get_system_language","params":[],"returns":"string | nil"},{"namespace":"app","name":"get_title_bar_metrics","params":[],"returns":"table | nil"},{"namespace":"app","name":"log","params":[{"name":"text","type":"string","required":true}],"returns":"boolean"},{"namespace":"app","name":"notify_renderer_ready","params":[],"returns":"boolean"},{"namespace":"app","name":"open_folder","params":[{"name":"folder_path","type":"string","required":true}],"returns":"string"},{"namespace":"app","name":"read_project_metadata","params":[{"name":"folder_path","type":"string","required":true}],"returns":"ProjectMetadata | nil"},{"namespace":"app","name":"select_folder","params":[{"name":"title","type":"string","required":false}],"returns":"string | nil"},{"namespace":"app","name":"set_preview_post_target","params":[{"name":"post_id","type":"string","required":false}],"returns":"boolean"},{"namespace":"app","name":"show_item_in_folder","params":[{"name":"item_path","type":"string","required":true}],"returns":"nil"},{"namespace":"app","name":"trigger_menu_action","params":[{"name":"action","type":"string","required":true}],"returns":"nil"},{"namespace":"projects","name":"create","params":[{"name":"data","type":"table","required":true}],"returns":"ProjectData | nil"},{"namespace":"projects","name":"delete","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"projects","name":"delete_with_data","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"projects","name":"get","params":[{"name":"id","type":"string","required":true}],"returns":"ProjectData | nil"},{"namespace":"projects","name":"get_all","params":[],"returns":"ProjectData[]"},{"namespace":"projects","name":"get_active","params":[],"returns":"ProjectData | nil"},{"namespace":"projects","name":"set_active","params":[{"name":"id","type":"string","required":true}],"returns":"ProjectData | nil"},{"namespace":"projects","name":"update","params":[{"name":"id","type":"string","required":true},{"name":"data","type":"table","required":true}],"returns":"ProjectData | nil"},{"namespace":"posts","name":"create","params":[{"name":"data","type":"table","required":true}],"returns":"PostData | nil"},{"namespace":"posts","name":"update","params":[{"name":"id","type":"string","required":true},{"name":"data","type":"table","required":true}],"returns":"PostData | nil"},{"namespace":"posts","name":"delete","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"posts","name":"discard","params":[{"name":"id","type":"string","required":true}],"returns":"PostData | nil"},{"namespace":"posts","name":"filter","params":[{"name":"filters","type":"table","required":true}],"returns":"PostData[] | nil"},{"namespace":"posts","name":"generate_unique_slug","params":[{"name":"title","type":"string","required":true},{"name":"exclude_post_id","type":"string","required":false}],"returns":"string"},{"namespace":"posts","name":"get","params":[{"name":"id","type":"string","required":true}],"returns":"PostData | nil"},{"namespace":"posts","name":"get_all","params":[],"returns":"PostData[]"},{"namespace":"posts","name":"get_by_slug","params":[{"name":"slug","type":"string","required":true}],"returns":"PostData | nil"},{"namespace":"posts","name":"get_by_status","params":[{"name":"status","type":"string","required":true}],"returns":"PostData[]"},{"namespace":"posts","name":"get_by_year_month","params":[],"returns":"table[]"},{"namespace":"posts","name":"get_categories","params":[],"returns":"string[]"},{"namespace":"posts","name":"get_categories_with_counts","params":[],"returns":"table[]"},{"namespace":"posts","name":"get_dashboard_stats","params":[],"returns":"table"},{"namespace":"posts","name":"get_linked_by","params":[{"name":"post_id","type":"string","required":true}],"returns":"table[]"},{"namespace":"posts","name":"get_links_to","params":[{"name":"post_id","type":"string","required":true}],"returns":"table[]"},{"namespace":"posts","name":"get_preview_url","params":[{"name":"post_id","type":"string","required":true},{"name":"options","type":"table","required":false}],"returns":"string | nil"},{"namespace":"posts","name":"get_tags","params":[],"returns":"string[]"},{"namespace":"posts","name":"get_tags_with_counts","params":[],"returns":"table[]"},{"namespace":"posts","name":"get_translation","params":[{"name":"post_id","type":"string","required":true},{"name":"language","type":"string","required":true}],"returns":"table | nil"},{"namespace":"posts","name":"get_translations","params":[{"name":"post_id","type":"string","required":true}],"returns":"table[]"},{"namespace":"posts","name":"has_published_version","params":[{"name":"post_id","type":"string","required":true}],"returns":"boolean"},{"namespace":"posts","name":"is_slug_available","params":[{"name":"slug","type":"string","required":true},{"name":"exclude_post_id","type":"string","required":false}],"returns":"boolean"},{"namespace":"posts","name":"publish","params":[{"name":"id","type":"string","required":true}],"returns":"PostData | nil"},{"namespace":"posts","name":"publish_translation","params":[{"name":"post_id","type":"string","required":true},{"name":"language","type":"string","required":true}],"returns":"table | nil"},{"namespace":"posts","name":"rebuild_from_files","params":[],"returns":"PostData[] | nil"},{"namespace":"posts","name":"rebuild_links","params":[],"returns":"boolean"},{"namespace":"posts","name":"reindex_text","params":[],"returns":"boolean"},{"namespace":"posts","name":"search","params":[{"name":"query","type":"string","required":true}],"returns":"PostData[] | nil"},{"namespace":"media","name":"delete_translation","params":[{"name":"media_id","type":"string","required":true},{"name":"language","type":"string","required":true}],"returns":"boolean"},{"namespace":"media","name":"filter","params":[{"name":"filters","type":"table","required":true}],"returns":"MediaData[]"},{"namespace":"media","name":"import","params":[{"name":"data","type":"table","required":true}],"returns":"MediaData | nil"},{"namespace":"media","name":"get_by_year_month","params":[],"returns":"table[]"},{"namespace":"media","name":"get_file_path","params":[{"name":"media_id","type":"string","required":true}],"returns":"string | nil"},{"namespace":"media","name":"update","params":[{"name":"id","type":"string","required":true},{"name":"data","type":"table","required":true}],"returns":"MediaData | nil"},{"namespace":"media","name":"delete","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"media","name":"get","params":[{"name":"id","type":"string","required":true}],"returns":"MediaData | nil"},{"namespace":"media","name":"get_all","params":[],"returns":"MediaData[]"},{"namespace":"media","name":"get_tags","params":[],"returns":"string[]"},{"namespace":"media","name":"get_tags_with_counts","params":[],"returns":"table[]"},{"namespace":"media","name":"get_thumbnail","params":[{"name":"media_id","type":"string","required":true},{"name":"size","type":"string","required":false}],"returns":"string | nil"},{"namespace":"media","name":"get_translation","params":[{"name":"media_id","type":"string","required":true},{"name":"language","type":"string","required":true}],"returns":"table | nil"},{"namespace":"media","name":"get_translations","params":[{"name":"media_id","type":"string","required":true}],"returns":"table[]"},{"namespace":"media","name":"get_url","params":[{"name":"media_id","type":"string","required":true}],"returns":"string | nil"},{"namespace":"media","name":"rebuild_from_files","params":[],"returns":"MediaData[] | nil"},{"namespace":"media","name":"regenerate_missing_thumbnails","params":[],"returns":"table"},{"namespace":"media","name":"regenerate_thumbnails","params":[{"name":"media_id","type":"string","required":true}],"returns":"table | nil"},{"namespace":"media","name":"reindex_text","params":[],"returns":"boolean"},{"namespace":"media","name":"replace_file","params":[{"name":"media_id","type":"string","required":true},{"name":"source_path","type":"string","required":true}],"returns":"MediaData | nil"},{"namespace":"media","name":"search","params":[{"name":"query","type":"string","required":true}],"returns":"MediaData[] | nil"},{"namespace":"media","name":"upsert_translation","params":[{"name":"media_id","type":"string","required":true},{"name":"language","type":"string","required":true},{"name":"data","type":"table","required":true}],"returns":"table | nil"},{"namespace":"scripts","name":"create","params":[{"name":"data","type":"table","required":true}],"returns":"ScriptData | nil"},{"namespace":"scripts","name":"update","params":[{"name":"id","type":"string","required":true},{"name":"data","type":"table","required":true}],"returns":"ScriptData | nil"},{"namespace":"scripts","name":"delete","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"scripts","name":"get","params":[{"name":"id","type":"string","required":true}],"returns":"ScriptData | nil"},{"namespace":"scripts","name":"get_all","params":[],"returns":"ScriptData[]"},{"namespace":"scripts","name":"publish","params":[{"name":"id","type":"string","required":true}],"returns":"ScriptData | nil"},{"namespace":"scripts","name":"rebuild_from_files","params":[],"returns":"ScriptData[] | nil"},{"namespace":"templates","name":"create","params":[{"name":"data","type":"table","required":true}],"returns":"TemplateData | nil"},{"namespace":"templates","name":"update","params":[{"name":"id","type":"string","required":true},{"name":"data","type":"table","required":true}],"returns":"TemplateData | nil"},{"namespace":"templates","name":"delete","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"templates","name":"get","params":[{"name":"id","type":"string","required":true}],"returns":"TemplateData | nil"},{"namespace":"templates","name":"get_all","params":[],"returns":"TemplateData[]"},{"namespace":"templates","name":"get_enabled_by_kind","params":[{"name":"kind","type":"string","required":true}],"returns":"TemplateData[]"},{"namespace":"templates","name":"publish","params":[{"name":"id","type":"string","required":true}],"returns":"TemplateData | nil"},{"namespace":"templates","name":"rebuild_from_files","params":[],"returns":"TemplateData[] | nil"},{"namespace":"templates","name":"validate","params":[{"name":"content","type":"string","required":true}],"returns":"ValidationResult | nil"},{"namespace":"meta","name":"add_category","params":[{"name":"name","type":"string","required":true}],"returns":"ProjectMetadata | nil"},{"namespace":"meta","name":"add_tag","params":[{"name":"name","type":"string","required":true}],"returns":"string[]"},{"namespace":"meta","name":"clear_publishing_preferences","params":[],"returns":"table | nil"},{"namespace":"meta","name":"get_categories","params":[],"returns":"string[]"},{"namespace":"meta","name":"get_project_metadata","params":[],"returns":"ProjectMetadata"},{"namespace":"meta","name":"get_publishing_preferences","params":[],"returns":"table | nil"},{"namespace":"meta","name":"get_tags","params":[],"returns":"string[]"},{"namespace":"meta","name":"remove_category","params":[{"name":"name","type":"string","required":true}],"returns":"ProjectMetadata | nil"},{"namespace":"meta","name":"remove_tag","params":[{"name":"name","type":"string","required":true}],"returns":"string[]"},{"namespace":"meta","name":"set_project_metadata","params":[{"name":"updates","type":"table","required":true}],"returns":"ProjectMetadata | nil"},{"namespace":"meta","name":"set_publishing_preferences","params":[{"name":"prefs","type":"table","required":true}],"returns":"table | nil"},{"namespace":"meta","name":"sync_on_startup","params":[],"returns":"table"},{"namespace":"meta","name":"update_project_metadata","params":[{"name":"updates","type":"table","required":true}],"returns":"ProjectMetadata | nil"},{"namespace":"tags","name":"create","params":[{"name":"data","type":"table","required":true}],"returns":"TagData | nil"},{"namespace":"tags","name":"update","params":[{"name":"id","type":"string","required":true},{"name":"data","type":"table","required":true}],"returns":"TagData | nil"},{"namespace":"tags","name":"delete","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"tags","name":"get","params":[{"name":"id","type":"string","required":true}],"returns":"TagData | nil"},{"namespace":"tags","name":"get_all","params":[],"returns":"TagData[]"},{"namespace":"tags","name":"get_by_name","params":[{"name":"name","type":"string","required":true}],"returns":"TagData | nil"},{"namespace":"tags","name":"get_posts_with_tag","params":[{"name":"tag_id","type":"string","required":true}],"returns":"string[]"},{"namespace":"tags","name":"get_with_counts","params":[],"returns":"table[]"},{"namespace":"tags","name":"merge","params":[{"name":"source_tag_ids","type":"table","required":true},{"name":"target_tag_id","type":"string","required":true}],"returns":"boolean"},{"namespace":"tags","name":"rename","params":[{"name":"id","type":"string","required":true},{"name":"new_name","type":"string","required":true}],"returns":"TagData | nil"},{"namespace":"tags","name":"sync_from_posts","params":[],"returns":"TagData[] | nil"},{"namespace":"tasks","name":"cancel","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"tasks","name":"clear_completed","params":[],"returns":"boolean"},{"namespace":"tasks","name":"get","params":[{"name":"id","type":"string","required":true}],"returns":"TaskData | nil"},{"namespace":"tasks","name":"get_all","params":[],"returns":"TaskData[]"},{"namespace":"tasks","name":"get_running","params":[],"returns":"TaskData[]"},{"namespace":"tasks","name":"status_snapshot","params":[],"returns":"TaskStatus"},{"namespace":"publish","name":"upload_site","params":[{"name":"credentials","type":"table","required":true}],"returns":"TaskData | nil"},{"namespace":"chat","name":"analyze_media_image","params":[{"name":"media_id","type":"string","required":true}],"returns":"table | nil"},{"namespace":"chat","name":"analyze_post","params":[{"name":"post_id","type":"string","required":true}],"returns":"table | nil"},{"namespace":"chat","name":"detect_media_language","params":[{"name":"title","type":"string","required":true},{"name":"alt","type":"string","required":false},{"name":"caption","type":"string","required":false}],"returns":"table"},{"namespace":"chat","name":"detect_post_language","params":[{"name":"title","type":"string","required":true},{"name":"content","type":"string","required":true}],"returns":"table"},{"namespace":"chat","name":"translate_media_metadata","params":[{"name":"media_id","type":"string","required":true},{"name":"language","type":"string","required":true}],"returns":"table | nil"},{"namespace":"chat","name":"translate_post","params":[{"name":"post_id","type":"string","required":true},{"name":"language","type":"string","required":true}],"returns":"table | nil"},{"namespace":"embeddings","name":"compute_similarities","params":[{"name":"post_id","type":"string","required":true},{"name":"target_ids","type":"table","required":true}],"returns":"table | nil"},{"namespace":"embeddings","name":"dismiss_pair","params":[{"name":"post_id_a","type":"string","required":true},{"name":"post_id_b","type":"string","required":true}],"returns":"boolean"},{"namespace":"embeddings","name":"find_duplicates","params":[],"returns":"table | nil"},{"namespace":"embeddings","name":"find_similar","params":[{"name":"post_id","type":"string","required":true},{"name":"limit","type":"integer","required":false}],"returns":"table | nil"},{"namespace":"embeddings","name":"get_progress","params":[],"returns":"table | nil"},{"namespace":"embeddings","name":"index_unindexed_posts","params":[],"returns":"table | nil"},{"namespace":"embeddings","name":"suggest_tags","params":[{"name":"post_id","type":"string","required":true},{"name":"exclude_tags","type":"table","required":false}],"returns":"table | nil"}]} +{"version":"0.4.0","methods":[{"namespace":"app","name":"copy_to_clipboard","params":[{"name":"text","type":"string","required":true}],"returns":"boolean"},{"namespace":"app","name":"get_blogmark_bookmarklet","params":[],"returns":"string"},{"namespace":"app","name":"get_data_paths","params":[],"returns":"table"},{"namespace":"app","name":"get_default_project_path","params":[],"returns":"string | nil"},{"namespace":"app","name":"get_system_language","params":[],"returns":"string | nil"},{"namespace":"app","name":"get_title_bar_metrics","params":[],"returns":"table | nil"},{"namespace":"app","name":"log","params":[{"name":"text","type":"string","required":true}],"returns":"boolean"},{"namespace":"app","name":"notify_renderer_ready","params":[],"returns":"boolean"},{"namespace":"app","name":"open_folder","params":[{"name":"folder_path","type":"string","required":true}],"returns":"string"},{"namespace":"app","name":"read_project_metadata","params":[{"name":"folder_path","type":"string","required":true}],"returns":"ProjectMetadata | nil"},{"namespace":"app","name":"select_folder","params":[{"name":"title","type":"string","required":false}],"returns":"string | nil"},{"namespace":"app","name":"set_preview_post_target","params":[{"name":"post_id","type":"string","required":false}],"returns":"boolean"},{"namespace":"app","name":"show_item_in_folder","params":[{"name":"item_path","type":"string","required":true}],"returns":"nil"},{"namespace":"app","name":"trigger_menu_action","params":[{"name":"action","type":"string","required":true}],"returns":"nil"},{"namespace":"projects","name":"create","params":[{"name":"data","type":"table","required":true}],"returns":"ProjectData | nil"},{"namespace":"projects","name":"delete","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"projects","name":"delete_with_data","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"projects","name":"get","params":[{"name":"id","type":"string","required":true}],"returns":"ProjectData | nil"},{"namespace":"projects","name":"get_all","params":[],"returns":"ProjectData[]"},{"namespace":"projects","name":"get_active","params":[],"returns":"ProjectData | nil"},{"namespace":"projects","name":"set_active","params":[{"name":"id","type":"string","required":true}],"returns":"ProjectData | nil"},{"namespace":"projects","name":"update","params":[{"name":"id","type":"string","required":true},{"name":"data","type":"table","required":true}],"returns":"ProjectData | nil"},{"namespace":"posts","name":"create","params":[{"name":"data","type":"table","required":true}],"returns":"PostData | nil"},{"namespace":"posts","name":"update","params":[{"name":"id","type":"string","required":true},{"name":"data","type":"table","required":true}],"returns":"PostData | nil"},{"namespace":"posts","name":"delete","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"posts","name":"discard","params":[{"name":"id","type":"string","required":true}],"returns":"PostData | nil"},{"namespace":"posts","name":"filter","params":[{"name":"filters","type":"table","required":true}],"returns":"PostData[] | nil"},{"namespace":"posts","name":"generate_unique_slug","params":[{"name":"title","type":"string","required":true},{"name":"exclude_post_id","type":"string","required":false}],"returns":"string"},{"namespace":"posts","name":"get","params":[{"name":"id","type":"string","required":true}],"returns":"PostData | nil"},{"namespace":"posts","name":"get_all","params":[],"returns":"PostData[]"},{"namespace":"posts","name":"get_by_slug","params":[{"name":"slug","type":"string","required":true}],"returns":"PostData | nil"},{"namespace":"posts","name":"get_by_status","params":[{"name":"status","type":"string","required":true}],"returns":"PostData[]"},{"namespace":"posts","name":"get_by_year_month","params":[],"returns":"table[]"},{"namespace":"posts","name":"get_categories","params":[],"returns":"string[]"},{"namespace":"posts","name":"get_categories_with_counts","params":[],"returns":"table[]"},{"namespace":"posts","name":"get_dashboard_stats","params":[],"returns":"table"},{"namespace":"posts","name":"get_linked_by","params":[{"name":"post_id","type":"string","required":true}],"returns":"table[]"},{"namespace":"posts","name":"get_links_to","params":[{"name":"post_id","type":"string","required":true}],"returns":"table[]"},{"namespace":"posts","name":"get_preview_url","params":[{"name":"post_id","type":"string","required":true},{"name":"options","type":"table","required":false}],"returns":"string | nil"},{"namespace":"posts","name":"get_tags","params":[],"returns":"string[]"},{"namespace":"posts","name":"get_tags_with_counts","params":[],"returns":"table[]"},{"namespace":"posts","name":"get_translation","params":[{"name":"post_id","type":"string","required":true},{"name":"language","type":"string","required":true}],"returns":"table | nil"},{"namespace":"posts","name":"get_translations","params":[{"name":"post_id","type":"string","required":true}],"returns":"table[]"},{"namespace":"posts","name":"has_published_version","params":[{"name":"post_id","type":"string","required":true}],"returns":"boolean"},{"namespace":"posts","name":"is_slug_available","params":[{"name":"slug","type":"string","required":true},{"name":"exclude_post_id","type":"string","required":false}],"returns":"boolean"},{"namespace":"posts","name":"publish","params":[{"name":"id","type":"string","required":true}],"returns":"PostData | nil"},{"namespace":"posts","name":"publish_translation","params":[{"name":"post_id","type":"string","required":true},{"name":"language","type":"string","required":true}],"returns":"table | nil"},{"namespace":"posts","name":"rebuild_from_files","params":[],"returns":"PostData[] | nil"},{"namespace":"posts","name":"rebuild_links","params":[],"returns":"boolean"},{"namespace":"posts","name":"reindex_text","params":[],"returns":"boolean"},{"namespace":"posts","name":"search","params":[{"name":"query","type":"string","required":true}],"returns":"PostData[] | nil"},{"namespace":"media","name":"delete_translation","params":[{"name":"media_id","type":"string","required":true},{"name":"language","type":"string","required":true}],"returns":"boolean"},{"namespace":"media","name":"filter","params":[{"name":"filters","type":"table","required":true}],"returns":"MediaData[]"},{"namespace":"media","name":"import","params":[{"name":"data","type":"table","required":true}],"returns":"MediaData | nil"},{"namespace":"media","name":"get_by_year_month","params":[],"returns":"table[]"},{"namespace":"media","name":"get_file_path","params":[{"name":"media_id","type":"string","required":true}],"returns":"string | nil"},{"namespace":"media","name":"update","params":[{"name":"id","type":"string","required":true},{"name":"data","type":"table","required":true}],"returns":"MediaData | nil"},{"namespace":"media","name":"delete","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"media","name":"get","params":[{"name":"id","type":"string","required":true}],"returns":"MediaData | nil"},{"namespace":"media","name":"get_all","params":[],"returns":"MediaData[]"},{"namespace":"media","name":"get_tags","params":[],"returns":"string[]"},{"namespace":"media","name":"get_tags_with_counts","params":[],"returns":"table[]"},{"namespace":"media","name":"get_thumbnail","params":[{"name":"media_id","type":"string","required":true},{"name":"size","type":"string","required":false}],"returns":"string | nil"},{"namespace":"media","name":"get_translation","params":[{"name":"media_id","type":"string","required":true},{"name":"language","type":"string","required":true}],"returns":"table | nil"},{"namespace":"media","name":"get_translations","params":[{"name":"media_id","type":"string","required":true}],"returns":"table[]"},{"namespace":"media","name":"get_url","params":[{"name":"media_id","type":"string","required":true}],"returns":"string | nil"},{"namespace":"media","name":"rebuild_from_files","params":[],"returns":"MediaData[] | nil"},{"namespace":"media","name":"regenerate_missing_thumbnails","params":[],"returns":"table"},{"namespace":"media","name":"regenerate_thumbnails","params":[{"name":"media_id","type":"string","required":true}],"returns":"table | nil"},{"namespace":"media","name":"reindex_text","params":[],"returns":"boolean"},{"namespace":"media","name":"replace_file","params":[{"name":"media_id","type":"string","required":true},{"name":"source_path","type":"string","required":true}],"returns":"MediaData | nil"},{"namespace":"media","name":"search","params":[{"name":"query","type":"string","required":true}],"returns":"MediaData[] | nil"},{"namespace":"media","name":"upsert_translation","params":[{"name":"media_id","type":"string","required":true},{"name":"language","type":"string","required":true},{"name":"data","type":"table","required":true}],"returns":"table | nil"},{"namespace":"scripts","name":"create","params":[{"name":"data","type":"table","required":true}],"returns":"ScriptData | nil"},{"namespace":"scripts","name":"update","params":[{"name":"id","type":"string","required":true},{"name":"data","type":"table","required":true}],"returns":"ScriptData | nil"},{"namespace":"scripts","name":"delete","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"scripts","name":"get","params":[{"name":"id","type":"string","required":true}],"returns":"ScriptData | nil"},{"namespace":"scripts","name":"get_all","params":[],"returns":"ScriptData[]"},{"namespace":"scripts","name":"publish","params":[{"name":"id","type":"string","required":true}],"returns":"ScriptData | nil"},{"namespace":"scripts","name":"rebuild_from_files","params":[],"returns":"ScriptData[] | nil"},{"namespace":"templates","name":"create","params":[{"name":"data","type":"table","required":true}],"returns":"TemplateData | nil"},{"namespace":"templates","name":"update","params":[{"name":"id","type":"string","required":true},{"name":"data","type":"table","required":true}],"returns":"TemplateData | nil"},{"namespace":"templates","name":"delete","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"templates","name":"get","params":[{"name":"id","type":"string","required":true}],"returns":"TemplateData | nil"},{"namespace":"templates","name":"get_all","params":[],"returns":"TemplateData[]"},{"namespace":"templates","name":"get_enabled_by_kind","params":[{"name":"kind","type":"string","required":true}],"returns":"TemplateData[]"},{"namespace":"templates","name":"publish","params":[{"name":"id","type":"string","required":true}],"returns":"TemplateData | nil"},{"namespace":"templates","name":"rebuild_from_files","params":[],"returns":"TemplateData[] | nil"},{"namespace":"templates","name":"validate","params":[{"name":"content","type":"string","required":true}],"returns":"ValidationResult | nil"},{"namespace":"meta","name":"add_category","params":[{"name":"name","type":"string","required":true}],"returns":"ProjectMetadata | nil"},{"namespace":"meta","name":"add_tag","params":[{"name":"name","type":"string","required":true}],"returns":"string[]"},{"namespace":"meta","name":"clear_publishing_preferences","params":[],"returns":"table | nil"},{"namespace":"meta","name":"get_categories","params":[],"returns":"string[]"},{"namespace":"meta","name":"get_project_metadata","params":[],"returns":"ProjectMetadata"},{"namespace":"meta","name":"get_publishing_preferences","params":[],"returns":"table | nil"},{"namespace":"meta","name":"get_tags","params":[],"returns":"string[]"},{"namespace":"meta","name":"remove_category","params":[{"name":"name","type":"string","required":true}],"returns":"ProjectMetadata | nil"},{"namespace":"meta","name":"remove_tag","params":[{"name":"name","type":"string","required":true}],"returns":"string[]"},{"namespace":"meta","name":"set_project_metadata","params":[{"name":"updates","type":"table","required":true}],"returns":"ProjectMetadata | nil"},{"namespace":"meta","name":"set_publishing_preferences","params":[{"name":"prefs","type":"table","required":true}],"returns":"table | nil"},{"namespace":"meta","name":"sync_on_startup","params":[],"returns":"table"},{"namespace":"meta","name":"update_project_metadata","params":[{"name":"updates","type":"table","required":true}],"returns":"ProjectMetadata | nil"},{"namespace":"tags","name":"create","params":[{"name":"data","type":"table","required":true}],"returns":"TagData | nil"},{"namespace":"tags","name":"update","params":[{"name":"id","type":"string","required":true},{"name":"data","type":"table","required":true}],"returns":"TagData | nil"},{"namespace":"tags","name":"delete","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"tags","name":"get","params":[{"name":"id","type":"string","required":true}],"returns":"TagData | nil"},{"namespace":"tags","name":"get_all","params":[],"returns":"TagData[]"},{"namespace":"tags","name":"get_by_name","params":[{"name":"name","type":"string","required":true}],"returns":"TagData | nil"},{"namespace":"tags","name":"get_posts_with_tag","params":[{"name":"tag_id","type":"string","required":true}],"returns":"string[]"},{"namespace":"tags","name":"get_with_counts","params":[],"returns":"table[]"},{"namespace":"tags","name":"merge","params":[{"name":"source_tag_ids","type":"table","required":true},{"name":"target_tag_id","type":"string","required":true}],"returns":"boolean"},{"namespace":"tags","name":"rename","params":[{"name":"id","type":"string","required":true},{"name":"new_name","type":"string","required":true}],"returns":"TagData | nil"},{"namespace":"tags","name":"sync_from_posts","params":[],"returns":"TagData[] | nil"},{"namespace":"tasks","name":"cancel","params":[{"name":"id","type":"string","required":true}],"returns":"boolean"},{"namespace":"tasks","name":"clear_completed","params":[],"returns":"boolean"},{"namespace":"tasks","name":"get","params":[{"name":"id","type":"string","required":true}],"returns":"TaskData | nil"},{"namespace":"tasks","name":"get_all","params":[],"returns":"TaskData[]"},{"namespace":"tasks","name":"get_running","params":[],"returns":"TaskData[]"},{"namespace":"tasks","name":"status_snapshot","params":[],"returns":"TaskStatus"},{"namespace":"publish","name":"upload_site","params":[{"name":"credentials","type":"table","required":true}],"returns":"TaskData | nil"},{"namespace":"chat","name":"analyze_media_image","params":[{"name":"media_id","type":"string","required":true}],"returns":"table | nil"},{"namespace":"chat","name":"analyze_post","params":[{"name":"post_id","type":"string","required":true}],"returns":"table | nil"},{"namespace":"chat","name":"detect_media_language","params":[{"name":"title","type":"string","required":true},{"name":"alt","type":"string","required":false},{"name":"caption","type":"string","required":false}],"returns":"table"},{"namespace":"chat","name":"detect_post_language","params":[{"name":"title","type":"string","required":true},{"name":"content","type":"string","required":true}],"returns":"table"},{"namespace":"chat","name":"translate_media_metadata","params":[{"name":"media_id","type":"string","required":true},{"name":"language","type":"string","required":true}],"returns":"table | nil"},{"namespace":"chat","name":"translate_post","params":[{"name":"post_id","type":"string","required":true},{"name":"language","type":"string","required":true}],"returns":"table | nil"},{"namespace":"embeddings","name":"compute_similarities","params":[{"name":"post_id","type":"string","required":true},{"name":"target_ids","type":"table","required":true}],"returns":"table | nil"},{"namespace":"embeddings","name":"dismiss_pair","params":[{"name":"post_id_a","type":"string","required":true},{"name":"post_id_b","type":"string","required":true}],"returns":"boolean"},{"namespace":"embeddings","name":"find_duplicates","params":[],"returns":"table | nil"},{"namespace":"embeddings","name":"find_similar","params":[{"name":"post_id","type":"string","required":true},{"name":"limit","type":"integer","required":false}],"returns":"table | nil"},{"namespace":"embeddings","name":"get_progress","params":[],"returns":"table | nil"},{"namespace":"embeddings","name":"index_unindexed_posts","params":[],"returns":"table | nil"},{"namespace":"embeddings","name":"suggest_tags","params":[{"name":"post_id","type":"string","required":true},{"name":"exclude_tags","type":"table","required":false}],"returns":"table | nil"},{"namespace":"sync","name":"check_availability","params":[],"returns":"boolean"},{"namespace":"sync","name":"commit_all","params":[{"name":"message","type":"string","required":true}],"returns":"table | nil"},{"namespace":"sync","name":"fetch","params":[],"returns":"table | nil"},{"namespace":"sync","name":"get_history","params":[],"returns":"table | nil"},{"namespace":"sync","name":"get_remote_state","params":[],"returns":"table | nil"},{"namespace":"sync","name":"get_repo_state","params":[],"returns":"table | nil"},{"namespace":"sync","name":"get_status","params":[],"returns":"table | nil"},{"namespace":"sync","name":"pull","params":[],"returns":"table | nil"},{"namespace":"sync","name":"push","params":[],"returns":"table | nil"}]} diff --git a/docs/scripting/completions.json b/docs/scripting/completions.json index 050e512..97d4fda 100644 --- a/docs/scripting/completions.json +++ b/docs/scripting/completions.json @@ -1546,6 +1546,84 @@ "parameters": [], "returns": "boolean" }, + { + "description": "Return whether Git is available on the current machine.", + "detail": "bds.sync.check_availability() -> boolean", + "insert_text": "bds.sync.check_availability()", + "label": "bds.sync.check_availability", + "parameters": [], + "returns": "boolean" + }, + { + "description": "Return repository state for the active project using the GitRepositoryState shape.", + "detail": "bds.sync.get_repo_state() -> table | nil", + "insert_text": "bds.sync.get_repo_state()", + "label": "bds.sync.get_repo_state", + "parameters": [], + "returns": "table | nil" + }, + { + "description": "Return Git status for the active project using the GitStatusResult shape.", + "detail": "bds.sync.get_status() -> table | nil", + "insert_text": "bds.sync.get_status()", + "label": "bds.sync.get_status", + "parameters": [], + "returns": "table | nil" + }, + { + "description": "Return commit history for the active project using the GitHistoryResult shape.", + "detail": "bds.sync.get_history() -> table | nil", + "insert_text": "bds.sync.get_history()", + "label": "bds.sync.get_history", + "parameters": [], + "returns": "table | nil" + }, + { + "description": "Return the GitRepositoryState for the active project, matching the bDS2 remote-state alias.", + "detail": "bds.sync.get_remote_state() -> table | nil", + "insert_text": "bds.sync.get_remote_state()", + "label": "bds.sync.get_remote_state", + "parameters": [], + "returns": "table | nil" + }, + { + "description": "Fetch and prune remote Git refs for the active project, returning GitNetworkResult; unavailable in airplane mode.", + "detail": "bds.sync.fetch() -> table | nil", + "insert_text": "bds.sync.fetch()", + "label": "bds.sync.fetch", + "parameters": [], + "returns": "table | nil" + }, + { + "description": "Fast-forward pull the active project, reconcile its cache database, and return GitNetworkResult; unavailable in airplane mode.", + "detail": "bds.sync.pull() -> table | nil", + "insert_text": "bds.sync.pull()", + "label": "bds.sync.pull", + "parameters": [], + "returns": "table | nil" + }, + { + "description": "Push the active project to its configured remote and return GitNetworkResult; unavailable in airplane mode.", + "detail": "bds.sync.push() -> table | nil", + "insert_text": "bds.sync.push()", + "label": "bds.sync.push", + "parameters": [], + "returns": "table | nil" + }, + { + "description": "Stage and commit every pending change in the active project, returning GitCommitResult.", + "detail": "bds.sync.commit_all(message: string) -> table | nil", + "insert_text": "bds.sync.commit_all(\"Working\")", + "label": "bds.sync.commit_all", + "parameters": [ + { + "name": "message", + "required": true, + "type": "string" + } + ], + "returns": "table | nil" + }, { "description": "Upload the rendered site using the provided publishing credentials.", "detail": "bds.publish.upload_site(credentials: table) -> TaskData | nil", diff --git a/specs/script.allium b/specs/script.allium index 31aefb7..ddca144 100644 --- a/specs/script.allium +++ b/specs/script.allium @@ -135,6 +135,8 @@ surface ScriptRuntimeSurface { -- sync_from_posts. -- bds.tasks: get, status_snapshot, cancel, get_all, get_running, -- clear_completed. + -- bds.sync: check_availability, get_repo_state, get_status, + -- get_history, get_remote_state, fetch, pull, push, commit_all. -- bds.publish: upload_site. -- bds.chat: detect_post_language, analyze_post, translate_post, -- analyze_media_image, detect_media_language, @@ -144,7 +146,6 @@ surface ScriptRuntimeSurface { -- host may retain bds.app.progress and bds.app.toast as additive APIs. -- bds.embeddings: compute_similarities, dismiss_pair, find_duplicates, -- find_similar, get_progress, index_unindexed_posts, suggest_tags. - -- bds.sync remains a future extension contract. @guarantee HostApiFailureValues -- Host failures do not expose implementation exceptions across the Lua