Triple-C marketplace: agents, skills, commands, hooks and plugins for all or individual containers (#65)
Build App / compute-version (push) Successful in 4s
Secret Scan / scan (push) Successful in 4s
Build App / build-macos (push) Successful in 3m55s
Build App / build-windows (push) Successful in 7m11s
Build App / build-linux (push) Successful in 9m53s
Build App / create-tag (push) Successful in 5s
Build App / sync-to-github (push) Successful in 56s
Build App / compute-version (push) Successful in 4s
Secret Scan / scan (push) Successful in 4s
Build App / build-macos (push) Successful in 3m55s
Build App / build-windows (push) Successful in 7m11s
Build App / build-linux (push) Successful in 9m53s
Build App / create-tag (push) Successful in 5s
Build App / sync-to-github (push) Successful in 56s
This commit was merged in pull request #65.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
effectiveInstalls,
|
||||
formatItemRef,
|
||||
isStale,
|
||||
itemRefKey,
|
||||
projectItemState,
|
||||
STALE_AFTER_MS,
|
||||
} from "./marketplace";
|
||||
import type { MarketplaceInstall, MarketplaceSnapshot, Project } from "./types";
|
||||
|
||||
const A = "a".repeat(40);
|
||||
const B = "b".repeat(40);
|
||||
|
||||
const inst = (key: string, commit = A, kind: MarketplaceInstall["kind"] = "agent"): MarketplaceInstall => ({
|
||||
marketplace_id: "m1",
|
||||
kind,
|
||||
key,
|
||||
commit,
|
||||
});
|
||||
|
||||
const project = (patch: Partial<Project> = {}): Project =>
|
||||
({
|
||||
id: "p1",
|
||||
name: "api",
|
||||
marketplace_installs: [],
|
||||
marketplace_disabled: [],
|
||||
...patch,
|
||||
}) as unknown as Project;
|
||||
|
||||
describe("itemRefKey / formatItemRef", () => {
|
||||
it("keys and formats a ref", () => {
|
||||
const r = { marketplace_id: "m1", kind: "hook" as const, key: "notify" };
|
||||
expect(itemRefKey(r)).toBe("m1/hook/notify");
|
||||
expect(formatItemRef(r)).toBe("hook:notify");
|
||||
});
|
||||
});
|
||||
|
||||
describe("projectItemState", () => {
|
||||
const ref = { marketplace_id: "m1", kind: "agent" as const, key: "rev" };
|
||||
|
||||
it("is none when nothing installs it", () => {
|
||||
expect(projectItemState(ref, [], project())).toBe("none");
|
||||
});
|
||||
|
||||
it("is inherited from a global install", () => {
|
||||
expect(projectItemState(ref, [inst("rev")], project())).toBe("inherited");
|
||||
});
|
||||
|
||||
it("is opted_out when the project disabled the global install", () => {
|
||||
const p = project({ marketplace_disabled: [ref] });
|
||||
expect(projectItemState(ref, [inst("rev")], p)).toBe("opted_out");
|
||||
});
|
||||
|
||||
it("is project for a project-only install", () => {
|
||||
const p = project({ marketplace_installs: [inst("rev")] });
|
||||
expect(projectItemState(ref, [], p)).toBe("project");
|
||||
});
|
||||
|
||||
it("is project when project and global share the pin", () => {
|
||||
const p = project({ marketplace_installs: [inst("rev", A)] });
|
||||
expect(projectItemState(ref, [inst("rev", A)], p)).toBe("project");
|
||||
});
|
||||
|
||||
it("flags a project pin that differs from the global pin", () => {
|
||||
const p = project({ marketplace_installs: [inst("rev", B)] });
|
||||
expect(projectItemState(ref, [inst("rev", A)], p)).toBe("project_pinned_differently");
|
||||
});
|
||||
|
||||
it("does not confuse kinds with the same key", () => {
|
||||
expect(projectItemState(ref, [inst("rev", A, "skill")], project())).toBe("none");
|
||||
});
|
||||
});
|
||||
|
||||
describe("effectiveInstalls", () => {
|
||||
it("merges global minus disabled plus project, project winning", () => {
|
||||
const disabledRef = { marketplace_id: "m1", kind: "agent" as const, key: "off" };
|
||||
const p = project({
|
||||
marketplace_disabled: [disabledRef],
|
||||
marketplace_installs: [inst("both", B), inst("mine")],
|
||||
});
|
||||
const out = effectiveInstalls([inst("glob"), inst("off"), inst("both", A)], p);
|
||||
expect(out.map((i) => [i.key, i.commit, i.source])).toEqual([
|
||||
["both", B, "project"],
|
||||
["glob", A, "global"],
|
||||
["mine", A, "project"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isStale", () => {
|
||||
const snap = (fetched_at: string | null): MarketplaceSnapshot => ({
|
||||
marketplace_id: "m1",
|
||||
head_commit: null,
|
||||
fetched_at,
|
||||
fetch_error: null,
|
||||
items: [],
|
||||
});
|
||||
const now = Date.parse("2026-09-27T12:00:00Z");
|
||||
|
||||
it("treats a never-fetched snapshot as stale", () => {
|
||||
expect(isStale(snap(null), now)).toBe(true);
|
||||
});
|
||||
|
||||
it("is fresh within 15 minutes and stale after", () => {
|
||||
expect(isStale(snap(new Date(now - STALE_AFTER_MS + 1000).toISOString()), now)).toBe(false);
|
||||
expect(isStale(snap(new Date(now - STALE_AFTER_MS - 1000).toISOString()), now)).toBe(true);
|
||||
});
|
||||
|
||||
it("treats an unparsable timestamp as stale", () => {
|
||||
expect(isStale(snap("not a date"), now)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import type {
|
||||
ItemKind,
|
||||
ItemUpdate,
|
||||
MarketplaceInstall,
|
||||
MarketplaceItemRef,
|
||||
MarketplaceSnapshot,
|
||||
Project,
|
||||
} from "./types";
|
||||
|
||||
/** How a project relates to one marketplace item. */
|
||||
export type ProjectItemState =
|
||||
| "none"
|
||||
| "inherited"
|
||||
| "opted_out"
|
||||
| "project"
|
||||
| "project_pinned_differently";
|
||||
|
||||
export const KIND_ORDER: ItemKind[] = ["agent", "skill", "command", "hook", "plugin"];
|
||||
|
||||
export const KIND_LABELS: Record<ItemKind, string> = {
|
||||
agent: "Agents",
|
||||
skill: "Skills",
|
||||
command: "Commands",
|
||||
hook: "Hooks",
|
||||
plugin: "Plugins",
|
||||
};
|
||||
|
||||
/** A marketplace is refreshed when its tab opens if the last fetch is older than this. */
|
||||
export const STALE_AFTER_MS = 15 * 60 * 1000;
|
||||
|
||||
/** Updates that can actually be applied (not refused as invalid at head). */
|
||||
export const applicableUpdates = (updates: ItemUpdate[]) => updates.filter((u) => u.invalid_at_head === null);
|
||||
|
||||
export const itemRefKey = (r: MarketplaceItemRef) => `${r.marketplace_id}/${r.kind}/${r.key}`;
|
||||
|
||||
/** Same shape as the item strings in a `SyncReport`. */
|
||||
export const formatItemRef = (r: MarketplaceItemRef) => `${r.kind}:${r.key}`;
|
||||
|
||||
const sameItem = (a: MarketplaceItemRef, b: MarketplaceItemRef) =>
|
||||
a.marketplace_id === b.marketplace_id && a.kind === b.kind && a.key === b.key;
|
||||
|
||||
export function projectItemState(
|
||||
item: MarketplaceItemRef,
|
||||
globalInstalls: MarketplaceInstall[],
|
||||
project: Project,
|
||||
): ProjectItemState {
|
||||
const own = project.marketplace_installs.find((i) => sameItem(i, item));
|
||||
const global = globalInstalls.find((i) => sameItem(i, item));
|
||||
if (own) {
|
||||
return global && global.commit !== own.commit ? "project_pinned_differently" : "project";
|
||||
}
|
||||
if (!global) return "none";
|
||||
return project.marketplace_disabled.some((d) => sameItem(d, item)) ? "opted_out" : "inherited";
|
||||
}
|
||||
|
||||
/** Mirror of the backend's `effective_installs`, tagged with where each install comes from. */
|
||||
export function effectiveInstalls(
|
||||
globalInstalls: MarketplaceInstall[],
|
||||
project: Project,
|
||||
): (MarketplaceInstall & { source: "global" | "project" })[] {
|
||||
const byKey = new Map<string, MarketplaceInstall & { source: "global" | "project" }>();
|
||||
for (const g of globalInstalls) {
|
||||
if (project.marketplace_disabled.some((d) => sameItem(d, g))) continue;
|
||||
byKey.set(itemRefKey(g), { ...g, source: "global" });
|
||||
}
|
||||
for (const p of project.marketplace_installs) {
|
||||
byKey.set(itemRefKey(p), { ...p, source: "project" });
|
||||
}
|
||||
return [...byKey.entries()]
|
||||
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
.map(([, v]) => v);
|
||||
}
|
||||
|
||||
export function isStale(snapshot: MarketplaceSnapshot, now: number): boolean {
|
||||
if (!snapshot.fetched_at) return true;
|
||||
const at = Date.parse(snapshot.fetched_at);
|
||||
if (Number.isNaN(at)) return true;
|
||||
return now - at > STALE_AFTER_MS;
|
||||
}
|
||||
@@ -20,6 +20,10 @@ function preview(overrides: Partial<SettingsImportPreview> = {}): SettingsImport
|
||||
gateway_api_base: null,
|
||||
image_source: "registry",
|
||||
custom_image_name: null,
|
||||
marketplace_count: 0,
|
||||
global_hook_install_count: 0,
|
||||
global_plugin_install_count: 0,
|
||||
marketplace_account_token_count: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -79,6 +83,13 @@ describe("describeImport", () => {
|
||||
expect(items.some((i) => i.includes("OpenAI-compatible"))).toBe(false);
|
||||
});
|
||||
|
||||
it("names marketplaces and marketplace account tokens, with counts", () => {
|
||||
const items = describeImport(preview({ marketplace_count: 1, marketplace_account_token_count: 2 }));
|
||||
expect(items).toContain("1 marketplace");
|
||||
expect(items).toContain("2 marketplace account tokens");
|
||||
expect(describeImport(preview()).some((i) => i.includes("marketplace"))).toBe(false);
|
||||
});
|
||||
|
||||
it("names a custom Docker image when set, falling back to a placeholder if unnamed", () => {
|
||||
expect(
|
||||
describeImport(preview({ image_source: "custom", custom_image_name: "ghcr.io/me/triple-c" })),
|
||||
@@ -116,6 +127,24 @@ describe("describeImportWarnings", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("warns when the import installs hooks for every project", () => {
|
||||
expect(describeImportWarnings(preview({ global_hook_install_count: 1 }))).toEqual([
|
||||
"Installs 1 marketplace hook for all projects. Hooks run commands in every project container, and these skip the confirmation that lists a hook's commands before a Marketplace tab install.",
|
||||
]);
|
||||
expect(describeImportWarnings(preview({ global_hook_install_count: 3 }))[0]).toMatch(
|
||||
/^Installs 3 marketplace hooks for all projects\./,
|
||||
);
|
||||
});
|
||||
|
||||
it("warns when the import installs plugins for every project", () => {
|
||||
expect(describeImportWarnings(preview({ global_plugin_install_count: 1 }))).toEqual([
|
||||
"Installs 1 marketplace plugin for all projects. Plugins can bring their own hooks, MCP servers and commands into every project container, and these skip the confirmation that lists what a plugin brings before a Marketplace tab install.",
|
||||
]);
|
||||
expect(
|
||||
describeImportWarnings(preview({ global_plugin_install_count: 2, global_hook_install_count: 1 })),
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("warns about a custom Docker image every time, not only when it changes", () => {
|
||||
expect(
|
||||
describeImportWarnings(preview({ image_source: "custom", custom_image_name: "evil:latest" })),
|
||||
|
||||
@@ -28,6 +28,13 @@ export function describeImport(preview: SettingsImportPreview): string[] {
|
||||
if (preview.image_source === "custom") {
|
||||
items.push(`Docker image: ${preview.custom_image_name ?? "(no image name set)"}`);
|
||||
}
|
||||
if (preview.marketplace_count > 0) {
|
||||
items.push(`${preview.marketplace_count} marketplace${preview.marketplace_count === 1 ? "" : "s"}`);
|
||||
}
|
||||
if (preview.marketplace_account_token_count > 0) {
|
||||
const n = preview.marketplace_account_token_count;
|
||||
items.push(`${n} marketplace account token${n === 1 ? "" : "s"}`);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -45,6 +52,11 @@ export function describeImport(preview: SettingsImportPreview): string[] {
|
||||
* through the UI, with no import-time signal that it wasn't freshly
|
||||
* generated.
|
||||
*
|
||||
* Global marketplace hooks get one too: a hook runs commands in every
|
||||
* project container, and an imported install never passed the hook-confirm
|
||||
* step an install from the Marketplace tab shows. Global plugins likewise:
|
||||
* a plugin can carry its own hooks and MCP servers.
|
||||
*
|
||||
* A custom Docker image gets a warning every time, not just on change: it's
|
||||
* the image every project container is created from, so it's worth calling
|
||||
* out regardless of what was configured before the import.
|
||||
@@ -58,6 +70,18 @@ export function describeImportWarnings(preview: SettingsImportPreview): string[]
|
||||
"Includes a web terminal access token that will activate the next time the web terminal is turned on.",
|
||||
);
|
||||
}
|
||||
if (preview.global_hook_install_count > 0) {
|
||||
const n = preview.global_hook_install_count;
|
||||
warnings.push(
|
||||
`Installs ${n} marketplace hook${n === 1 ? "" : "s"} for all projects. Hooks run commands in every project container, and these skip the confirmation that lists a hook's commands before a Marketplace tab install.`,
|
||||
);
|
||||
}
|
||||
if (preview.global_plugin_install_count > 0) {
|
||||
const n = preview.global_plugin_install_count;
|
||||
warnings.push(
|
||||
`Installs ${n} marketplace plugin${n === 1 ? "" : "s"} for all projects. Plugins can bring their own hooks, MCP servers and commands into every project container, and these skip the confirmation that lists what a plugin brings before a Marketplace tab install.`,
|
||||
);
|
||||
}
|
||||
if (preview.image_source === "custom") {
|
||||
warnings.push(
|
||||
`Runs every project container from a custom Docker image: ${preview.custom_image_name ?? "(no image name set)"}.`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { Project, ProjectPath, ProjectRemovalReport, ProjectResetOutcome, ContainerInfo, AppSettings, SettingsImportPreview, SettingsImportOutcome, UpdateInfo, ImageUpdateInfo, FileEntry, FileContents, WebTerminalInfo, SttStatus, GatewayStatus, InstallOptions, ClaudeSession, ContainerCapabilities, ScheduledTask, ScheduledTaskInput, SchedulerNotification, AuthBridgeStatus, BrowserViewStatus, BrowserViewPopoutState, BrowserPageState, PlaywrightDetection, BrowserSetupOutcome, BrowserInstallTarget, ContainerStaleness, MigrationOptions, MigrationReport, MigrationState, ClearTokenOutcome, CaCertInfo, UploadOutcome, Note, ViewerFile, ViewerPoll, ViewerSaved, ViewerState } from "./types";
|
||||
import type { Project, ProjectPath, ProjectRemovalReport, ProjectResetOutcome, ContainerInfo, AppSettings, SettingsImportPreview, SettingsImportOutcome, UpdateInfo, ImageUpdateInfo, FileEntry, FileContents, WebTerminalInfo, SttStatus, GatewayStatus, InstallOptions, ClaudeSession, ContainerCapabilities, ScheduledTask, ScheduledTaskInput, SchedulerNotification, AuthBridgeStatus, BrowserViewStatus, BrowserViewPopoutState, BrowserPageState, PlaywrightDetection, BrowserSetupOutcome, BrowserInstallTarget, ContainerStaleness, MigrationOptions, MigrationReport, MigrationState, ClearTokenOutcome, CaCertInfo, UploadOutcome, Note, ViewerFile, ViewerPoll, ViewerSaved, ViewerState, FileDiff, InstallScope, ItemUpdate, Marketplace, MarketplaceAccount, MarketplaceItemRef, MarketplaceSnapshot, ProjectSyncResult, SyncReport } from "./types";
|
||||
|
||||
// Docker
|
||||
export const checkDocker = () => invoke<boolean>("check_docker");
|
||||
@@ -432,3 +432,57 @@ export const viewerWriteFile = (contentsBase64: string, baseHash: string) =>
|
||||
invoke<ViewerSaved>("viewer_write_file", { contentsBase64, baseHash });
|
||||
export const viewerChooseFile = (index: number) =>
|
||||
invoke<ViewerState>("viewer_choose_file", { index });
|
||||
|
||||
// ---- Marketplace ----
|
||||
|
||||
export const listMarketplaceSnapshots = () =>
|
||||
invoke<MarketplaceSnapshot[]>("list_marketplace_snapshots");
|
||||
export const refreshMarketplaces = (marketplaceId?: string) =>
|
||||
invoke<MarketplaceSnapshot[]>("refresh_marketplaces", { marketplaceId: marketplaceId ?? null });
|
||||
export const addMarketplace = (
|
||||
name: string,
|
||||
url: string,
|
||||
branch: string | null,
|
||||
accountId: string | null,
|
||||
) => invoke<MarketplaceSnapshot>("add_marketplace", { name, url, branch, accountId });
|
||||
export const updateMarketplace = (marketplace: Marketplace) =>
|
||||
invoke<AppSettings>("update_marketplace", { marketplace });
|
||||
export const removeMarketplace = (marketplaceId: string) =>
|
||||
invoke<AppSettings>("remove_marketplace", { marketplaceId });
|
||||
/** `expectedCommit`: the head the user reviewed; the backend refuses if it moved. */
|
||||
export const installMarketplaceItem = (item: MarketplaceItemRef, scope: InstallScope, expectedCommit: string) =>
|
||||
invoke<AppSettings>("install_marketplace_item", { item, scope, expectedCommit });
|
||||
export const uninstallMarketplaceItem = (item: MarketplaceItemRef, scope: InstallScope) =>
|
||||
invoke<void>("uninstall_marketplace_item", { item, scope });
|
||||
export const setGlobalItemDisabled = (
|
||||
projectId: string,
|
||||
item: MarketplaceItemRef,
|
||||
disabled: boolean,
|
||||
) => invoke<Project>("set_global_item_disabled", { projectId, item, disabled });
|
||||
export const forgetMarketplaceInstalls = (marketplaceId: string) =>
|
||||
invoke<void>("forget_marketplace_installs", { marketplaceId });
|
||||
export const listMarketplaceUpdates = () => invoke<ItemUpdate[]>("list_marketplace_updates");
|
||||
export const marketplaceItemDiff = (
|
||||
item: MarketplaceItemRef,
|
||||
fromCommit: string,
|
||||
toCommit: string,
|
||||
) => invoke<FileDiff[]>("marketplace_item_diff", { item, fromCommit, toCommit });
|
||||
/** `expectedCommit`: the head whose diff the user accepted; the backend refuses if it moved. */
|
||||
export const updateMarketplaceItem = (item: MarketplaceItemRef, scope: InstallScope, expectedCommit: string) =>
|
||||
invoke<void>("update_marketplace_item", { item, scope, expectedCommit });
|
||||
export const applyMarketplaceNow = (projectId?: string) =>
|
||||
invoke<ProjectSyncResult[]>("apply_marketplace_now", { projectId: projectId ?? null });
|
||||
export const getMarketplaceSyncReport = (projectId: string) =>
|
||||
invoke<SyncReport | null>("get_marketplace_sync_report", { projectId });
|
||||
export const addMarketplaceTokenAccount = (label: string, host: string, token: string) =>
|
||||
invoke<MarketplaceAccount>("add_marketplace_token_account", { label, host, token });
|
||||
export const addMarketplaceGhHostAccount = (label: string, host: string) =>
|
||||
invoke<MarketplaceAccount>("add_marketplace_gh_host_account", { label, host });
|
||||
export const startMarketplaceGhContainerLogin = (label: string, host: string, projectId: string) =>
|
||||
invoke<MarketplaceAccount>("start_marketplace_gh_container_login", { label, host, projectId });
|
||||
export const cancelMarketplaceGhLogin = () => invoke<void>("cancel_marketplace_gh_login");
|
||||
export const testMarketplaceAccount = (accountId: string) =>
|
||||
invoke<string>("test_marketplace_account", { accountId });
|
||||
export const removeMarketplaceAccount = (accountId: string) =>
|
||||
invoke<AppSettings>("remove_marketplace_account", { accountId });
|
||||
export const marketplaceGhHostAvailable = () => invoke<boolean>("marketplace_gh_host_available");
|
||||
|
||||
@@ -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,90 @@ 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;
|
||||
/** Plugins only: what the plugin brings that runs or adds commands (entry + folder). */
|
||||
plugin_components: PluginComponent[];
|
||||
}
|
||||
export interface PluginComponent {
|
||||
/** Where it comes from, e.g. "marketplace.json entry: mcpServers". */
|
||||
label: string;
|
||||
content: 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;
|
||||
/** Why the item cannot be installed at `head`, so the update would be refused; null when it applies. */
|
||||
invalid_at_head: string | null;
|
||||
}
|
||||
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 —
|
||||
@@ -327,6 +413,17 @@ export interface SettingsImportPreview {
|
||||
* more attention than an ordinary setting. */
|
||||
image_source: ImageSource;
|
||||
custom_image_name: string | null;
|
||||
/** Marketplaces the import configures. */
|
||||
marketplace_count: number;
|
||||
/** Hooks the import installs for all projects — each runs commands in
|
||||
* every project container, without the confirm step a Marketplace-tab
|
||||
* install shows, so the preview warns about them. */
|
||||
global_hook_install_count: number;
|
||||
/** Plugins the import installs for all projects — a plugin can bring its
|
||||
* own hooks and MCP servers, and skips the same confirm step. */
|
||||
global_plugin_install_count: number;
|
||||
/** Marketplace account tokens the import restores to the keychain. */
|
||||
marketplace_account_token_count: number;
|
||||
}
|
||||
|
||||
/** What `apply_settings_import` returns: the settings that were actually
|
||||
|
||||
Reference in New Issue
Block a user