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
4 changes: 2 additions & 2 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"mcpServers": {
"petsonality": {
"type": "stdio",
"command": "bun",
"command": "node",
"args": [
"server/index.ts"
"dist/server.js"
]
}
}
Expand Down
24 changes: 24 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/sh
# pre-commit: rebuild dist/ if any source-side files are staged.
#
# Without this hook it's easy to forget `bun run build` after editing source
# code, which means main ends up with dist/ artifacts that don't match the
# source they were supposedly built from. Plugin / GitHub-clone users would
# then run stale binaries while the source on disk shows the latest changes.
#
# We only rebuild when staged files actually touch the build inputs — pure
# doc/test/CONTRIBUTING commits skip this and stay fast.

set -e

CHANGED=$(git diff --cached --name-only --diff-filter=ACMR)

# Build inputs: anything that ends up in dist/server.js, dist/cli/*.js,
# statusline/*, or dist/reactions-pool.json.
if echo "$CHANGED" | grep -qE '^(server/|cli/|scripts/|statusline/|hooks/|skills/|package\.json$|bun\.lock$|\.claude-plugin/)'; then
echo "[pre-commit] source files changed — rebuilding dist/ + statusline/"
bun run build
git add dist/ statusline/pet-status.sh statusline/pet-art.json
fi

