Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,17 @@ Default local URLs:

## Configuration

By default, `skill-manager` resolves harness paths from `HOME` and `XDG_CONFIG_HOME`. You can override individual roots with environment variables.
`skill-manager` stores its own app data in standard per-user locations.

On macOS, app-owned files live under `~/Library/Application Support/skill-manager`.

Useful paths:

- shared managed store: `~/Library/Application Support/skill-manager/shared`
- marketplace cache: `~/Library/Application Support/skill-manager/marketplace`
- app settings: `~/Library/Application Support/skill-manager/settings.json`

Most users do not need to change these locations. If you manage skills in a custom environment, you can override individual harness roots with environment variables.

### Codex

Expand All @@ -209,15 +219,9 @@ By default, `skill-manager` resolves harness paths from `HOME` and `XDG_CONFIG_H

- global scope defaults to `~/.openclaw/skills`

### Marketplace

- `SKILL_MANAGER_MARKETPLACE_BASE_URL`

These overrides are useful when you need to relocate the canonical global skill roots in a controlled environment. `SKILL_MANAGER_MARKETPLACE_BASE_URL` is an advanced override for deterministic tests and release validation; normal installs should continue to use the production `skills.sh` marketplace.

## Troubleshooting

- If Marketplace requests fail with `Marketplace is temporarily unavailable`, verify your network connection first. Packaged installs use the bundled CA bundle automatically; if failures persist after a reinstall, check whether your environment overrides `SSL_CERT_FILE`.
- If Marketplace requests fail with `Marketplace is temporarily unavailable`, verify your network connection and try reinstalling `skill-manager` if the problem persists.
- If `npm install -g @mode-io/skill-manager` reports that Homebrew already owns `skill-manager`, uninstall the Homebrew formula first. The inverse also applies: uninstall the npm package before switching back to Homebrew.

## Development
Expand Down
12 changes: 3 additions & 9 deletions frontend/src/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { App } from "../App";
import { createMarketplaceItem } from "../features/marketplace/test-fixtures";

const fetchMock = vi.fn();

Expand Down Expand Up @@ -196,22 +197,15 @@ function mockSkillsPage(options?: { codexSupportEnabled?: boolean }) {
ok: true,
json: async () => ({
items: [
{
createMarketplaceItem({
id: "skillssh:mode-io/shared-audit:shared-audit",
name: "Shared Audit",
description: "Shared audit workflow",
installs: 44,
stars: 33,
repoLabel: "mode-io/shared-audit",
repoUrl: "https://github.com/mode-io/shared-audit",
repoImageUrl: "https://avatars.githubusercontent.com/u/424242?v=4",
skillsDetailUrl: "https://skills.sh/mode-io/shared-audit/shared-audit",
installToken: "token-shared-audit",
installation: {
status: "installable",
installedSkillRef: null,
},
},
}),
],
nextOffset: null,
hasMore: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,10 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";

import type { MarketplaceItemDto } from "../api/types";
import { createMarketplaceItem } from "../test-fixtures";
import { MarketplaceCard } from "./MarketplaceCard";

const baseItem: MarketplaceItemDto = {
id: "skillssh:mode-io/skills:mode-switch",
name: "Mode Switch",
description: "Switch between supported skill execution modes.",
installs: 128,
stars: 512,
repoLabel: "mode-io/skills",
repoUrl: "https://github.com/mode-io/skills",
repoImageUrl: "https://avatars.githubusercontent.com/u/424242?v=4",
skillsDetailUrl: "https://skills.sh/mode-io/skills/mode-switch",
installToken: "token-mode-switch",
installation: {
status: "installable",
installedSkillRef: null,
},
};
const baseItem = createMarketplaceItem();

describe("MarketplaceCard", () => {
it("renders repo identity, installs, and stars", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";

import { useMarketplaceDetailQuery, useMarketplaceDocumentQuery } from "../api/queries";
import { createMarketplaceDetail } from "../test-fixtures";
import { MarketplaceDetailView } from "./MarketplaceDetailView";

vi.mock("../api/queries", () => ({
Expand All @@ -15,26 +16,9 @@ const useMarketplaceDocumentQueryMock = vi.mocked(useMarketplaceDocumentQuery);
describe("MarketplaceDetailView", () => {
it("shows the backend refresh error message when detail loading fails", () => {
useMarketplaceDetailQueryMock.mockReturnValue({
data: {
id: "skillssh:mode-io/skills:mode-switch",
name: "Mode Switch",
data: createMarketplaceDetail({
description: "Mode Switch description",
installs: 128,
stars: 512,
repoLabel: "mode-io/skills",
repoImageUrl: "https://avatars.githubusercontent.com/u/424242?v=4",
sourceLinks: {
repoLabel: "mode-io/skills",
repoUrl: "https://github.com/mode-io/skills",
folderUrl: null,
skillsDetailUrl: "https://skills.sh/mode-io/skills/mode-switch",
},
installation: {
status: "installable",
installedSkillRef: null,
},
installToken: "token-mode-switch",
},
}),
isPending: false,
isFetching: false,
error: new Error("Marketplace is temporarily unavailable. Check your network connection or reinstall skill-manager if the problem persists."),
Expand Down Expand Up @@ -66,26 +50,14 @@ describe("MarketplaceDetailView", () => {

it("does not render a refresh spinner for background detail refetches", () => {
useMarketplaceDetailQueryMock.mockReturnValue({
data: {
id: "skillssh:mode-io/skills:mode-switch",
name: "Mode Switch",
description: "Switch between supported skill execution modes.",
installs: 128,
stars: 512,
repoLabel: "mode-io/skills",
repoImageUrl: "https://avatars.githubusercontent.com/u/424242?v=4",
data: createMarketplaceDetail({
sourceLinks: {
repoLabel: "mode-io/skills",
repoUrl: "https://github.com/mode-io/skills",
folderUrl: "https://github.com/mode-io/skills/tree/main/skills/mode-switch",
skillsDetailUrl: "https://skills.sh/mode-io/skills/mode-switch",
},
installation: {
status: "installable",
installedSkillRef: null,
},
installToken: "token-mode-switch",
},
}),
isPending: false,
isFetching: true,
error: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MemoryRouter } from "react-router-dom";
import { describe, expect, it, vi } from "vitest";

import { useMarketplaceController } from "../model/use-marketplace-controller";
import { createMarketplaceItem } from "../test-fixtures";
import MarketplacePage from "./MarketplacePage";

vi.mock("../model/use-marketplace-controller", () => ({
Expand All @@ -19,24 +20,7 @@ describe("MarketplacePage loading ownership", () => {
errorMessage: "",
selectedItemId: null,
selectedItem: null,
items: [
{
id: "skillssh:mode-io/skills:mode-switch",
name: "Mode Switch",
description: "Switch between supported skill execution modes.",
installs: 128,
stars: 512,
repoLabel: "mode-io/skills",
repoUrl: "https://github.com/mode-io/skills",
repoImageUrl: "https://avatars.githubusercontent.com/u/424242?v=4",
skillsDetailUrl: "https://skills.sh/mode-io/skills/mode-switch",
installToken: "token-mode-switch",
installation: {
status: "installable",
installedSkillRef: null,
},
},
],
items: [createMarketplaceItem()],
feedQuery: {
isFetching: true,
fetchNextPage: vi.fn(async () => undefined),
Expand Down
74 changes: 23 additions & 51 deletions frontend/src/features/marketplace/screens/MarketplacePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { act, fireEvent, render, screen, waitFor, within } from "@testing-librar
import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { createMarketplaceDetail, createMarketplaceItem } from "../test-fixtures";
import MarketplacePage from "./MarketplacePage";

const fetchMock = vi.fn();
Expand Down Expand Up @@ -160,26 +161,16 @@ describe("MarketplacePage", () => {
});
}
if (url.includes("/api/marketplace/items/skillssh%3Amode-io%2Fskills%3Amode-switch")) {
return okJson({
id: "skillssh:mode-io/skills:mode-switch",
name: "Mode Switch",
description: "Switch between supported skill execution modes.",
installs: 128,
stars: 512,
repoLabel: "mode-io/skills",
repoImageUrl: "https://avatars.githubusercontent.com/u/424242?v=4",
sourceLinks: {
repoLabel: "mode-io/skills",
repoUrl: "https://github.com/mode-io/skills",
folderUrl: "https://github.com/mode-io/skills/tree/main/skills/mode-switch",
skillsDetailUrl: "https://skills.sh/mode-io/skills/mode-switch",
},
installation: {
status: "installable",
installedSkillRef: null,
},
installToken: "token-mode-switch",
});
return okJson(
createMarketplaceDetail({
sourceLinks: {
repoLabel: "mode-io/skills",
repoUrl: "https://github.com/mode-io/skills",
folderUrl: "https://github.com/mode-io/skills/tree/main/skills/mode-switch",
skillsDetailUrl: "https://skills.sh/mode-io/skills/mode-switch",
},
}),
);
}
throw new Error(`Unhandled URL ${url}`);
});
Expand Down Expand Up @@ -231,26 +222,16 @@ describe("MarketplacePage", () => {

await act(async () => {
pendingDetail.resolve(
okJson({
id: "skillssh:mode-io/skills:mode-switch",
name: "Mode Switch",
description: "Switch between supported skill execution modes.",
installs: 128,
stars: 512,
repoLabel: "mode-io/skills",
repoImageUrl: "https://avatars.githubusercontent.com/u/424242?v=4",
sourceLinks: {
repoLabel: "mode-io/skills",
repoUrl: "https://github.com/mode-io/skills",
folderUrl: "https://github.com/mode-io/skills/tree/main/skills/mode-switch",
skillsDetailUrl: "https://skills.sh/mode-io/skills/mode-switch",
},
installation: {
status: "installable",
installedSkillRef: null,
},
installToken: "token-mode-switch",
}),
okJson(
createMarketplaceDetail({
sourceLinks: {
repoLabel: "mode-io/skills",
repoUrl: "https://github.com/mode-io/skills",
folderUrl: "https://github.com/mode-io/skills/tree/main/skills/mode-switch",
skillsDetailUrl: "https://skills.sh/mode-io/skills/mode-switch",
},
}),
),
);
});

Expand Down Expand Up @@ -299,20 +280,11 @@ function deferred<T>() {
}

function baseItem(id: string, name: string, installs: number) {
return {
return createMarketplaceItem({
id: `skillssh:mode-io/skills:${id}`,
name,
description: `${name} description`,
installs,
stars: 512,
repoLabel: "mode-io/skills",
repoUrl: "https://github.com/mode-io/skills",
repoImageUrl: "https://avatars.githubusercontent.com/u/424242?v=4",
skillsDetailUrl: `https://skills.sh/mode-io/skills/${id}`,
installToken: `token-${id}`,
installation: {
status: "installable",
installedSkillRef: null,
},
};
});
}
59 changes: 59 additions & 0 deletions frontend/src/features/marketplace/test-fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { MarketplaceDetailDto, MarketplaceItemDto } from "./api/types";

