Marketplace: data model, settings and project fields
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::gateway_settings::GatewaySettings;
|
||||
use super::marketplace::{Marketplace, MarketplaceAccount, MarketplaceInstall};
|
||||
use super::project::{ClaudeCodeSettings, EnvVar};
|
||||
|
||||
fn default_true() -> bool {
|
||||
@@ -135,6 +136,16 @@ pub struct AppSettings {
|
||||
pub gateway: GatewaySettings,
|
||||
#[serde(default)]
|
||||
pub global_claude_code_settings: Option<ClaudeCodeSettings>,
|
||||
/// Sign-in accounts for private marketplace repos. Secrets live in the
|
||||
/// OS keychain (`storage::secure::*_marketplace_token`), never here.
|
||||
#[serde(default)]
|
||||
pub marketplace_accounts: Vec<MarketplaceAccount>,
|
||||
/// Marketplace git repos the user added.
|
||||
#[serde(default)]
|
||||
pub marketplaces: Vec<Marketplace>,
|
||||
/// Items installed for every project (projects may opt out per item).
|
||||
#[serde(default)]
|
||||
pub global_marketplace_installs: Vec<MarketplaceInstall>,
|
||||
/// Whether the terminal loads `@xterm/addon-webgl`.
|
||||
///
|
||||
/// `None` is "auto", and auto is not the same answer on every platform.
|
||||
@@ -246,6 +257,9 @@ impl Default for AppSettings {
|
||||
stt: SttSettings::default(),
|
||||
gateway: GatewaySettings::default(),
|
||||
global_claude_code_settings: None,
|
||||
marketplace_accounts: Vec::new(),
|
||||
marketplaces: Vec::new(),
|
||||
global_marketplace_installs: Vec::new(),
|
||||
terminal_gpu_rendering: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
//! Marketplace data model — see `docs/superpowers/specs/2026-09-27-marketplace-design.md`.
|
||||
//!
|
||||
//! Plain data plus the pure rules that decide what a project actually gets
|
||||
//! ([`effective_installs`]) and what names are allowed to reach a container
|
||||
//! path ([`is_valid_item_key`], [`marketplace_slug`]).
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ItemKind {
|
||||
Agent,
|
||||
Skill,
|
||||
Command,
|
||||
Hook,
|
||||
Plugin,
|
||||
}
|
||||
|
||||
impl ItemKind {
|
||||
/// The lowercase name used in report strings (`"agent:code-reviewer"`) and the manifest.
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ItemKind::Agent => "agent",
|
||||
ItemKind::Skill => "skill",
|
||||
ItemKind::Command => "command",
|
||||
ItemKind::Hook => "hook",
|
||||
ItemKind::Plugin => "plugin",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AccountMethod {
|
||||
GhHost,
|
||||
GhContainer,
|
||||
Token,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MarketplaceAccount {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub host: String,
|
||||
pub method: AccountMethod,
|
||||
#[serde(default)]
|
||||
pub username: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Marketplace {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
#[serde(default)]
|
||||
pub branch: Option<String>,
|
||||
#[serde(default)]
|
||||
pub account_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct MarketplaceItemRef {
|
||||
pub marketplace_id: String,
|
||||
pub kind: ItemKind,
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MarketplaceInstall {
|
||||
pub marketplace_id: String,
|
||||
pub kind: ItemKind,
|
||||
pub key: String,
|
||||
pub commit: String,
|
||||
}
|
||||
|
||||
impl MarketplaceInstall {
|
||||
pub fn item_ref(&self) -> MarketplaceItemRef {
|
||||
MarketplaceItemRef {
|
||||
marketplace_id: self.marketplace_id.clone(),
|
||||
kind: self.kind,
|
||||
key: self.key.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a project's container actually gets: the global installs minus the
|
||||
/// ones this project opted out of, plus the project's own installs. When the
|
||||
/// project installs an item that is also global, the project's entry (and so
|
||||
/// its pin) wins. Sorted by item ref so the result is deterministic.
|
||||
pub fn effective_installs(
|
||||
global: &[MarketplaceInstall],
|
||||
disabled: &[MarketplaceItemRef],
|
||||
project: &[MarketplaceInstall],
|
||||
) -> Vec<MarketplaceInstall> {
|
||||
let mut out: BTreeMap<MarketplaceItemRef, MarketplaceInstall> = BTreeMap::new();
|
||||
for install in global {
|
||||
let item = install.item_ref();
|
||||
if disabled.contains(&item) {
|
||||
continue;
|
||||
}
|
||||
out.insert(item, install.clone());
|
||||
}
|
||||
for install in project {
|
||||
out.insert(install.item_ref(), install.clone());
|
||||
}
|
||||
out.into_values().collect()
|
||||
}
|
||||
|
||||
/// `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` — the only names that may become a
|
||||
/// container path component. No `/`, no leading `.` or `-`, no shell
|
||||
/// metacharacters.
|
||||
pub fn is_valid_item_key(key: &str) -> bool {
|
||||
let bytes = key.as_bytes();
|
||||
if bytes.is_empty() || bytes.len() > 64 {
|
||||
return false;
|
||||
}
|
||||
if !bytes[0].is_ascii_alphanumeric() {
|
||||
return false;
|
||||
}
|
||||
bytes
|
||||
.iter()
|
||||
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
|
||||
}
|
||||
|
||||
/// A container-safe, collision-free name for a marketplace: its name
|
||||
/// lowercased to `[a-z0-9-]`, dashes collapsed, at most 32 characters, then
|
||||
/// `-` and the first 8 characters of its id. An empty sanitised name becomes
|
||||
/// `marketplace`.
|
||||
pub fn marketplace_slug(name: &str, id: &str) -> String {
|
||||
let mut base = String::new();
|
||||
for c in name.chars() {
|
||||
let c = c.to_ascii_lowercase();
|
||||
if c.is_ascii_lowercase() || c.is_ascii_digit() {
|
||||
base.push(c);
|
||||
} else if !base.ends_with('-') && !base.is_empty() {
|
||||
base.push('-');
|
||||
}
|
||||
}
|
||||
let mut base: String = base.trim_matches('-').chars().take(32).collect();
|
||||
while base.ends_with('-') {
|
||||
base.pop();
|
||||
}
|
||||
if base.is_empty() {
|
||||
base.push_str("marketplace");
|
||||
}
|
||||
let id_part: String = id
|
||||
.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric())
|
||||
.map(|c| c.to_ascii_lowercase())
|
||||
.take(8)
|
||||
.collect();
|
||||
format!("{}-{}", base, id_part)
|
||||
}
|
||||
|
||||
/// A full, lowercase, 40-character hex object id.
|
||||
pub fn is_valid_commit(commit: &str) -> bool {
|
||||
commit.len() == 40
|
||||
&& commit
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CatalogItem {
|
||||
pub kind: ItemKind,
|
||||
pub key: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
/// Repo-relative path of the item (file or folder).
|
||||
pub path: String,
|
||||
/// `Some(reason)` when the item cannot be installed.
|
||||
pub invalid: Option<String>,
|
||||
/// Hooks only: rendered commands with `${HOOK_DIR}` substituted.
|
||||
#[serde(default)]
|
||||
pub hook_commands: Vec<String>,
|
||||
/// Agents/commands/skills: the markdown body (≤ 64 KiB, truncated);
|
||||
/// plugins: a component listing.
|
||||
#[serde(default)]
|
||||
pub preview: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct MarketplaceSnapshot {
|
||||
pub marketplace_id: String,
|
||||
pub head_commit: Option<String>,
|
||||
/// RFC 3339.
|
||||
pub fetched_at: Option<String>,
|
||||
pub fetch_error: Option<String>,
|
||||
pub items: Vec<CatalogItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ItemUpdate {
|
||||
pub item: MarketplaceItemRef,
|
||||
pub pinned: String,
|
||||
pub head: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FileChange {
|
||||
Added,
|
||||
Removed,
|
||||
Modified,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct FileDiff {
|
||||
pub path: String,
|
||||
pub change: FileChange,
|
||||
/// Unified diff text; `None` when either side is binary.
|
||||
pub unified: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct SkippedItem {
|
||||
pub item: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct SyncReport {
|
||||
#[serde(default)]
|
||||
pub installed: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub updated: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub removed: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub skipped: Vec<SkippedItem>,
|
||||
#[serde(default)]
|
||||
pub errors: Vec<String>,
|
||||
/// RFC 3339, set by the host.
|
||||
#[serde(default)]
|
||||
pub finished_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum InstallScope {
|
||||
Global,
|
||||
Project { project_id: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProjectSyncResult {
|
||||
pub project_id: String,
|
||||
pub report: SyncReport,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn install(market: &str, kind: ItemKind, key: &str, commit: &str) -> MarketplaceInstall {
|
||||
MarketplaceInstall {
|
||||
marketplace_id: market.to_string(),
|
||||
kind,
|
||||
key: key.to_string(),
|
||||
commit: commit.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_set_is_global_minus_disabled_plus_project() {
|
||||
let global = vec![
|
||||
install("m1", ItemKind::Agent, "reviewer", "a"),
|
||||
install("m1", ItemKind::Hook, "notify", "a"),
|
||||
];
|
||||
let disabled = vec![MarketplaceItemRef {
|
||||
marketplace_id: "m1".into(),
|
||||
kind: ItemKind::Hook,
|
||||
key: "notify".into(),
|
||||
}];
|
||||
let project = vec![install("m2", ItemKind::Skill, "tidy", "b")];
|
||||
|
||||
let got = effective_installs(&global, &disabled, &project);
|
||||
|
||||
assert_eq!(
|
||||
got,
|
||||
vec![
|
||||
install("m1", ItemKind::Agent, "reviewer", "a"),
|
||||
install("m2", ItemKind::Skill, "tidy", "b"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_pin_wins_over_global_pin() {
|
||||
let global = vec![install("m1", ItemKind::Agent, "reviewer", "old")];
|
||||
let project = vec![install("m1", ItemKind::Agent, "reviewer", "new")];
|
||||
let got = effective_installs(&global, &[], &project);
|
||||
assert_eq!(got, vec![install("m1", ItemKind::Agent, "reviewer", "new")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_key_different_kind_are_different_items() {
|
||||
let global = vec![
|
||||
install("m1", ItemKind::Agent, "x", "a"),
|
||||
install("m1", ItemKind::Command, "x", "a"),
|
||||
];
|
||||
assert_eq!(effective_installs(&global, &[], &[]).len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item_keys_follow_the_pattern() {
|
||||
for ok in ["a", "code-reviewer", "A.b_c-9", &"x".repeat(64)] {
|
||||
assert!(is_valid_item_key(ok), "{ok} should be valid");
|
||||
}
|
||||
for bad in [
|
||||
"",
|
||||
".hidden",
|
||||
"-flag",
|
||||
"_x",
|
||||
"a/b",
|
||||
"a b",
|
||||
"a;rm",
|
||||
"$(x)",
|
||||
"ä",
|
||||
"..",
|
||||
&"x".repeat(65),
|
||||
] {
|
||||
assert!(!is_valid_item_key(bad), "{bad:?} should be invalid");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slug_is_sanitised_and_suffixed_with_the_id() {
|
||||
assert_eq!(
|
||||
marketplace_slug("Triple-C Marketplace!", "1A2B3C4D-ffff"),
|
||||
"triple-c-marketplace-1a2b3c4d"
|
||||
);
|
||||
assert_eq!(
|
||||
marketplace_slug("***", "abcdef0123"),
|
||||
"marketplace-abcdef01"
|
||||
);
|
||||
let long = marketplace_slug(&"x".repeat(80), "12345678");
|
||||
assert_eq!(long, format!("{}-12345678", "x".repeat(32)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commits_must_be_full_lowercase_hex() {
|
||||
assert!(is_valid_commit(&"a".repeat(40)));
|
||||
assert!(!is_valid_commit(&"A".repeat(40)));
|
||||
assert!(!is_valid_commit(&"a".repeat(39)));
|
||||
assert!(!is_valid_commit("HEAD"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_scope_serialises_tagged() {
|
||||
assert_eq!(
|
||||
serde_json::to_value(InstallScope::Global).unwrap(),
|
||||
serde_json::json!({"type": "global"})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(InstallScope::Project {
|
||||
project_id: "p".into()
|
||||
})
|
||||
.unwrap(),
|
||||
serde_json::json!({"type": "project", "project_id": "p"})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kinds_serialise_snake_case() {
|
||||
assert_eq!(serde_json::to_value(ItemKind::Plugin).unwrap(), "plugin");
|
||||
assert_eq!(
|
||||
serde_json::to_value(AccountMethod::GhHost).unwrap(),
|
||||
"gh_host"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_and_projects_saved_before_the_marketplace_still_load() {
|
||||
let mut settings = serde_json::to_value(crate::models::AppSettings::default()).unwrap();
|
||||
for key in [
|
||||
"marketplace_accounts",
|
||||
"marketplaces",
|
||||
"global_marketplace_installs",
|
||||
] {
|
||||
settings.as_object_mut().unwrap().remove(key);
|
||||
}
|
||||
let settings: crate::models::AppSettings = serde_json::from_value(settings).unwrap();
|
||||
assert!(settings.marketplace_accounts.is_empty());
|
||||
assert!(settings.marketplaces.is_empty());
|
||||
assert!(settings.global_marketplace_installs.is_empty());
|
||||
|
||||
let mut project =
|
||||
serde_json::to_value(crate::models::Project::new("p".to_string(), Vec::new())).unwrap();
|
||||
for key in ["marketplace_installs", "marketplace_disabled"] {
|
||||
project.as_object_mut().unwrap().remove(key);
|
||||
}
|
||||
let project: crate::models::Project = serde_json::from_value(project).unwrap();
|
||||
assert!(project.marketplace_installs.is_empty());
|
||||
assert!(project.marketplace_disabled.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod app_settings;
|
||||
pub mod container_config;
|
||||
pub mod gateway_settings;
|
||||
pub mod marketplace;
|
||||
pub mod migration;
|
||||
pub mod note;
|
||||
pub mod project;
|
||||
@@ -10,6 +11,7 @@ pub mod update_info;
|
||||
pub use app_settings::*;
|
||||
pub use container_config::*;
|
||||
pub use gateway_settings::*;
|
||||
pub use marketplace::*;
|
||||
pub use migration::*;
|
||||
pub use note::*;
|
||||
pub use project::*;
|
||||
|
||||
@@ -446,6 +446,12 @@ pub struct Project {
|
||||
/// User-defined display names for terminal tabs, keyed by session id.
|
||||
#[serde(default)]
|
||||
pub renamed_session_names: HashMap<String, String>,
|
||||
/// Marketplace items installed for this project only (spec §2).
|
||||
#[serde(default)]
|
||||
pub marketplace_installs: Vec<super::marketplace::MarketplaceInstall>,
|
||||
/// Global marketplace installs this project opts out of.
|
||||
#[serde(default)]
|
||||
pub marketplace_disabled: Vec<super::marketplace::MarketplaceItemRef>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
@@ -693,6 +699,8 @@ impl Project {
|
||||
claude_instructions: None,
|
||||
claude_code_settings: None,
|
||||
renamed_session_names: HashMap::new(),
|
||||
marketplace_installs: Vec::new(),
|
||||
marketplace_disabled: Vec::new(),
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
}
|
||||
@@ -789,7 +797,10 @@ mod tests {
|
||||
}
|
||||
|
||||
fn env(key: &str, value: &str) -> EnvVar {
|
||||
EnvVar { key: key.to_string(), value: value.to_string() }
|
||||
EnvVar {
|
||||
key: key.to_string(),
|
||||
value: value.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -887,7 +898,10 @@ mod tests {
|
||||
// `merge_claude_code_settings` spells it. `main` resolved this with
|
||||
// `if p.env_scrub { true } else { g.env_scrub }`, i.e. the global won —
|
||||
// and it has to go on winning, because the user never turned this off.
|
||||
let global = ClaudeCodeSettings { env_scrub: Some(true), ..Default::default() };
|
||||
let global = ClaudeCodeSettings {
|
||||
env_scrub: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
stored.env_scrub.or(global.env_scrub),
|
||||
Some(true),
|
||||
@@ -902,7 +916,10 @@ mod tests {
|
||||
let json = r#"{ "env_scrub": false }"#;
|
||||
let chosen: ClaudeCodeSettings = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(chosen.env_scrub, Some(false));
|
||||
let global = ClaudeCodeSettings { env_scrub: Some(true), ..Default::default() };
|
||||
let global = ClaudeCodeSettings {
|
||||
env_scrub: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(chosen.env_scrub.or(global.env_scrub), Some(false));
|
||||
}
|
||||
|
||||
@@ -916,7 +933,10 @@ mod tests {
|
||||
assert_eq!(json, "{}");
|
||||
assert!(!json.contains("null"));
|
||||
|
||||
let partial = ClaudeCodeSettings { env_scrub: Some(false), ..Default::default() };
|
||||
let partial = ClaudeCodeSettings {
|
||||
env_scrub: Some(false),
|
||||
..Default::default()
|
||||
};
|
||||
let json = serde_json::to_string(&partial).unwrap();
|
||||
assert_eq!(json, r#"{"env_scrub":false}"#);
|
||||
// And it reads back as what it is.
|
||||
@@ -984,7 +1004,10 @@ mod tests {
|
||||
});
|
||||
let migrated = Project::migrate_from_value(legacy);
|
||||
let obj = migrated.as_object().unwrap();
|
||||
assert!(obj.contains_key("paths"), "the migration should still do its own job");
|
||||
assert!(
|
||||
obj.contains_key("paths"),
|
||||
"the migration should still do its own job"
|
||||
);
|
||||
assert!(!obj.contains_key("auth_bridge_enabled"));
|
||||
assert!(!obj.contains_key("browser_view_enabled"));
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ const baseProject: Project = {
|
||||
claude_instructions: null,
|
||||
claude_code_settings: null,
|
||||
renamed_session_names: {},
|
||||
marketplace_installs: [],
|
||||
marketplace_disabled: [],
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
@@ -60,6 +60,8 @@ const baseProject: Project = {
|
||||
claude_instructions: null,
|
||||
claude_code_settings: null,
|
||||
renamed_session_names: {},
|
||||
marketplace_installs: [],
|
||||
marketplace_disabled: [],
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
@@ -125,6 +125,8 @@ const project: Project = {
|
||||
claude_instructions: null,
|
||||
claude_code_settings: null,
|
||||
renamed_session_names: {},
|
||||
marketplace_installs: [],
|
||||
marketplace_disabled: [],
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
} as unknown as Project;
|
||||
|
||||
@@ -44,6 +44,8 @@ const baseProject: Project = {
|
||||
claude_instructions: null,
|
||||
claude_code_settings: null,
|
||||
renamed_session_names: {},
|
||||
marketplace_installs: [],
|
||||
marketplace_disabled: [],
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
@@ -34,6 +34,8 @@ const baseProject: Project = {
|
||||
claude_instructions: null,
|
||||
claude_code_settings: null,
|
||||
renamed_session_names: {},
|
||||
marketplace_installs: [],
|
||||
marketplace_disabled: [],
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
@@ -47,6 +47,8 @@ const baseProject: Project = {
|
||||
claude_instructions: null,
|
||||
claude_code_settings: null,
|
||||
renamed_session_names: {},
|
||||
marketplace_installs: [],
|
||||
marketplace_disabled: [],
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
@@ -39,6 +39,8 @@ const baseProject: Project = {
|
||||
claude_instructions: null,
|
||||
claude_code_settings: null,
|
||||
renamed_session_names: {},
|
||||
marketplace_installs: [],
|
||||
marketplace_disabled: [],
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
@@ -72,6 +72,8 @@ const baseProject: Project = {
|
||||
claude_instructions: null,
|
||||
claude_code_settings: null,
|
||||
renamed_session_names: {},
|
||||
marketplace_installs: [],
|
||||
marketplace_disabled: [],
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
@@ -66,6 +66,8 @@ export interface Project {
|
||||
claude_instructions: string | null;
|
||||
claude_code_settings: ClaudeCodeSettings | null;
|
||||
renamed_session_names: Record<string, string>;
|
||||
marketplace_installs: MarketplaceInstall[];
|
||||
marketplace_disabled: MarketplaceItemRef[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -296,6 +298,81 @@ export interface AppSettings {
|
||||
* canvas renderer it would otherwise fall back to. See
|
||||
* `resolveTerminalGpuRendering` in `lib/terminalRenderer.ts`. */
|
||||
terminal_gpu_rendering: boolean | null;
|
||||
marketplace_accounts: MarketplaceAccount[];
|
||||
marketplaces: Marketplace[];
|
||||
global_marketplace_installs: MarketplaceInstall[];
|
||||
}
|
||||
|
||||
// ── Marketplace (mirrors src-tauri/src/models/marketplace.rs) ───────────────
|
||||
|
||||
export type ItemKind = "agent" | "skill" | "command" | "hook" | "plugin";
|
||||
export type AccountMethod = "gh_host" | "gh_container" | "token";
|
||||
export interface MarketplaceAccount {
|
||||
id: string;
|
||||
label: string;
|
||||
host: string;
|
||||
method: AccountMethod;
|
||||
username: string | null;
|
||||
}
|
||||
export interface Marketplace {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
branch: string | null;
|
||||
account_id: string | null;
|
||||
}
|
||||
export interface MarketplaceItemRef {
|
||||
marketplace_id: string;
|
||||
kind: ItemKind;
|
||||
key: string;
|
||||
}
|
||||
export interface MarketplaceInstall extends MarketplaceItemRef {
|
||||
commit: string;
|
||||
}
|
||||
export interface CatalogItem {
|
||||
kind: ItemKind;
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
path: string;
|
||||
invalid: string | null;
|
||||
hook_commands: string[];
|
||||
preview: string;
|
||||
}
|
||||
export interface MarketplaceSnapshot {
|
||||
marketplace_id: string;
|
||||
head_commit: string | null;
|
||||
fetched_at: string | null;
|
||||
fetch_error: string | null;
|
||||
items: CatalogItem[];
|
||||
}
|
||||
export interface ItemUpdate {
|
||||
item: MarketplaceItemRef;
|
||||
pinned: string;
|
||||
head: string;
|
||||
}
|
||||
export type FileChange = "added" | "removed" | "modified";
|
||||
export interface FileDiff {
|
||||
path: string;
|
||||
change: FileChange;
|
||||
unified: string | null;
|
||||
}
|
||||
export interface SkippedItem {
|
||||
item: string;
|
||||
reason: string;
|
||||
}
|
||||
export interface SyncReport {
|
||||
installed: string[];
|
||||
updated: string[];
|
||||
removed: string[];
|
||||
skipped: SkippedItem[];
|
||||
errors: string[];
|
||||
finished_at: string;
|
||||
}
|
||||
export type InstallScope = { type: "global" } | { type: "project"; project_id: string };
|
||||
export interface ProjectSyncResult {
|
||||
project_id: string;
|
||||
report: SyncReport;
|
||||
}
|
||||
|
||||
/** What `preview_settings_import` returns before anything is applied —
|
||||
|
||||
Reference in New Issue
Block a user