exit 0
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
node_modules/
dist/
.DS_Store
*.log
.petsonality/
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,13 @@ petsonality/
│ ├── which.ts ← cross-platform binary lookup
│ └── openclaw-patch.ts ← OpenClaw TUI patch (transitional)
├── hooks/ ← Claude Code hook scripts (PostToolUse, Stop)
├── statusline/ ← bash status line (pet-status.sh, generated from art.ts)
├── statusline/ ← bash + PowerShell status lines
├── skills/ ← Claude Code skill definition (pet/SKILL.md)
├── scripts/ ← build-time tools
└── dist/ ← bundled output (gitignored)
└── dist/ ← bundled output (tracked for plugin installs)
```

The `dist/` directory is the only thing shipped to npm beyond the source — see
The `dist/` directory is the bundled runtime used by npm and plugin installs — see
`package.json` `"files"` for the actual ship list.

---
Expand Down
10 changes: 5 additions & 5 deletions PRD-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@
| 渠道 | 用户类型 | 当前状态 | 需要做什么 |
|------|---------|---------|-----------|
| `npm + npx petsonality` | 看到推文 / Reddit / HN 来 | ✅ 主流 | 维护即可 |
| `claude plugin install github:nanami-he/petsonality` | Claude Code 内 browse 插件的人 | 🟡 manifest 已写但指向 `bun + server/index.ts`(需要 bun + 源码) | 改 plugin.json 指向 `node + dist/server.js` + 让 git 仓库带 dist/ |
| `claude plugin install github:nanami-he/petsonality` | Claude Code 内 browse 插件的人 | 🟢 manifest 已改为 `node + dist/server.js`,dist/ 已准备入 git | 真实 Claude Code plugin install smoke test |
| MCP Server Registry ([modelcontextprotocol.io](https://modelcontextprotocol.io)) | 在 MCP 官方目录浏览的人 | ❌ 未列 | 提交注册 PR / 表单 |
| `git clone + bun install + bun run build` | 开发者 / 想看源码 | ✅ 已能用 | CONTRIBUTING.md 已写 |
| Homebrew formula | macOS 原生开发者 | ❌ 未做 | 写 formula + 提交(长期项) |

**核心架构决策:dist/ 入不入 git?**

```
现状:dist/ 在 .gitignore 里
旧现状:dist/ 在 .gitignore 里
→ npm 路径:tarball 包含预编译 dist/,用户 npx 不需要 bun ✅
→ plugin/clone 路径:必须 bun build 才能跑 ⚠️
```
Expand All @@ -70,10 +70,10 @@
**决策方向**:倾向**方案 A** —— 1.1 MB git 增长可接受,换来 multi-channel 干净体验。

**任务清单(按优先序)**:
- [ ] 改 `.claude-plugin/plugin.json` —— `command` 改成 `node`,`args` 指向 `dist/server.js`
- [ ] 把 `dist/` 从 `.gitignore` 移除(保留 `dist/reactions-pool.json` 等已有内容)
- [x] 改 `.claude-plugin/plugin.json` —— `command` 改成 `node`,`args` 指向 `dist/server.js`
- [x] 把 `dist/` 从 `.gitignore` 移除(保留 `dist/reactions-pool.json` 等已有内容)
- [ ] 加 pre-commit hook 自动 `bun run build`(避免忘了 build 直接 commit)
- [ ] 把 dist/ 第一次 commit 进去(一次性增加 ~1.1 MB git 历史
- [ ] 把 dist/ 第一次 commit 进去(本次变更已生成 dist/,提交时纳入
- [ ] 测 `claude plugin install github:nanami-he/petsonality` 是否真能 work(需要新装 Claude Code 测)
- [ ] 提交 petsonality 到 [MCP Server Registry](https://modelcontextprotocol.io) 的 server directory
- [ ] README 加「Install」章节,列出 3 条路径(npx 主推,plugin 次推,git clone 给开发者)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
</p>

<p align="center">
<a href="https://www.npmjs.com/package/petsonality"><img src="https://img.shields.io/badge/npm-0.4.0-blue" alt="npm"></a>
<a href="https://www.npmjs.com/package/petsonality"><img src="https://img.shields.io/npm/v/petsonality?color=blue" alt="npm"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license"></a>
<a href="https://github.com/nanami-he/petsonality/stargazers"><img src="https://img.shields.io/github/stars/nanami-he/petsonality?style=flat" alt="stars"></a>
<a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-powered-orange" alt="MCP"></a>
Expand Down
13 changes: 13 additions & 0 deletions cli/doctor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, test, expect } from "bun:test";
import { readFileSync } from "fs";
import { join } from "path";

describe("doctor platform diagnostics", () => {
test("has a Windows-specific statusline smoke path", () => {
const source = readFileSync(join(import.meta.dir, "doctor.ts"), "utf8");

expect(source).toContain("IS_WIN");
expect(source).toContain("pet-status.ps1");
expect(source).toContain("powershell.exe");
});
});
50 changes: 37 additions & 13 deletions cli/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@
* Copy the entire output and paste it in a GitHub issue.
*/

import { readFileSync, existsSync, statSync } from "fs";
import { readFileSync, existsSync } from "fs";
import { execSync } from "child_process";
import { join } from "path";
import { homedir } from "os";
import { homedir, platform } from "os";
import { findPackageRoot } from "./find-package-root.ts";

const PROJECT_ROOT = findPackageRoot(import.meta.url);
const HOME = homedir();
const STATUS_SCRIPT = join(PROJECT_ROOT, "statusline", "pet-status.sh");
const IS_WIN = platform() === "win32";
const STATUS_SCRIPT = join(PROJECT_ROOT, "statusline", IS_WIN ? "pet-status.ps1" : "pet-status.sh");

const RED = "\x1b[31m";
const GREEN = "\x1b[32m";
Expand Down Expand Up @@ -71,13 +72,24 @@ console.log(`\n${DIM}Copy this entire output into your GitHub issue.${NC}`);
// ─── Environment ────────────────────────────────────────────────────────────

section("Environment");
row("OS", tryExec("uname -srm"));
row("Hostname", tryExec("uname -n"));
row("User shell", process.env.SHELL ?? "(unset)");
row("Bash version", tryExec("bash --version | head -1"));
if (IS_WIN) {
row("OS", tryExec("cmd.exe /c ver", process.platform));
row("Hostname", process.env.COMPUTERNAME ?? "(unset)");
row("User shell", process.env.ComSpec ?? process.env.COMSPEC ?? "(unset)");
row("PowerShell version", tryExec('powershell.exe -NoProfile -Command "$PSVersionTable.PSVersion.ToString()"', "(not in PATH)"));
} else {
row("OS", tryExec("uname -srm"));
row("Hostname", tryExec("uname -n"));
row("User shell", process.env.SHELL ?? "(unset)");
row("Bash version", tryExec("bash --version | head -1"));
}
row("Bun version", tryExec("bun --version"));
row("Node version", tryExec("node --version", "(not installed)"));
row("jq version", tryExec("jq --version", "(not installed)"));
if (IS_WIN) {
row("JSON parser", "PowerShell ConvertFrom-Json");
} else {
row("jq version", tryExec("jq --version", "(not installed)"));
}
row("Claude Code version", tryExec("claude --version", "(not in PATH)"));

// ─── Terminal ───────────────────────────────────────────────────────────────
Expand All @@ -88,14 +100,23 @@ row("COLORTERM", process.env.COLORTERM ?? "(unset)");
row("TERM_PROGRAM", process.env.TERM_PROGRAM ?? "(unset)");
row("LANG", process.env.LANG ?? "(unset)");
row("COLUMNS env var", process.env.COLUMNS ?? "(unset in subprocess)");
row("stty size", tryExec("stty size 2>/dev/null", "(no tty)"));
row("tput cols", tryExec("tput cols 2>/dev/null", "(failed)"));
if (IS_WIN) {
row("WT_SESSION", process.env.WT_SESSION ?? "(unset)");
row("ConEmuANSI", process.env.ConEmuANSI ?? "(unset)");
} else {
row("stty size", tryExec("stty size 2>/dev/null", "(no tty)"));
row("tput cols", tryExec("tput cols 2>/dev/null", "(failed)"));
}

// ─── Filesystem checks ──────────────────────────────────────────────────────

section("Filesystem");
const procExists = existsSync("/proc");
row("/proc exists", procExists ? `${GREEN}yes${NC} (Linux)` : `${RED}no${NC} (macOS/BSD)`);
if (IS_WIN) {
row("Windows profile", HOME);
} else {
row("/proc exists", procExists ? `${GREEN}yes${NC} (Linux)` : `${RED}no${NC} (macOS/BSD)`);
}
row("~/.claude/ exists", existsSync(join(HOME, ".claude")) ? "yes" : "no");
row("~/.claude.json exists", existsSync(join(HOME, ".claude.json")) ? "yes" : "no");
row("~/.petsonality/ exists", existsSync(join(HOME, ".petsonality")) ? "yes" : "no");
Expand Down Expand Up @@ -189,8 +210,11 @@ try {
// ─── Live status line test ──────────────────────────────────────────────────

section("Live status line output");
console.log(` ${DIM}(running: echo '{}' | ${STATUS_SCRIPT})${NC}\n`);
const liveOutput = tryExec(`echo '{}' | bash "${STATUS_SCRIPT}" 2>&1`, "(script failed)");
const liveCommand = IS_WIN
? `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${STATUS_SCRIPT}"`
: `echo '{}' | bash "${STATUS_SCRIPT}" 2>&1`;
console.log(` ${DIM}(running: ${liveCommand})${NC}\n`);
const liveOutput = tryExec(liveCommand, "(script failed)");
const lines = liveOutput.split("\n");
console.log(lines.map(l => ` │ ${l}`).join("\n"));
console.log(` ${DIM}(${lines.length} lines, total ${liveOutput.length} bytes)${NC}`);
Expand Down
14 changes: 14 additions & 0 deletions cli/plugin-manifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, test, expect } from "bun:test";
import { readFileSync } from "fs";
import { join } from "path";