function repoOwner(repoLabel: string): string {
return repoLabel.split("/", 1)[0] || repoLabel;
}

function skillIdFromItemId(itemId: string): string {
const [, skillId = "mode-switch"] = itemId.match(/^[^:]+:[^:]+:(.+)$/) ?? [];
return skillId;
}

export function marketplaceRepoImageUrl(repoLabel: string): string {
return `https://github.com/${repoOwner(repoLabel)}.png?size=96`;
}

export function createMarketplaceItem(overrides: Partial<MarketplaceItemDto> = {}): MarketplaceItemDto {
const repoLabel = overrides.repoLabel ?? "mode-io/skills";
const id = overrides.id ?? `skillssh:${repoLabel}:mode-switch`;
const skillId = skillIdFromItemId(id);

return {
id,
name: overrides.name ?? "Mode Switch",
description: overrides.description ?? "Switch between supported skill execution modes.",
installs: overrides.installs ?? 128,
stars: overrides.stars ?? 512,
repoLabel,
repoUrl: overrides.repoUrl ?? `https://github.com/${repoLabel}`,
repoImageUrl: overrides.repoImageUrl ?? marketplaceRepoImageUrl(repoLabel),
skillsDetailUrl: overrides.skillsDetailUrl ?? `https://skills.sh/${repoLabel}/${skillId}`,
installToken: overrides.installToken ?? `token-${skillId}`,
installation: overrides.installation ?? {
status: "installable",
installedSkillRef: null,
},
};
}

