Implement the bDS2 sync scripting API.

This commit is contained in:
2026-07-20 15:53:31 +02:00
parent b10212d47e
commit 49447ef451
14 changed files with 1140 additions and 51 deletions

View File

@@ -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.

View File

@@ -335,47 +335,20 @@ fn active_project(db: &Database) -> Result<(Project, PathBuf)> {
fn rebuild(db: &Database, incremental: bool) -> Result<CommandOutput> {
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::<Vec<_>>();
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,
}),
));
}

View File

@@ -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;

View File

@@ -29,6 +29,51 @@ pub struct FullRebuildReport {
pub errors: Vec<String>,
}
#[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<IncrementalRebuildReport> {
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<dyn Fn(f32, &str) + Send + Sync>;

View File

@@ -1239,6 +1239,64 @@ impl CoreHost {
Ok(Value::Bool(true))
}
fn sync(&self, method: &str, args: &[Value]) -> HostResult<Value> {
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::<Vec<_>>(),
})),
"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::<Vec<_>>(),
}))
}
"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<Value> {
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<TaskSnapshot>) -> HostResult<Value> {
.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<Value> {
let metadata = engine::meta::read_project_json(data_dir)?;
Ok(json!({
@@ -1862,6 +1969,255 @@ fn one_shot_json(value: engine::ai::OneShotResponse) -> HostResult<Value> {
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() {

View File

@@ -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.",

View File

@@ -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]

View File

@@ -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 {

View File

@@ -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`

View File

@@ -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.

View File

@@ -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": {

File diff suppressed because one or more lines are too long

View File

@@ -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",

View File

@@ -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