describe("Claude plugin manifest", () => {
test("uses the bundled Node server instead of Bun source execution", () => {
const manifest = JSON.parse(
readFileSync(join(import.meta.dir, "..", ".claude-plugin", "plugin.json"), "utf8"),
);

expect(manifest.mcpServers.petsonality.command).toBe("node");
expect(manifest.mcpServers.petsonality.args).toEqual(["dist/server.js"]);
});
});
26 changes: 26 additions & 0 deletions cli/statusline-art.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,29 @@ describe("PowerShell statusline", () => {
}
});
});

describe("golden retriever statusline actions", () => {
test("bash renderer maps lick and spin to the redesigned golden frames", () => {
const script = readFileSync(join(STATUSLINE_DIR, "pet-status.sh"), "utf8");

expect(script).toContain("lick) if [ $(( (ACT_STEP / 5) % 2 )) -eq 0 ]; then FRAME=5; else FRAME=6; fi ;;");
expect(script).toContain("spin) if [ $(( (ACT_STEP / 5) % 2 )) -eq 0 ]; then FRAME=7; else FRAME=0; fi ;;");
expect(script).toContain('FRAME=7; echo "ACT_TYPE=spin;');
});

test("PowerShell renderer maps lick and spin to the redesigned golden frames", () => {
const script = readFileSync(join(STATUSLINE_DIR, "pet-status.ps1"), "utf8");

expect(script).toContain('"lick" { if (([math]::Floor($a.Step / 5) % 2) -eq 0) { $frame = 5 } else { $frame = 6 } }');
expect(script).toContain('default { if (([math]::Floor($a.Step / 5) % 2) -eq 0) { $frame = 7 } else { $frame = 0 } }');
expect(script).toContain('Start-Action ".gold_act" "spin" (Get-Random -Minimum 80 -Maximum 100) 0; return 7');
});

test("golden uses its explicit blink frame instead of generic eye replacement", () => {
const shell = readFileSync(join(STATUSLINE_DIR, "pet-status.sh"), "utf8");
const ps = readFileSync(join(STATUSLINE_DIR, "pet-status.ps1"), "utf8");

expect(shell).toContain("raven|owl|bear|golden) FRAME=2; BLINK=0 ;;");
expect(ps).toContain('$PetId -eq "golden"');
});
});
Loading
Loading