Initial commit: pokebuddy_cli

This commit is contained in:
Felipe Domingues 2026-04-25 23:14:38 -03:00
commit d58df9bd10
47 changed files with 1684 additions and 0 deletions

31
src/ansi.js Normal file
View file

@ -0,0 +1,31 @@
export const RESET = '\x1b[0m';
export const BOLD = '\x1b[1m';
export const DIM = '\x1b[2m';
export const palette = {
green: '\x1b[38;2;0;255;144m',
cyan: '\x1b[38;2;0;230;255m',
yellow: '\x1b[38;2;255;214;10m',
magenta: '\x1b[38;2;255;77;255m',
orange: '\x1b[38;2;255;106;61m',
purple: '\x1b[38;2;167;139;250m',
gray: '\x1b[38;2;155;163;175m',
white: '\x1b[38;2;230;237;243m',
red: '\x1b[38;2;255;90;90m'
};
export function paint(text, color = 'white') {
return `${palette[color] ?? ''}${text}${RESET}`;
}
export function strong(text, color = 'white') {
return `${BOLD}${palette[color] ?? ''}${text}${RESET}`;
}
export function dim(text) {
return `${DIM}${text}${RESET}`;
}
export function stripAnsi(value) {
return String(value).replace(/\x1b\[[0-9;]*m/g, '');
}

184
src/cli.js Normal file
View file

@ -0,0 +1,184 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { creatures, getCreatureByName } from './creatures.js';
import { nextCreature } from './hatch.js';
import { addEvent, ensureCompanionState, loadState, resetState, saveState } from './state.js';
import { banner, boxed, compactCreatureLine, creatureCard, dex, helpText, legalText, listCompanions, statusView } from './render.js';
import { paint, strong, dim } from './ansi.js';
const dirname = path.dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(fs.readFileSync(path.join(dirname, '..', 'package.json'), 'utf8'));
export async function run(argv) {
const [command = 'help', ...args] = argv;
switch (command) {
case 'hatch':
return hatch(args);
case 'list':
return output(listCompanions(loadState()));
case 'dex':
return output(`${strong('PokeBuddy Creature Dex', 'green')}\n${dex()}`);
case 'show':
return show(args);
case 'poke':
return interact('poke', args);
case 'feed':
return interact('feed', args);
case 'status':
return output(statusView(loadState()));
case 'rename':
return rename(args);
case 'banner':
return output(banner());
case 'legal':
return output(legalText());
case 'reset':
return reset(args);
case '--version':
case '-v':
case 'version':
return output(pkg.version);
case '--help':
case '-h':
case 'help':
return output(helpText(pkg.version));
default:
return output(`${paint(`Unknown command: ${command}`, 'red')}\n\n${helpText(pkg.version)}`, 1);
}
}
function hatch(args) {
const state = loadState();
const force = args.includes('--force');
const all = args.includes('--all');
if (all) {
for (const creature of creatures) {
ensureCompanionState(state, creature);
}
state.active = state.active ?? creatures[0].id;
addEvent(state, 'hatch:all', { count: creatures.length });
saveState(state);
return output(`${boxed('All companions joined your terminal.', 'green')}\n\n${listCompanions(state)}`);
}
const discoveredIds = Object.keys(state.companions);
if (discoveredIds.length > 0 && !force) {
return output(`${paint('You already have a companion.', 'yellow')} Use ${strong('pokebuddy hatch --force', 'green')} to discover another.\n\n${listCompanions(state)}`);
}
const creature = nextCreature(discoveredIds);
if (!creature) {
return output(`${paint('Every known companion has already joined you.', 'yellow')}\n\n${listCompanions(state)}`);
}
const entry = ensureCompanionState(state, creature);
state.active = creature.id;
addEvent(state, 'hatch', { id: creature.id });
saveState(state);
return output(
`${boxed('The terminal hums. A companion signal appears.', creature.color)}\n\n` +
`${strong(`You hatched ${creature.name}!`, creature.color)}\n\n` +
`${creatureCard(creature, entry)}`
);
}
function show(args) {
const name = args[0];
const state = loadState();
if (!name) {
if (!state.active) return output(`Run ${strong('pokebuddy hatch', 'green')} first.`);
const active = state.companions[state.active];
const creature = getCreatureByName(active.id);
return output(creatureCard(creature, active));
}
const creature = getCreatureByName(name);
if (!creature) return output(notFound(name), 1);
return output(creatureCard(creature, state.companions[creature.id] ?? null));
}
function interact(kind, args) {
const maybeName = args[0];
const state = loadState();
let creature = maybeName ? getCreatureByName(maybeName) : null;
if (!creature && state.active) {
creature = getCreatureByName(state.active);
}
if (!creature) {
return output(`No companion selected. Run ${strong('pokebuddy hatch', 'green')} first.`, 1);
}
const entry = ensureCompanionState(state, creature);
const item = kind === 'feed' ? args.slice(1).join(' ') || 'context crumbs' : null;
if (kind === 'poke') {
entry.pokes += 1;
entry.energy = Math.min(100, entry.energy + 10);
entry.xp += 12;
entry.mood = entry.energy > 85 ? 'overclocked' : 'happy';
}
if (kind === 'feed') {
entry.feeds += 1;
entry.energy = Math.min(100, entry.energy + 6);
entry.xp += 18;
entry.mood = 'focused';
}
entry.level = 1 + Math.floor(entry.xp / 100);
state.active = creature.id;
addEvent(state, kind, { id: creature.id, item });
saveState(state);
const reaction = kind === 'feed'
? `${creature.reactions.feed}\n${dim(`Offering: ${item}`)}`
: creature.reactions.poke;
return output(
`${strong(`$ pokebuddy ${kind} ${creature.id}`, 'green')}\n` +
`${paint(reaction, creature.color)}\n\n` +
`Energy: ${entry.energy}% Mood: ${paint(entry.mood, 'green')} XP: ${entry.xp} Level: ${entry.level}`
);
}
function rename(args) {
const [name, ...aliasParts] = args;
const alias = aliasParts.join(' ').trim();
if (!name || !alias) {
return output(`Usage: ${strong('pokebuddy rename <name> <alias>', 'green')}`, 1);
}
const creature = getCreatureByName(name);
if (!creature) return output(notFound(name), 1);
const state = loadState();
const entry = ensureCompanionState(state, creature);
entry.alias = alias;
addEvent(state, 'rename', { id: creature.id, alias });
saveState(state);
return output(`${paint(creature.name, creature.color)} is now known as ${strong(alias, creature.color)}.`);
}
function reset(args) {
if (!args.includes('--yes')) {
return output(`This will remove local PokeBuddy state. Re-run with ${strong('pokebuddy reset --yes', 'red')} to confirm.`, 1);
}
resetState();
return output(`${paint('Local PokeBuddy state reset.', 'green')} Run ${strong('pokebuddy hatch', 'green')} to begin again.`);
}
function notFound(name) {
return `${paint(`Unknown companion: ${name}`, 'red')}\n\nKnown companions:\n${creatures.map(compactCreatureLine).join('\n')}`;
}
function output(message, code = 0) {
console.log(message);
if (code !== 0) process.exitCode = code;
}

191
src/creatures.js Normal file
View file

@ -0,0 +1,191 @@
export const rarityColors = {
Uncommon: 'green',
Rare: 'yellow',
Epic: 'magenta',
Mythic: 'cyan'
};
export const creatures = [
{
id: 'zapbit',
name: 'Zapbit',
emoji: '⚡',
type: ['Logic', 'Spark'],
rarity: 'Rare',
color: 'yellow',
personality: 'Energetic, clever, and always ready to spark an idea.',
flavor: 'Hyper-charged logic companion.',
quote: 'I will light the way.',
likes: ['clean commits', 'passing tests', 'short functions'],
dislikes: ['flaky tests', 'mystery globals', 'panic refactors'],
stats: { DEBUG: 74, FOCUS: 82, CHAOS: 61, WISDOM: 58, SNARK: 77 },
reactions: {
poke: 'Zapbit sparks happily. Great commit. Clean and efficient.',
feed: 'Zapbit crunches the input into tiny bright logic crumbs.',
status: 'Every circuit is humming. Slightly smug, but useful.',
idle: 'Zapbit watches your cursor like it owes money.'
},
ascii: String.raw`
/\__/\
___/ \___
/ \ ▽ / \
/____/\_0101_/\\____\
/ || \
/___||___\`
},
{
id: 'diskettex',
name: 'Diskettex',
emoji: '💾',
type: ['Storage', 'Memory'],
rarity: 'Epic',
color: 'cyan',
personality: 'Reliable, nostalgic, and never forgets a byte.',
flavor: 'Never forgets a byte.',
quote: 'Old school. Always in sync.',
likes: ['backups', 'version tags', 'good changelogs'],
dislikes: ['force-push accidents', 'unnamed files', 'lost context'],
stats: { DEBUG: 63, FOCUS: 91, CHAOS: 28, WISDOM: 85, SNARK: 52 },
reactions: {
poke: 'Diskettex blinks, saves your current vibe, and waves politely.',
feed: 'Diskettex archives the offering under /memories/useful-things.',
status: 'Diskettex is calm. Suspiciously organized.',
idle: 'Diskettex labels a tiny folder called maybe-important.'
},
ascii: String.raw`
`
},
{
id: 'nullwisp',
name: 'NullWisp',
emoji: '⌁',
type: ['Void', 'Debug'],
rarity: 'Mythic',
color: 'cyan',
personality: 'Ethereal, mysterious, and very good at finding what is missing.',
flavor: 'Haunts hidden bugs.',
quote: 'I see what others cannot.',
likes: ['null checks', 'stack traces', 'quiet terminals'],
dislikes: ['undefined behavior', 'silent failures', 'empty promises'],
stats: { DEBUG: 96, FOCUS: 68, CHAOS: 88, WISDOM: 82, SNARK: 41 },
reactions: {
poke: 'NullWisp ripples through the terminal and points at a suspicious variable.',
feed: 'NullWisp absorbs the fragment and whispers: not null anymore.',
status: 'NullWisp is present, allegedly. The logs disagree.',
idle: 'NullWisp quietly haunts your TODO comments.'
},
ascii: String.raw`
0 1 0
..
. .
( NULL )
1 0`
},
{
id: 'promptmoth',
name: 'PromptMoth',
emoji: '✦',
type: ['AI', 'Prompt'],
rarity: 'Epic',
color: 'magenta',
personality: 'Curious, expressive, and thriving on context.',
flavor: 'Drawn to glowing context.',
quote: 'Ask nicely. I will help you fly.',
likes: ['clear instructions', 'examples', 'structured prompts'],
dislikes: ['vague tasks', 'empty specs', 'context starvation'],
stats: { DEBUG: 55, FOCUS: 79, CHAOS: 64, WISDOM: 92, SNARK: 63 },
reactions: {
poke: 'PromptMoth flutters around your request and improves the phrasing.',
feed: 'PromptMoth drinks the context window like neon nectar.',
status: 'PromptMoth is glowing. It has probably read too much.',
idle: 'PromptMoth circles a blinking cursor with dramatic intent.'
},
ascii: String.raw`
\ /
\/
PROMPT
__`
},
{
id: 'cachegoblin',
name: 'CacheGoblin',
emoji: '🧩',
type: ['Cache', 'Chaos'],
rarity: 'Uncommon',
color: 'green',
personality: 'Greedy for speed and hiding things in all the right places.',
flavor: 'Makes fast things suspicious.',
quote: 'Fast things stay in my pockets.',
likes: ['warm caches', 'tiny shortcuts', 'build artifacts'],
dislikes: ['cache invalidation', 'cold starts', 'clean installs'],
stats: { DEBUG: 49, FOCUS: 57, CHAOS: 95, WISDOM: 45, SNARK: 88 },
reactions: {
poke: 'CacheGoblin grins and returns an answer from somewhere it refuses to explain.',
feed: 'CacheGoblin snatches the cache and scatters fragments everywhere.',
status: 'CacheGoblin is fast today. That should make everyone nervous.',
idle: 'CacheGoblin is hoarding .tmp files under the floorboards.'
},
ascii: String.raw`
.-""""-.
.- -.
/ \
CACHE
________
`
},
{
id: 'mergedrake',
name: 'MergeDrake',
emoji: '🔥',
type: ['Git', 'Fire'],
rarity: 'Rare',
color: 'orange',
personality: 'Protective, loyal, and deeply invested in clean merges.',
flavor: 'Guardian of branches.',
quote: 'I guard your branches.',
likes: ['rebases that end well', 'small pull requests', 'green CI'],
dislikes: ['merge conflicts', 'stale branches', 'Friday deploys'],
stats: { DEBUG: 71, FOCUS: 88, CHAOS: 67, WISDOM: 76, SNARK: 70 },
reactions: {
poke: 'MergeDrake puffs a tiny flame and promises to guard the branch.',
feed: 'MergeDrake burns away conflict markers with theatrical dignity.',
status: 'MergeDrake is perched on main, judging all branches equally.',
idle: 'MergeDrake curls around a pull request and sleeps with one eye open.'
},
ascii: String.raw`
/\___/\
___/ \___
/ \
____ GIT____
🔥 /_\ 🔥`
}
];
export function getCreatureByIdOrName(value) {
if (!value) return null;
const key = String(value).toLowerCase();
return creatures.find((creature) => creature.id === key || creature.name.toLowerCase() === key) ?? null;
}
export function getCreatureByName(value) {
return getCreatureByIdOrName(value);
}

22
src/hatch.js Normal file
View file

@ -0,0 +1,22 @@
import crypto from 'node:crypto';
import os from 'node:os';
import { creatures } from './creatures.js';
function seedSource() {
const user = os.userInfo().username || 'anonymous';
const host = os.hostname() || 'localhost';
const custom = process.env.POKEBUDDY_SEED || '';
return `${user}:${host}:${custom}:pokebuddy-cli-v0`;
}
export function nextCreature(excludedIds = []) {
const pool = creatures.filter((creature) => !excludedIds.includes(creature.id));
if (pool.length === 0) return null;
const digest = crypto
.createHash('sha256')
.update(`${seedSource()}:${excludedIds.sort().join(',')}`)
.digest('hex');
const index = parseInt(digest.slice(0, 8), 16) % pool.length;
return pool[index];
}

115
src/render.js Normal file
View file

@ -0,0 +1,115 @@
import { creatures, rarityColors } from './creatures.js';
import { paint, strong, dim, stripAnsi } from './ansi.js';
export function banner() {
return `${paint('╭──────────────────────────────────────────────╮', 'green')}\n` +
`${paint('│', 'green')} ${strong('PokeBuddy', 'white')} ${strong('CLI', 'green')} ${dim('· tiny terminal companions')} ${paint('│', 'green')}\n` +
`${paint('│', 'green')} ${paint('POKE', 'green')} = Programmable Open-source Kinetic Entities ${paint('│', 'green')}\n` +
`${paint('╰──────────────────────────────────────────────╯', 'green')}`;
}
export function helpText(version) {
return `${banner()}\n\n` +
`${strong('Usage', 'green')}\n` +
` pokebuddy <command> [options]\n\n` +
`${strong('Core commands', 'green')}\n` +
` hatch [--all] [--force] Hatch a companion\n` +
` list List discovered companions\n` +
` dex Show every known companion\n` +
` show [name] Show one companion card\n` +
` poke [name] Interact with a companion\n` +
` feed [name] [thing] Feed context, cache or a snack\n` +
` status Show local companion status\n` +
` rename <name> <alias> Rename a discovered companion\n` +
` banner Print the project banner\n` +
` legal Print legal notice\n` +
` reset --yes Reset local state\n\n` +
`${strong('Examples', 'green')}\n` +
` pokebuddy hatch\n` +
` pokebuddy poke zapbit\n` +
` pokebuddy feed cachegoblin ./build/cache\n` +
` pokebuddy show nullwisp\n\n` +
`${dim(`v${version} · local-first · offline-ready`)}`;
}
export function legalText() {
return `${strong('Legal notice', 'green')}\n\n` +
`PokeBuddy CLI is an independent open-source project.\n` +
`It is not affiliated with, endorsed by, sponsored by, authorized by,\n` +
`or officially connected with Nintendo, The Pokémon Company, Game Freak,\n` +
`Creatures Inc., Anthropic, or Claude Code.\n\n` +
`POKE means Programmable Open-source Kinetic Entities.\n` +
`All companions, names, lore, ASCII art, and code in this repository are original.\n` +
`Do not submit official characters, sprites, names, logos, music, or derivative assets.\n`;
}
export function listCompanions(state) {
const discovered = Object.values(state.companions);
if (discovered.length === 0) {
return `${paint('No companions discovered yet.', 'yellow')} Run ${strong('pokebuddy hatch', 'green')} to begin.`;
}
const rows = discovered.map((entry) => {
const creature = creatures.find((item) => item.id === entry.id);
const color = creature?.color ?? 'green';
const active = state.active === entry.id ? paint('●', 'green') : ' ';
return ` ${active} ${paint(entry.alias.padEnd(14), color)} ${paint(creature.emoji, color)} ${entry.mood.padEnd(12)} lv.${entry.level} xp.${entry.xp}`;
});
return `${strong('$ pokebuddy list', 'green')}\n${rows.join('\n')}`;
}
export function dex() {
return creatures.map((creature) => compactCreatureLine(creature)).join('\n');
}
export function compactCreatureLine(creature) {
const rarityColor = rarityColors[creature.rarity] ?? creature.color;
return `${paint(creature.name.padEnd(13), creature.color)} ${paint(creature.emoji, creature.color)} ${creature.type.join(' / ').padEnd(18)} ${paint(creature.rarity, rarityColor)} ${dim(creature.flavor)}`;
}
export function creatureCard(creature, entry = null) {
const title = `${creature.emoji} ${creature.name}`;
const alias = entry && entry.alias !== creature.name ? ` aka ${entry.alias}` : '';
const stats = Object.entries(creature.stats)
.map(([label, value]) => ` ${label.padEnd(7)} ${bar(value)} ${String(value).padStart(3)}`)
.join('\n');
const meta = [
`Type: ${paint(creature.type.join(' / '), creature.color)}`,
`Rarity: ${paint(creature.rarity, rarityColors[creature.rarity] ?? creature.color)}`,
entry ? `Level: ${entry.level} XP: ${entry.xp} Mood: ${entry.mood}` : null
].filter(Boolean).join('\n');
return `${strong(title, creature.color)}${dim(alias)}\n${paint(creature.ascii, creature.color)}\n\n${meta}\n\n${creature.personality}\n\n${paint(`${creature.quote}`, creature.color)}\n\n${stats}`;
}
export function statusView(state) {
const discovered = Object.values(state.companions);
const active = discovered.find((item) => item.id === state.active) ?? discovered[0];
const activeCreature = active ? creatures.find((item) => item.id === active.id) : null;
return `${strong('$ pokebuddy status', 'green')}\n` +
`Companions discovered: ${paint(String(discovered.length), 'green')} / ${creatures.length}\n` +
`Active companion: ${activeCreature ? paint(active.alias, activeCreature.color) : dim('none')}\n` +
`State file: ${dim('~/.pokebuddy/state.json')}\n` +
`Recent events: ${state.events.length}\n` +
(activeCreature ? `\n${creatureCard(activeCreature, active)}` : `\nRun ${strong('pokebuddy hatch', 'green')} to hatch your first companion.`);
}
export function bar(value, width = 10) {
const filled = Math.max(0, Math.min(width, Math.round((value / 100) * width)));
return `${paint('█'.repeat(filled), 'green')}${dim('░'.repeat(width - filled))}`;
}
export function boxed(message, color = 'green') {
const lines = String(message).split('\n');
const width = Math.max(...lines.map((line) => stripAnsi(line).length));
const top = `${'─'.repeat(width + 2)}`;
const bottom = `${'─'.repeat(width + 2)}`;
const body = lines.map((line) => {
const pad = ' '.repeat(width - stripAnsi(line).length);
return `${line}${pad}`;
});
return [paint(top, color), ...body.map((line) => paint(line, color)), paint(bottom, color)].join('\n');
}

74
src/state.js Normal file
View file

@ -0,0 +1,74 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
const STATE_DIR = path.join(os.homedir(), '.pokebuddy');
const STATE_FILE = path.join(STATE_DIR, 'state.json');
export function getStatePath() {
return STATE_FILE;
}
export function loadState() {
if (!fs.existsSync(STATE_FILE)) {
return createInitialState();
}
try {
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
} catch {
return createInitialState();
}
}
export function saveState(state) {
fs.mkdirSync(STATE_DIR, { recursive: true });
fs.writeFileSync(STATE_FILE, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
}
export function resetState() {
if (fs.existsSync(STATE_FILE)) {
fs.rmSync(STATE_FILE);
}
}
export function createInitialState() {
return {
schemaVersion: 1,
createdAt: new Date().toISOString(),
active: null,
companions: {},
events: []
};
}
export function ensureCompanionState(state, creature) {
if (!state.companions[creature.id]) {
state.companions[creature.id] = {
id: creature.id,
alias: creature.name,
discoveredAt: new Date().toISOString(),
level: 1,
xp: 0,
energy: 50,
mood: 'idle',
pokes: 0,
feeds: 0
};
}
if (!state.active) {
state.active = creature.id;
}
return state.companions[creature.id];
}
export function addEvent(state, type, payload = {}) {
state.events.unshift({
type,
payload,
at: new Date().toISOString()
});
state.events = state.events.slice(0, 30);
}