export function createMarketplaceDetail(overrides: Partial<MarketplaceDetailDto> = {}): MarketplaceDetailDto {
const item = createMarketplaceItem(overrides);

return {
id: item.id,
name: item.name,
description: item.description,
installs: item.installs,
stars: item.stars,
repoLabel: item.repoLabel,
repoImageUrl: item.repoImageUrl,
sourceLinks: overrides.sourceLinks ?? {
repoLabel: item.repoLabel,
repoUrl: item.repoUrl,
folderUrl: null,
skillsDetailUrl: item.skillsDetailUrl,
},
installation: overrides.installation ?? item.installation,
installToken: overrides.installToken ?? item.installToken,
};
}
2 changes: 1 addition & 1 deletion frontend/src/features/skills/screens/ManagedSkillsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export default function ManagedSkillsPage() {
<div>
<p className="skills-empty-state__eyebrow">No managed skills yet</p>
<h3>Your shared inventory is empty.</h3>
<p>Review detected local skills or install something from the marketplace to start managing coverage here.</p>
<p>Review unmanaged skills found in supported global roots or install something from the marketplace to start managing coverage here.</p>
</div>
<div className="skills-empty-state__actions">
<Link to="/skills/unmanaged" className="btn btn-primary">
Expand Down
3 changes: 2 additions & 1 deletion skill_manager/application/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from dataclasses import dataclass
import os

from skill_manager.storage_paths import default_harness_support_path
from .marketplace import (
MarketplaceCatalog,
MarketplaceDocumentService,
Expand All @@ -13,7 +14,7 @@
from .settings import SettingsMutationService, SettingsQueryService
from .skills import SkillsMutationService, SkillsQueryService
from .source_fetch_service import SourceFetchService
from skill_manager.store import HarnessSupportStore, default_harness_support_path
from skill_manager.store import HarnessSupportStore


@dataclass(frozen=True)
Expand Down
Loading
Loading