@ramarivera/coding-buddy 0.4.0-alpha.8 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
- package/{adapters/claude/plugin → .claude-plugin}/plugin.json +9 -7
- package/LICENSE +1 -1
- package/README.md +102 -72
- package/adapters/omp/commands.ts +201 -0
- package/adapters/omp/context.ts +22 -0
- package/adapters/omp/events.ts +551 -0
- package/adapters/omp/identity.ts +10 -0
- package/adapters/omp/index.ts +39 -0
- package/adapters/omp/logger.ts +52 -0
- package/adapters/omp/prompt.ts +60 -0
- package/adapters/omp/renderers.ts +73 -0
- package/adapters/omp/storage.ts +25 -0
- package/adapters/omp/ui.ts +85 -0
- package/adapters/pi/commands.ts +64 -37
- package/adapters/pi/events.ts +147 -72
- package/adapters/pi/index.ts +9 -4
- package/adapters/pi/storage.ts +14 -285
- package/adapters/pi/ui.ts +56 -5
- package/adapters/shared/file-storage.ts +526 -0
- package/{adapters/claude/install → cli}/backup.ts +59 -44
- package/cli/biomes.ts +309 -0
- package/cli/buddy-shell.ts +818 -0
- package/{adapters/claude/install → cli}/disable.ts +14 -32
- package/{adapters/claude/install → cli}/doctor.ts +55 -57
- package/{adapters/claude/install → cli}/hunt.ts +7 -9
- package/cli/index.ts +36 -25
- package/{adapters/claude/install → cli}/install.ts +100 -145
- package/{adapters/claude/install → cli}/pick.ts +111 -50
- package/{adapters/claude/install → cli}/settings.ts +2 -2
- package/{adapters/claude/install → cli}/show.ts +4 -4
- package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
- package/{adapters/claude/install → cli}/test-statusline.ts +11 -11
- package/cli/tui.tsx +2333 -0
- package/{adapters/claude/install → cli}/uninstall.ts +23 -38
- package/cli/upgrade.ts +213 -0
- package/cli/validate-species.ts +209 -0
- package/cli/verify.ts +3 -3
- package/core/achievements.ts +12 -20
- package/core/art-data.ts +26 -0
- package/core/command-service.ts +99 -97
- package/core/engine.ts +83 -1
- package/core/ports.ts +10 -0
- package/core/reactions.ts +533 -95
- package/core/slot-slug.ts +22 -0
- package/{adapters/claude/hooks → hooks}/buddy-comment.sh +23 -2
- package/hooks/file-type-react.sh +401 -0
- package/hooks/hooks.json +52 -0
- package/hooks/mood-react.sh +208 -0
- package/{adapters/claude/hooks → hooks}/name-react.sh +5 -2
- package/hooks/react.sh +1368 -0
- package/hooks/suggest.sh +55 -0
- package/package.json +113 -59
- package/scripts/gen-emoji-widths.ts +34 -0
- package/scripts/paths.sh +56 -0
- package/scripts/postinstall.cjs +42 -0
- package/scripts/snapshot-statusline.sh +54 -0
- package/server/achievements.ts +1419 -0
- package/server/art.ts +450 -0
- package/server/award-xp.ts +48 -0
- package/server/check-suggestions.ts +102 -0
- package/server/consolidate.ts +33 -0
- package/server/index.ts +1340 -0
- package/server/mcp-launcher.sh +26 -0
- package/server/memory.ts +404 -0
- package/server/mood.ts +254 -0
- package/server/path.ts +82 -0
- package/server/shift-mood.ts +43 -0
- package/{adapters/claude/storage → server}/state.ts +200 -7
- package/server/suggestions.ts +335 -0
- package/server/theme.ts +53 -0
- package/server/xp.ts +435 -0
- package/{adapters/claude/skills → skills}/buddy/SKILL.md +48 -4
- package/statusline/buddy-status.sh +397 -0
- package/statusline/combined-status.sh +134 -0
- package/statusline/emoji-widths.data +4 -0
- package/adapters/claude/hooks/hooks.json +0 -35
- package/adapters/claude/hooks/react.sh +0 -204
- package/adapters/claude/popup/buddy-popup.sh +0 -92
- package/adapters/claude/popup/buddy-render.sh +0 -540
- package/adapters/claude/popup/popup-manager.sh +0 -355
- package/adapters/claude/rendering/art.ts +0 -264
- package/adapters/claude/server/index.ts +0 -752
- package/adapters/claude/server/instructions.ts +0 -24
- package/adapters/claude/server/resources.ts +0 -38
- package/adapters/claude/statusline/buddy-status.sh +0 -389
- package/adapters/claude/storage/achievements.ts +0 -253
- package/adapters/claude/storage/identity.ts +0 -13
- package/adapters/claude/storage/paths.ts +0 -24
- package/adapters/claude/storage/settings.ts +0 -41
- package/adapters/pi/README.md +0 -83
- package/adapters/pi/tools.ts +0 -6
|
@@ -1,31 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* coding-buddy uninstall — remove all integrations
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { readFileSync, writeFileSync, existsSync, rmSync, readdirSync } from "fs";
|
|
6
6
|
import { join } from "path";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface HookMatcherEntry {
|
|
16
|
-
matcher?: string;
|
|
17
|
-
hooks?: HookCommand[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface ClaudeSettings {
|
|
21
|
-
statusLine?: { command?: string };
|
|
22
|
-
hooks?: Record<string, HookMatcherEntry[]>;
|
|
23
|
-
[key: string]: unknown;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function hasBuddyHook(entry: HookMatcherEntry): boolean {
|
|
27
|
-
return (entry.hooks ?? []).some((hook) => hook.command.includes("claude-buddy"));
|
|
28
|
-
}
|
|
7
|
+
import {
|
|
8
|
+
buddyStateDir,
|
|
9
|
+
claudeSettingsPath,
|
|
10
|
+
claudeSkillDir,
|
|
11
|
+
claudeUserConfigPath,
|
|
12
|
+
} from "../server/path.ts";
|
|
29
13
|
|
|
30
14
|
const GREEN = "\x1b[32m";
|
|
31
15
|
const YELLOW = "\x1b[33m";
|
|
@@ -34,13 +18,12 @@ const NC = "\x1b[0m";
|
|
|
34
18
|
function ok(msg: string) { console.log(`${GREEN}✓${NC} ${msg}`); }
|
|
35
19
|
function warn(msg: string) { console.log(`${YELLOW}⚠${NC} ${msg}`); }
|
|
36
20
|
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const STATE_DIR = join(homedir(), ".claude-buddy");
|
|
21
|
+
const SETTINGS_FILE = claudeSettingsPath();
|
|
22
|
+
const SKILL_DIR = claudeSkillDir("buddy");
|
|
23
|
+
const STATE_DIR = buddyStateDir();
|
|
24
|
+
const CLAUDE_JSON_PATH = claudeUserConfigPath();
|
|
42
25
|
|
|
43
|
-
console.log("\
|
|
26
|
+
console.log("\ncoding-buddy uninstall\n");
|
|
44
27
|
|
|
45
28
|
// Stop all popup reopen loops and close any running popup
|
|
46
29
|
try {
|
|
@@ -69,22 +52,22 @@ try {
|
|
|
69
52
|
ok("Popup stopped");
|
|
70
53
|
} catch { /* not in tmux or no popup */ }
|
|
71
54
|
|
|
72
|
-
// Remove MCP server from
|
|
55
|
+
// Remove MCP server from Claude's user config
|
|
73
56
|
try {
|
|
74
|
-
const claudeJson = JSON.parse(readFileSync(
|
|
57
|
+
const claudeJson = JSON.parse(readFileSync(CLAUDE_JSON_PATH, "utf8"));
|
|
75
58
|
if (claudeJson.mcpServers?.["claude-buddy"]) {
|
|
76
59
|
delete claudeJson.mcpServers["claude-buddy"];
|
|
77
60
|
if (Object.keys(claudeJson.mcpServers).length === 0) delete claudeJson.mcpServers;
|
|
78
|
-
writeFileSync(
|
|
79
|
-
ok(`MCP server removed from ${
|
|
61
|
+
writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(claudeJson, null, 2));
|
|
62
|
+
ok(`MCP server removed from ${CLAUDE_JSON_PATH}`);
|
|
80
63
|
}
|
|
81
64
|
} catch {
|
|
82
|
-
warn(`Could not update ${
|
|
65
|
+
warn(`Could not update ${CLAUDE_JSON_PATH}`);
|
|
83
66
|
}
|
|
84
67
|
|
|
85
68
|
// Remove hooks and statusline from settings.json
|
|
86
69
|
try {
|
|
87
|
-
const settings = JSON.parse(readFileSync(SETTINGS_FILE, "utf8"))
|
|
70
|
+
const settings = JSON.parse(readFileSync(SETTINGS_FILE, "utf8"));
|
|
88
71
|
let changed = false;
|
|
89
72
|
|
|
90
73
|
if (settings.statusLine?.command?.includes("buddy")) {
|
|
@@ -93,11 +76,11 @@ try {
|
|
|
93
76
|
changed = true;
|
|
94
77
|
}
|
|
95
78
|
|
|
96
|
-
for (const hookType of ["PostToolUse", "Stop", "SessionStart", "SessionEnd"] as const) {
|
|
79
|
+
for (const hookType of ["PostToolUse", "Stop", "SessionStart", "SessionEnd", "UserPromptSubmit"] as const) {
|
|
97
80
|
if (settings.hooks?.[hookType]) {
|
|
98
81
|
const before = settings.hooks[hookType].length;
|
|
99
82
|
settings.hooks[hookType] = settings.hooks[hookType].filter(
|
|
100
|
-
(h) => !
|
|
83
|
+
(h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("coding-buddy") || hh.command?.includes("claude-buddy")),
|
|
101
84
|
);
|
|
102
85
|
if (settings.hooks[hookType].length < before) {
|
|
103
86
|
ok(`${hookType} hooks removed`);
|
|
@@ -123,7 +106,9 @@ if (existsSync(SKILL_DIR)) {
|
|
|
123
106
|
warn("Skill not found (already removed)");
|
|
124
107
|
}
|
|
125
108
|
|
|
126
|
-
// Keep state dir (companion data) — user might want it back
|
|
109
|
+
// Keep state dir (companion data) — user might want it back.
|
|
110
|
+
// This is profile-scoped when CLAUDE_CONFIG_DIR is set, so other
|
|
111
|
+
// profiles' buddies are unaffected.
|
|
127
112
|
if (existsSync(STATE_DIR)) {
|
|
128
113
|
warn(`Companion data kept at ${STATE_DIR} — delete manually if not needed`);
|
|
129
114
|
}
|
package/cli/upgrade.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { join, resolve, dirname } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
|
|
8
|
+
const CYAN = "\x1b[36m";
|
|
9
|
+
const GREEN = "\x1b[32m";
|
|
10
|
+
const YELLOW = "\x1b[33m";
|
|
11
|
+
const RED = "\x1b[31m";
|
|
12
|
+
const BOLD = "\x1b[1m";
|
|
13
|
+
const DIM = "\x1b[2m";
|
|
14
|
+
const NC = "\x1b[0m";
|
|
15
|
+
|
|
16
|
+
const PROJECT_ROOT = resolve(dirname(import.meta.dir));
|
|
17
|
+
|
|
18
|
+
function ok(msg: string) { console.log(`${GREEN}✓${NC} ${msg}`); }
|
|
19
|
+
function info(msg: string) { console.log(`${CYAN}→${NC} ${msg}`); }
|
|
20
|
+
function warn(msg: string) { console.log(`${YELLOW}⚠${NC} ${msg}`); }
|
|
21
|
+
function err(msg: string) { console.log(`${RED}✗${NC} ${msg}`); }
|
|
22
|
+
|
|
23
|
+
function tryExec(cmd: string, fallback = ""): string {
|
|
24
|
+
try {
|
|
25
|
+
return execSync(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
26
|
+
} catch {
|
|
27
|
+
return fallback;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getCurrentVersion(): string {
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse(readFileSync(join(PROJECT_ROOT, "package.json"), "utf8"));
|
|
34
|
+
return pkg.version ?? "unknown";
|
|
35
|
+
} catch {
|
|
36
|
+
return "unknown";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function banner() {
|
|
41
|
+
console.log(`
|
|
42
|
+
${CYAN}╔══════════════════════════════════════════════════════════╗${NC}
|
|
43
|
+
${CYAN}║${NC} ${BOLD}coding-buddy upgrade${NC} ${CYAN}║${NC}
|
|
44
|
+
${CYAN}╚══════════════════════════════════════════════════════════╝${NC}
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function checkGitRepo(): boolean {
|
|
49
|
+
const isRepo = tryExec("git rev-parse --is-inside-work-tree 2>/dev/null");
|
|
50
|
+
if (isRepo !== "true") {
|
|
51
|
+
err("Not inside a git repository. Upgrade requires a git clone of coding-buddy.");
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getRemoteBranch(): string {
|
|
58
|
+
const branch = tryExec("git rev-parse --abbrev-ref HEAD 2>/dev/null", "main");
|
|
59
|
+
return branch === "HEAD" ? "main" : branch;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function checkForUpdates(branch: string): { hasUpdate: boolean; local: string; remote: string; commits: string[] } {
|
|
63
|
+
info("Fetching latest from remote...\n");
|
|
64
|
+
try {
|
|
65
|
+
execSync("git fetch --quiet 2>/dev/null", { cwd: PROJECT_ROOT, stdio: "ignore" });
|
|
66
|
+
} catch {
|
|
67
|
+
warn("git fetch failed — proceeding with cached remote state");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const local = tryExec("git rev-parse HEAD 2>/dev/null");
|
|
71
|
+
const upstream = tryExec(`git rev-parse '@{upstream}' 2>/dev/null`);
|
|
72
|
+
const remote = upstream || tryExec(`git rev-parse origin/${branch} 2>/dev/null`);
|
|
73
|
+
|
|
74
|
+
if (!local || !remote) {
|
|
75
|
+
warn("Could not determine remote HEAD — no tracking branch configured");
|
|
76
|
+
info(`To set one: git branch --set-upstream-to=origin/${branch} ${branch}`);
|
|
77
|
+
return { hasUpdate: false, local, remote, commits: [] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (local === remote) {
|
|
81
|
+
return { hasUpdate: false, local, remote, commits: [] };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const commits = tryExec(
|
|
85
|
+
`git log --oneline ${local}..origin/${branch} 2>/dev/null`,
|
|
86
|
+
).split("\n").filter(Boolean);
|
|
87
|
+
|
|
88
|
+
return { hasUpdate: true, local, remote, commits };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function pullLatest(branch: string): boolean {
|
|
92
|
+
info(`Pulling latest from origin/${branch}...`);
|
|
93
|
+
try {
|
|
94
|
+
const output = execSync(`git pull --ff-only origin ${branch} 2>&1`, {
|
|
95
|
+
cwd: PROJECT_ROOT,
|
|
96
|
+
encoding: "utf8",
|
|
97
|
+
});
|
|
98
|
+
ok("Git pull successful");
|
|
99
|
+
return true;
|
|
100
|
+
} catch (e: any) {
|
|
101
|
+
err("git pull failed — you may have local changes that conflict");
|
|
102
|
+
err(e.message?.split("\n")[0] || "unknown error");
|
|
103
|
+
info("Stash or commit your local changes, then re-run upgrade");
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function installDeps(): boolean {
|
|
109
|
+
info("Installing dependencies...");
|
|
110
|
+
try {
|
|
111
|
+
execSync("bun install 2>&1", { cwd: PROJECT_ROOT, stdio: "ignore" });
|
|
112
|
+
ok("Dependencies installed");
|
|
113
|
+
return true;
|
|
114
|
+
} catch {
|
|
115
|
+
err("bun install failed");
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function reinstallBuddy(): boolean {
|
|
121
|
+
info("Re-running install-buddy to update integrations...\n");
|
|
122
|
+
try {
|
|
123
|
+
execSync("bun run install-buddy 2>&1", { cwd: PROJECT_ROOT, stdio: "inherit" });
|
|
124
|
+
return true;
|
|
125
|
+
} catch {
|
|
126
|
+
err("install-buddy failed");
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function printSummary(oldVersion: string, commits: string[]) {
|
|
132
|
+
const newVersion = getCurrentVersion();
|
|
133
|
+
|
|
134
|
+
console.log("");
|
|
135
|
+
console.log(`${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}`);
|
|
136
|
+
console.log(`${GREEN} Upgrade complete!${NC}`);
|
|
137
|
+
console.log(`${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}`);
|
|
138
|
+
console.log("");
|
|
139
|
+
console.log(` ${BOLD}Version:${NC} ${oldVersion} → ${BOLD}${newVersion}${NC}`);
|
|
140
|
+
|
|
141
|
+
if (commits.length > 0) {
|
|
142
|
+
console.log(` ${BOLD}Changes:${NC}`);
|
|
143
|
+
const display = commits.slice(0, 15);
|
|
144
|
+
for (const c of display) {
|
|
145
|
+
console.log(` ${DIM}${c}${NC}`);
|
|
146
|
+
}
|
|
147
|
+
if (commits.length > 15) {
|
|
148
|
+
console.log(` ${DIM}... and ${commits.length - 15} more${NC}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log(`${DIM} Restart Claude Code for changes to take effect.${NC}`);
|
|
154
|
+
console.log("");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const args = process.argv.slice(2);
|
|
158
|
+
const checkOnly = args.includes("--check");
|
|
159
|
+
|
|
160
|
+
banner();
|
|
161
|
+
|
|
162
|
+
const oldVersion = getCurrentVersion();
|
|
163
|
+
info(`Current version: ${oldVersion}\n`);
|
|
164
|
+
|
|
165
|
+
if (!checkGitRepo()) {
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const branch = getRemoteBranch();
|
|
170
|
+
const { hasUpdate, commits } = checkForUpdates(branch);
|
|
171
|
+
|
|
172
|
+
if (!hasUpdate) {
|
|
173
|
+
ok(`Already up to date (v${oldVersion})`);
|
|
174
|
+
console.log("");
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (checkOnly) {
|
|
179
|
+
warn(`Update available! ${commits.length} new commit${commits.length === 1 ? "" : "s"}:`);
|
|
180
|
+
for (const c of commits.slice(0, 10)) {
|
|
181
|
+
console.log(` ${DIM}${c}${NC}`);
|
|
182
|
+
}
|
|
183
|
+
if (commits.length > 10) {
|
|
184
|
+
console.log(` ${DIM}... and ${commits.length - 10} more${NC}`);
|
|
185
|
+
}
|
|
186
|
+
console.log("");
|
|
187
|
+
info("Run without --check to apply the update");
|
|
188
|
+
console.log("");
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
info(`${commits.length} new commit${commits.length === 1 ? "" : ""} available:`);
|
|
193
|
+
for (const c of commits.slice(0, 10)) {
|
|
194
|
+
console.log(` ${DIM}${c}${NC}`);
|
|
195
|
+
}
|
|
196
|
+
if (commits.length > 10) {
|
|
197
|
+
console.log(` ${DIM}... and ${commits.length - 10} more${NC}`);
|
|
198
|
+
}
|
|
199
|
+
console.log("");
|
|
200
|
+
|
|
201
|
+
if (!pullLatest(branch)) {
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!installDeps()) {
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!reinstallBuddy()) {
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
printSummary(oldVersion, commits);
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Validate species art format.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun run cli/validate-species.ts <path>
|
|
7
|
+
* bun run cli/validate-species.ts --check-species <name>
|
|
8
|
+
*
|
|
9
|
+
* Exits 0 if valid, 1 if invalid (with error messages to stderr).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
|
|
15
|
+
interface ValidationResult {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
errors: string[];
|
|
18
|
+
warnings: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const EYE_PLACEHOLDER = "{E}";
|
|
22
|
+
|
|
23
|
+
/** Check a string for ANSI escape sequences */
|
|
24
|
+
function hasAnsiCodes(s: string): boolean {
|
|
25
|
+
return s.includes("\x1b[");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Strip ANSI codes from a string */
|
|
29
|
+
function stripAnsi(s: string): string {
|
|
30
|
+
return s.replace(/\x1b\[[^m]*m/g, "");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Validate a single art frame (must be exactly 5 lines) */
|
|
34
|
+
function validateFrame(
|
|
35
|
+
species: string,
|
|
36
|
+
frameIndex: number,
|
|
37
|
+
lines: string[],
|
|
38
|
+
): ValidationResult {
|
|
39
|
+
const errors: string[] = [];
|
|
40
|
+
const warnings: string[] = [];
|
|
41
|
+
|
|
42
|
+
if (lines.length !== 5) {
|
|
43
|
+
errors.push(
|
|
44
|
+
` Species "${species}" frame ${frameIndex}: expected 5 lines, got ${lines.length}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
const line = lines[i];
|
|
50
|
+
const rawLen = line.length;
|
|
51
|
+
const displayLen = stripAnsi(line).length;
|
|
52
|
+
|
|
53
|
+
if (hasAnsiCodes(line)) {
|
|
54
|
+
errors.push(
|
|
55
|
+
` Species "${species}" frame ${frameIndex} line ${i + 1}: contains ANSI escape sequences (not allowed)`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (displayLen > 14) {
|
|
60
|
+
errors.push(
|
|
61
|
+
` Species "${species}" frame ${frameIndex} line ${i + 1}: display width ${displayLen} exceeds max 14 chars`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Each species art should have the eye placeholder in at least one line
|
|
66
|
+
// (not strictly required but warn if missing)
|
|
67
|
+
if (!line.includes(EYE_PLACEHOLDER) && i === 1) {
|
|
68
|
+
warnings.push(
|
|
69
|
+
` Species "${species}" frame ${frameIndex} line ${i + 1}: missing ${EYE_PLACEHOLDER} eye placeholder (recommended for line 2)`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Validate art structure from parsed JSON */
|
|
78
|
+
function validateSpeciesArt(
|
|
79
|
+
species: string,
|
|
80
|
+
art: string[][],
|
|
81
|
+
): ValidationResult {
|
|
82
|
+
const errors: string[] = [];
|
|
83
|
+
const warnings: string[] = [];
|
|
84
|
+
|
|
85
|
+
if (!Array.isArray(art)) {
|
|
86
|
+
errors.push(`Species "${species}": art must be an array of frames`);
|
|
87
|
+
return { valid: false, errors, warnings };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (art.length !== 3) {
|
|
91
|
+
errors.push(
|
|
92
|
+
`Species "${species}": expected 3 animation frames, got ${art.length}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < art.length; i++) {
|
|
97
|
+
const frame = art[i];
|
|
98
|
+
const result = validateFrame(species, i, frame);
|
|
99
|
+
errors.push(...result.errors);
|
|
100
|
+
warnings.push(...result.warnings);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Validate a face template — must contain at least one eye placeholder */
|
|
107
|
+
function validateFaceTemplate(
|
|
108
|
+
species: string,
|
|
109
|
+
face: string,
|
|
110
|
+
): ValidationResult {
|
|
111
|
+
const errors: string[] = [];
|
|
112
|
+
const warnings: string[] = [];
|
|
113
|
+
|
|
114
|
+
if (face.length > 20) {
|
|
115
|
+
errors.push(
|
|
116
|
+
`Species "${species}" face template: display width ${face.length} exceeds max 20 chars`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!face.includes(EYE_PLACEHOLDER)) {
|
|
121
|
+
errors.push(
|
|
122
|
+
`Species "${species}" face template: missing ${EYE_PLACEHOLDER} eye placeholder`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Main validation function — validates a species JSON file or inline art.
|
|
131
|
+
* Returns exit code (0 = valid, 1 = invalid).
|
|
132
|
+
*/
|
|
133
|
+
export function validateSpeciesFile(filePath: string): ValidationResult {
|
|
134
|
+
const errors: string[] = [];
|
|
135
|
+
const warnings: string[] = [];
|
|
136
|
+
|
|
137
|
+
let data: Record<string, unknown>;
|
|
138
|
+
try {
|
|
139
|
+
const raw = readFileSync(filePath, "utf8");
|
|
140
|
+
data = JSON.parse(raw);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
return {
|
|
143
|
+
valid: false,
|
|
144
|
+
errors: [`Failed to read or parse JSON file: ${filePath}`],
|
|
145
|
+
warnings: [],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const species = data.name as string | undefined;
|
|
150
|
+
if (!species || typeof species !== "string") {
|
|
151
|
+
errors.push("Species must have a 'name' field (string)");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const art = data.art as string[][] | undefined;
|
|
155
|
+
if (!art) {
|
|
156
|
+
errors.push("Species must have an 'art' field (string[][] — 3 frames × 5 lines)");
|
|
157
|
+
} else {
|
|
158
|
+
const artResult = validateSpeciesArt(species ?? "unknown", art);
|
|
159
|
+
errors.push(...artResult.errors);
|
|
160
|
+
warnings.push(...artResult.warnings);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const face = data.face as string | undefined;
|
|
164
|
+
if (face) {
|
|
165
|
+
const faceResult = validateFaceTemplate(species ?? "unknown", face);
|
|
166
|
+
errors.push(...faceResult.errors);
|
|
167
|
+
warnings.push(...faceResult.warnings);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Print results and exit with appropriate code */
|
|
174
|
+
function main(): never {
|
|
175
|
+
const args = process.argv.slice(2);
|
|
176
|
+
|
|
177
|
+
if (args.length === 0) {
|
|
178
|
+
console.error("Usage: bun run cli/validate-species.ts <path-to-species-json>");
|
|
179
|
+
console.error(" Validates art format, ANSI codes, and eye placeholder.");
|
|
180
|
+
console.error("");
|
|
181
|
+
console.error("Example:");
|
|
182
|
+
console.error(" bun run cli/validate-species.ts ./my-species.json");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const filePath = args[0].startsWith("/") ? args[0] : join(process.cwd(), args[0]);
|
|
187
|
+
const result = validateSpeciesFile(filePath);
|
|
188
|
+
|
|
189
|
+
if (result.warnings.length > 0) {
|
|
190
|
+
console.warn("Warnings:");
|
|
191
|
+
for (const w of result.warnings) {
|
|
192
|
+
console.warn(w);
|
|
193
|
+
}
|
|
194
|
+
console.warn("");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (result.valid) {
|
|
198
|
+
console.log(`✓ ${args[0]}: valid`);
|
|
199
|
+
process.exit(0);
|
|
200
|
+
} else {
|
|
201
|
+
console.error("Errors:");
|
|
202
|
+
for (const e of result.errors) {
|
|
203
|
+
console.error(e);
|
|
204
|
+
}
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
main();
|
package/cli/verify.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* coding-buddy verify — show what buddy a user ID produces
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { generateBones, renderBuddy, STAT_NAMES } from "../core/engine.ts"
|
|
6
|
-
import { resolveUserId } from "../
|
|
5
|
+
import { generateBones, renderBuddy, STAT_NAMES } from "../core/engine.ts"
|
|
6
|
+
import { resolveUserId } from "../server/state.ts";
|
|
7
7
|
|
|
8
8
|
const userId = process.argv[3] || resolveUserId();
|
|
9
9
|
|
package/core/achievements.ts
CHANGED
|
@@ -1,24 +1,6 @@
|
|
|
1
|
-
import type { UnlockedAchievement } from "./model.ts";
|
|
1
|
+
import type { UnlockedAchievement, GlobalCounters, SlotCounters, EventCounters } from "./model.ts";
|
|
2
|
+
export type { GlobalCounters, SlotCounters, EventCounters };
|
|
2
3
|
|
|
3
|
-
export interface GlobalCounters {
|
|
4
|
-
errors_seen: number;
|
|
5
|
-
tests_failed: number;
|
|
6
|
-
large_diffs: number;
|
|
7
|
-
sessions: number;
|
|
8
|
-
commands_run: number;
|
|
9
|
-
days_active: number;
|
|
10
|
-
turns: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface SlotCounters {
|
|
14
|
-
pets: number;
|
|
15
|
-
reactions_given: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface EventCounters extends GlobalCounters {
|
|
19
|
-
pets: number;
|
|
20
|
-
reactions_given: number;
|
|
21
|
-
}
|
|
22
4
|
|
|
23
5
|
export const GLOBAL_KEYS: (keyof GlobalCounters)[] = [
|
|
24
6
|
"errors_seen", "tests_failed", "large_diffs",
|
|
@@ -189,12 +171,22 @@ export function getUnlockedAchievements(
|
|
|
189
171
|
): Achievement[] {
|
|
190
172
|
const newlyUnlocked: Achievement[] = [];
|
|
191
173
|
for (const ach of ACHIEVEMENTS) {
|
|
174
|
+
if (ach.id === "first_steps") continue;
|
|
192
175
|
if (unlockedIds.has(ach.id)) continue;
|
|
193
176
|
if (ach.check(events)) newlyUnlocked.push(ach);
|
|
194
177
|
}
|
|
195
178
|
return newlyUnlocked;
|
|
196
179
|
}
|
|
197
180
|
|
|
181
|
+
export function getCreationAchievements(
|
|
182
|
+
_events: EventCounters,
|
|
183
|
+
unlockedIds: Set<string>,
|
|
184
|
+
): Achievement[] {
|
|
185
|
+
if (unlockedIds.has("first_steps")) return [];
|
|
186
|
+
const first = ACHIEVEMENTS.find((ach) => ach.id === "first_steps");
|
|
187
|
+
return first ? [first] : [];
|
|
188
|
+
}
|
|
189
|
+
|
|
198
190
|
export function isAchievementUnlocked(
|
|
199
191
|
id: string,
|
|
200
192
|
unlocked: UnlockedAchievement[],
|
package/core/art-data.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Species, Hat } from "./engine.ts";
|
|
2
2
|
|
|
3
|
+
// ─── Species art: 3 frames × 5 lines each ──────────────────────────────────
|
|
4
|
+
|
|
3
5
|
export const SPECIES_ART: Record<Species, string[][]> = {
|
|
4
6
|
duck: [
|
|
5
7
|
[" ", " __ ", " <({E} )___ ", " ( ._> ", " `--' "],
|
|
@@ -91,8 +93,32 @@ export const SPECIES_ART: Record<Species, string[][]> = {
|
|
|
91
93
|
[" ", " /\\ /| ", " ( {E} {E} ) ", " ( .. ) ", " `------' "],
|
|
92
94
|
[" ", " /\\ /\\ ", " ( {E} {E} ) ", " ( .. ) ", " `------'~ "],
|
|
93
95
|
],
|
|
96
|
+
wyvern: [
|
|
97
|
+
["} {",
|
|
98
|
+
"|\\^```^/|",
|
|
99
|
+
"\\ {E}' '{E} /",
|
|
100
|
+
" ≈(° °)≈",
|
|
101
|
+
" '-'"],
|
|
102
|
+
["} {",
|
|
103
|
+
"|\\^```^/|",
|
|
104
|
+
"\\ {E}' '{E} /",
|
|
105
|
+
" ≈(° °)≈",
|
|
106
|
+
" '~'"],
|
|
107
|
+
["} {",
|
|
108
|
+
"|\\^```^/|",
|
|
109
|
+
"\\ {E}' '{E} /",
|
|
110
|
+
" ≈(° °)≈",
|
|
111
|
+
" 'v'"],
|
|
112
|
+
],
|
|
113
|
+
pikachu: [
|
|
114
|
+
[" ", " /\\_/\\ ", " ({E} {E}) ", " ( ω ) ", " (__) "],
|
|
115
|
+
[" ", " /\\_/\\ ", " (- -) ", " ( ω ) ", " (__) "],
|
|
116
|
+
[" ", " /\\_/\\ ", " ({E} {E}) ", " ( ~ ) ", " (__) "],
|
|
117
|
+
],
|
|
94
118
|
};
|
|
95
119
|
|
|
120
|
+
// ─── Hat art ────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
96
122
|
export const HAT_ART: Record<Hat, string> = {
|
|
97
123
|
none: "",
|
|
98
124
|
crown: " \\^^^/ ",
|