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
9 changes: 9 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,15 @@ jobs:
test do
assert_match version.to_s, shell_output("#{bin}/hs --version")
end

def caveats
<<~EOS
To install the Claude Code skill that drives this CLI:
hs skill install
Cross-agent (Cursor/Codex/Copilot/OpenCode), via skills.sh:
npx skills add say8425/hs-cli
EOS
end
end
RB

Expand Down
10 changes: 9 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,13 @@
"promise/no-return-wrap": "error",
"promise/param-names": "error"
},
"ignorePatterns": ["dist/**", "node_modules/**", "*.config.js", "*.config.ts"]
"ignorePatterns": ["dist/**", "node_modules/**", "*.config.js", "*.config.ts"],
"overrides": [
{
"files": ["src/commands/skill.ts"],
"rules": {
"no-await-in-loop": "off"
}
}
]
}
9 changes: 8 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ hs card <dbfId|cardId|name> [-l koKR] # card lookup
hs card --search <q> --class CLASS [-l koKR] # filtered search
hs card --class CLASS [--cost N] [-l koKR] # browse (blank/no --search = wildcard)
hs meta sets|classes|types|rarities [-l koKR]
hs skill install [--agent claude,cursor,codex,copilot,opencode] [--project] [--use-npx] # install the hearthstone-deck skill into agent skills dirs
```

Add `-f json` to any command for raw JSON. Default `table` format is agent-friendly (compressed).
Expand Down Expand Up @@ -60,16 +61,22 @@ Install channels for end users (CLI):

Claude Code plugin install (all channels): `/plugin marketplace add say8425/hs-cli` + `/plugin install hs-cli@say8425`. Validate with `claude plugin validate .` (marketplace) or `claude plugin validate ./plugins/hs-cli` (plugin).

Install the bundled `hearthstone-deck` skill into any agent (works across all CLI channels — the skill is embedded in the binary, no network needed): `hs skill install` (interactive multiselect) or `hs skill install --agent claude`. Cross-agent via [skills.sh](https://www.skills.sh/): `npx skills add say8425/hs-cli` (discovered through `.claude-plugin/marketplace.json`, no repo restructure needed).

**Do not duplicate SKILL.md at the repo root.** The single source of truth lives inside the plugin. Root-level docs should link to it, not copy it.

## Architecture

- `src/index.ts` — citty `runMain`, registers subcommands
- `src/commands/` — one file per command (deck/card/meta), each exports `defineCommand` instance
- `src/commands/` — one file per command (deck/card/meta/skill), each exports `defineCommand` instance. `skill.ts` nests `install` as a subcommand (`hs skill install`).
- `src/services/card-db.ts` — HearthstoneJSON CDN fetch + local cache at `~/.hs-cli/`
- `src/services/deck-decoder.ts` — wraps `deckstrings` npm, joins with card DB
- `src/services/formatter.ts` — table/json output. **Add new formatters here, not in commands.**
- `src/services/locale.ts` — locale detection + normalization (`ko` / `ko-KR` / `ko_KR` / `koKR` → `koKR`)
- `src/services/agent-dirs.ts` — agent→skills-dir map (claude/cursor/codex/copilot/opencode) + path resolver (injectable home/cwd for tests)
- `src/services/skill-bundle.ts` — `hearthstone-deck` SKILL.md + recipes embedded into the binary via Bun text imports. Single source of truth stays at `plugins/hs-cli/skills/hearthstone-deck/`; a test asserts byte-equality with disk.
- `src/services/skill-installer.ts` — pure FS writer for the embedded skill (`writeBundle`, `skillExists`)
- `src/services/skill-select.ts` — pure agent-selection resolver (`--agent` validation + non-TTY guard); clack multiselect lives in the command, not here
- `src/types/index.ts` — `Card`, `Deck`, `DeckCard` types + Korean class name map
- `tests/` — bun:test files, one per service. Imports use `.ts` extension (Bun resolves native TS).
- `tsconfig.json` — typecheck + IDE config. `noEmit: true`, `allowImportingTsExtensions: true`. No separate build config.
Expand Down
15 changes: 15 additions & 0 deletions README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@ hs --version
hs deck "AAECAQcAA0VjgAEAAA=="
```

### Instalar la skill de Claude Code

