Skip to main content

caboodle/
skills.rs

1//! Explicit local skill publication/materialization. Never edits harness settings.
2//! Catalogue files are caller-supplied inputs, not authenticated server evidence.
3mod native;
4mod publish;
5mod sync;
6
7use anyhow::{Context, Result};
8use clap::Subcommand;
9use serde::de::DeserializeOwned;
10use std::{
11    fs,
12    path::{Path, PathBuf},
13};
14
15#[derive(Subcommand)]
16pub enum Command {
17    /// Build a native pendant and release metadata from one explicit source tree
18    Publish {
19        #[arg(long)]
20        source: PathBuf,
21        #[arg(long)]
22        metadata: PathBuf,
23        #[arg(long)]
24        render_shapes: PathBuf,
25        #[arg(long, requires = "identifier_shapes")]
26        identifier_policy: Option<PathBuf>,
27        #[arg(long, requires = "identifier_policy")]
28        identifier_shapes: Option<PathBuf>,
29        /// A NEW output directory; existing releases are never replaced
30        #[arg(long)]
31        output: PathBuf,
32        #[arg(long, default_value = "quipu")]
33        quipu: PathBuf,
34    },
35    /// Render a complete pinned profile into ONE owned staging directory
36    Sync {
37        #[arg(long)]
38        profile: PathBuf,
39        #[arg(long)]
40        catalogue: PathBuf,
41        /// Explicit local map: native share ID -> archive path (never URLs)
42        #[arg(long)]
43        bundles: PathBuf,
44        /// Caller-selected native shape authority, not authority from a skill
45        #[arg(long)]
46        authority: PathBuf,
47        #[arg(long)]
48        render_shapes: PathBuf,
49        /// Owned profile tree, NOT a shared harness root or settings file
50        #[arg(long)]
51        dest: PathBuf,
52        #[arg(long, default_value = "quipu")]
53        quipu: PathBuf,
54    },
55}
56
57pub fn run(command: Command) -> Result<()> {
58    let result = match command {
59        Command::Publish {
60            source,
61            metadata,
62            render_shapes,
63            identifier_policy,
64            identifier_shapes,
65            output,
66            quipu,
67        } => publish::publish(
68            &source,
69            &metadata,
70            &render_shapes,
71            identifier_policy
72                .as_deref()
73                .zip(identifier_shapes.as_deref()),
74            &output,
75            &quipu,
76        )?,
77        Command::Sync {
78            profile,
79            catalogue,
80            bundles,
81            authority,
82            render_shapes,
83            dest,
84            quipu,
85        } => sync::sync(
86            &profile,
87            &catalogue,
88            &bundles,
89            &authority,
90            &render_shapes,
91            &dest,
92            &quipu,
93        )?,
94    };
95    println!("{}", serde_json::to_string_pretty(&result)?);
96    Ok(())
97}
98
99fn read<T: DeserializeOwned>(path: &Path) -> Result<T> {
100    let bytes = native::regular_bytes(path, 4 * 1024 * 1024)?;
101    serde_json::from_slice(&bytes)
102        .with_context(|| format!("invalid skill input: {}", path.display()))
103}
104
105fn write_json(path: &Path, value: &impl serde::Serialize) -> Result<()> {
106    fs::write(path, serde_json::to_vec_pretty(value)?)?;
107    Ok(())
108}
109
110#[cfg(test)]
111mod tests;