El CLI `hs` incluye la skill `hearthstone-deck`. Tras instalar el CLI:

```bash
hs skill install # interactivo: elige agentes
hs skill install --agent claude # no interactivo
```

Instalación multi-agente vía [skills.sh](https://www.skills.sh/) (Cursor, Codex, Copilot, OpenCode):

```bash
npx skills add say8425/hs-cli
```

## Uso

### Decodificar un mazo
Expand Down
15 changes: 15 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@ hs --version
hs deck "AAECAQcAA0VjgAEAAA=="
```

### Claude Code スキルのインストール

`hs` CLI には `hearthstone-deck` スキルが同梱されています。CLI インストール後:

```bash
hs skill install # 対話式: エージェントを選択
hs skill install --agent claude # 非対話式
```

[skills.sh](https://www.skills.sh/) 経由のマルチエージェントインストール (Cursor、Codex、Copilot、OpenCode):

```bash
npx skills add say8425/hs-cli
```

## 使い方

### デッキをデコード
Expand Down
15 changes: 15 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@ hs --version
hs deck "AAECAQcAA0VjgAEAAA=="
```

### Claude Code 스킬 설치

`hs` CLI에는 `hearthstone-deck` 스킬이 포함되어 있습니다. CLI 설치 후:

```bash
hs skill install # 대화형: 에이전트 선택
hs skill install --agent claude # 비대화형
```

[skills.sh](https://www.skills.sh/)를 통한 멀티 에이전트 설치 (Cursor, Codex, Copilot, OpenCode):

```bash
npx skills add say8425/hs-cli
```

## 사용법

### 덱 디코딩
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@ hs --version
hs deck "AAECAQcAA0VjgAEAAA=="
```

### Install the Claude Code skill

The `hs` CLI ships the `hearthstone-deck` skill. After installing the CLI:

```bash
hs skill install # interactive: pick agents
hs skill install --agent claude # non-interactive
```

Cross-agent install via [skills.sh](https://www.skills.sh/) (Cursor, Codex, Copilot, OpenCode):

```bash
npx skills add say8425/hs-cli
```

## Usage

### Decode a deck
Expand Down
15 changes: 15 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@ hs --version
hs deck "AAECAQcAA0VjgAEAAA=="
```

### 安装 Claude Code 技能

`hs` CLI 内置 `hearthstone-deck` 技能。安装 CLI 后:

```bash
hs skill install # 交互式:选择代理
hs skill install --agent claude # 非交互式
```

通过 [skills.sh](https://www.skills.sh/) 跨代理安装(Cursor、Codex、Copilot、OpenCode):

```bash
npx skills add say8425/hs-cli
```

## 使用方法

### 解码套牌
Expand Down
13 changes: 13 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"bun": ">=1.3"
},
"dependencies": {
"@clack/prompts": "^1.4.0",
"citty": "^0.1.6",
"deckstrings": "^3.1.2"
},
Expand Down
15 changes: 15 additions & 0 deletions plugins/hs-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ hs --version
hs deck "AAECAQcAA0VjgAEAAA=="
```

### Install the Claude Code skill

The `hs` CLI ships the `hearthstone-deck` skill. After installing the CLI:

```bash
hs skill install # interactive: pick agents
hs skill install --agent claude # non-interactive
```

Cross-agent install via [skills.sh](https://www.skills.sh/) (Cursor, Codex, Copilot, OpenCode):

```bash
npx skills add say8425/hs-cli
```

## Install the plugin

From the marketplace:
Expand Down
157 changes: 157 additions & 0 deletions src/commands/skill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { spawnSync } from "node:child_process";
import { defineCommand } from "citty";
import { isCancel, multiselect, confirm } from "@clack/prompts";
import { AGENTS, resolveAgentDir, type AgentId } from "../services/agent-dirs.ts";
import { resolveSelection } from "../services/skill-select.ts";
import { skillExists, writeBundle, targetSkillDir } from "../services/skill-installer.ts";
import { SKILL_NAME } from "../services/skill-bundle.ts";
import { formatSkillOutcomes } from "../services/formatter.ts";
import type { OutputFormat, SkillOutcome } from "../types/index.ts";

const fail = (message: string): never => {
process.stderr.write(`${message}\n`);
process.exit(1);
};

const promptAgents = async (): Promise<readonly AgentId[]> => {
const picked = await multiselect({
message: "Install the hearthstone-deck skill for which agents?",
options: AGENTS.map((a) => ({ value: a.id, label: a.label })),
required: true,
output: process.stderr,
});
if (isCancel(picked)) process.exit(0);
return picked as readonly AgentId[];
};

const delegateToNpx = (global: boolean): never => {
const args = ["skills", "add", "say8425/hs-cli", "--skill", SKILL_NAME];
if (global) args.push("-g");
process.stderr.write(`Delegating to: npx ${args.join(" ")}\n`);
const res = spawnSync("npx", args, { stdio: "inherit" });
process.exit(res.status ?? 1);
};

const installCommand = defineCommand({
meta: {
name: "install",
description: "Install the hearthstone-deck skill into agent skills dirs",
},
args: {
agent: {
type: "string",
description: "Comma-separated agent ids: claude,cursor,codex,copilot,opencode",
},
project: {
type: "boolean",
default: false,
description: "Install into the current project instead of the user home (global)",
},
"use-npx": {
type: "boolean",
default: false,
description:
"Delegate to `npx skills add` when npx is available (installs for all agents skills detects; ignores --agent)",
},
force: {
type: "boolean",
default: false,
description: "Overwrite an existing skill without prompting",
},
format: {
type: "string",
alias: "f",
default: "table",
description: "Output format: table or json",
},
},
run: async ({ args }) => {
const scope = args.project ? "project" : "global";

if (args["use-npx"]) {
const hasNpx = spawnSync("npx", ["--version"], { stdio: "ignore" }).status === 0;
if (hasNpx) delegateToNpx(scope === "global");
process.stderr.write("npx not found; falling back to embedded install.\n");
}

const agentFlags = (args.agent ?? "")
.split(",")
.map((s: string) => s.trim())
.filter((s: string) => s.length > 0);

const selection = resolveSelection({
agents: agentFlags,
isTTY: process.stdout.isTTY === true,
});
if (selection.kind === "error") fail(selection.message);

const agentIds: readonly AgentId[] =
selection.kind === "explicit" ? selection.agents : await promptAgents();

const outcomes: SkillOutcome[] = [];

// Several agents can resolve to the same dir (e.g. project scope: cursor/codex/
// copilot/opencode all map to .agents/skills). Dedupe by resolved target dir so we
// writeBundle once per physical dir and report a single accurate outcome for it.
const byDir = new Map<string, string[]>();
const order: string[] = [];
for (const id of agentIds) {
const def = AGENTS.find((a) => a.id === id);
if (!def) {
outcomes.push({ agent: id, path: id, status: "failed", error: "unknown agent" });
continue;
}
const baseDir = resolveAgentDir(def, { scope });
const existing = byDir.get(baseDir);
if (existing) {
existing.push(id);
} else {
byDir.set(baseDir, [id]);
order.push(baseDir);
}
}

// Sequential on purpose: the overwrite confirm() prompt must be shown one dir at a time.
for (const baseDir of order) {
const ids = byDir.get(baseDir) ?? [];
const agentLabel = ids.join(",");
try {
const exists = await skillExists(baseDir);
if (exists && !args.force && process.stdout.isTTY === true) {
const ok = await confirm({
message: `${agentLabel}: skill exists at ${baseDir}. Overwrite?`,
output: process.stderr,
});
if (isCancel(ok)) process.exit(0);
if (ok === false) {
outcomes.push({ agent: agentLabel, path: baseDir, status: "skipped" });
continue;
}
}
await writeBundle(baseDir);
outcomes.push({
agent: agentLabel,
path: targetSkillDir(baseDir),
status: exists ? "overwritten" : "installed",
});
} catch (err) {
outcomes.push({
agent: agentLabel,
path: baseDir,
status: "failed",
error: err instanceof Error ? err.message : String(err),
});
}
}

process.stdout.write(`${formatSkillOutcomes(outcomes, args.format as OutputFormat)}\n`);

const anySuccess = outcomes.some((o) => o.status === "installed" || o.status === "overwritten");
process.exit(anySuccess ? 0 : 1);
},
});

export const skillCommand = defineCommand({
meta: { name: "skill", description: "Manage the hearthstone-deck Claude Code skill" },
subCommands: { install: installCommand },
});
Loading