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

9
.editorconfig Normal file
View file

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

29
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Bug report
description: Report a problem with PokeBuddy CLI
title: "bug: "
labels: [bug]
body:
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Describe the bug.
validations:
required: true
- type: input
id: version
attributes:
label: PokeBuddy version
placeholder: "0.1.0"
- type: textarea
id: steps
attributes:
label: Steps to reproduce
placeholder: |
1. Run ...
2. See ...
- type: textarea
id: logs
attributes:
label: Logs or screenshots
render: shell

View file

@ -0,0 +1,40 @@
name: Companion submission
description: Propose an original PokeBuddy companion
title: "companion: "
labels: [companion]
body:
- type: input
id: name
attributes:
label: Companion name
placeholder: "LintFox"
validations:
required: true
- type: input
id: type
attributes:
label: Type
placeholder: "Quality / Speed"
- type: dropdown
id: rarity
attributes:
label: Rarity
options:
- Uncommon
- Rare
- Epic
- Mythic
- type: textarea
id: concept
attributes:
label: Concept
description: Describe the companion and why it belongs in PokeBuddy.
validations:
required: true
- type: checkboxes
id: originality
attributes:
label: Originality confirmation
options:
- label: This companion is original and not based on protected third-party characters or assets.
required: true

View file

@ -0,0 +1,20 @@
name: Feature request
description: Suggest an idea for PokeBuddy CLI
title: "feat: "
labels: [enhancement]
body:
- type: textarea
id: idea
attributes:
label: What would you like?
validations:
required: true
- type: textarea
id: why
attributes:
label: Why is this useful?
- type: textarea
id: shape
attributes:
label: Suggested command or API
render: shell

19
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,19 @@
## Summary
What changed?
## Type
- [ ] Bug fix
- [ ] Feature
- [ ] Docs
- [ ] Companion / art
- [ ] Refactor
## Checklist
- [ ] I ran `npm test`.
- [ ] I ran `npm run lint`.
- [ ] I updated docs if needed.
- [ ] My contribution is original.
- [ ] I did not include protected third-party assets, names or derivative designs.

19
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,19 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install
- run: npm run lint
- run: npm test

12
.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.DS_Store
.env
.env.*
!.env.example
coverage/
dist/
.pokebuddy/

6
.npmignore Normal file
View file

@ -0,0 +1,6 @@
.github/
docs/
test/
coverage/
.DS_Store
*.log

15
CHANGELOG.md Normal file
View file

@ -0,0 +1,15 @@
# Changelog
All notable changes to PokeBuddy CLI will be documented here.
## 0.1.0
Initial public alpha.
### Added
- CLI entrypoint.
- Six launch companions: Zapbit, Diskettex, NullWisp, PromptMoth, CacheGoblin and MergeDrake.
- Local state in `~/.pokebuddy/state.json`.
- Commands: `hatch`, `list`, `dex`, `show`, `poke`, `feed`, `status`, `rename`, `banner`, `legal`, `reset`.
- README, legal notice, contribution docs and roadmap.

19
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,19 @@
# Code of Conduct
PokeBuddy CLI is for developers who enjoy building delightful tools together.
## Our standard
We expect contributors to be kind, constructive and respectful. Debate ideas, not people. Keep issues and pull requests focused, useful and welcoming.
## Unacceptable behavior
Unacceptable behavior includes harassment, insults, discriminatory language, threats, doxxing, spam or repeated disruptive conduct.
## Enforcement
Maintainers may remove comments, close issues, reject pull requests or restrict participation when needed to keep the project healthy.
## Reporting
If you need to report a concern, contact the maintainers through the repository issue tracker or listed project contact.

86
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,86 @@
# Contributing to PokeBuddy CLI
Thanks for helping improve PokeBuddy CLI.
This project is meant to feel delightful, original and developer-native. We welcome code, docs, companion ideas, ASCII/ANSI art, plugins and bug reports.
## Development setup
```bash
git clone https://github.com/pokebuddy-cli/pokebuddy.git
cd pokebuddy
npm install
npm link
pokebuddy hatch
```
Run checks:
```bash
npm test
npm run lint
```
## Contribution areas
You can contribute:
- original companion names;
- original ASCII/ANSI art;
- new commands;
- Git hook integrations;
- docs and examples;
- tests;
- plugin ideas;
- bug fixes.
## Not allowed
Do not contribute:
- official character names from third-party franchises;
- modified or derivative character names;
- official sprites;
- modified sprites;
- ASCII art recreating protected characters;
- logos or logo-like artwork from third-party franchises;
- copyrighted music, sound effects or game assets;
- language that suggests official affiliation with Nintendo, The Pokémon Company, Game Freak, Creatures Inc., Anthropic or Claude Code.
## Creature naming rules
Good examples:
- Zapbit
- Diskettex
- NullWisp
- PromptMoth
- CacheGoblin
- MergeDrake
Avoid names that are too close to existing characters or brands.
When in doubt, make it weirder, more original and more developer-focused.
## Pull request checklist
Before opening a PR:
- [ ] I ran `npm test`.
- [ ] I ran `npm run lint`.
- [ ] My contribution is original.
- [ ] I did not include protected third-party assets or derivative names.
- [ ] I updated docs when needed.
## Tone
PokeBuddy should feel:
- playful;
- useful;
- terminal-native;
- not annoying;
- respectful of user privacy;
- local-first by default.
Tiny friends. Big commits.

51
LEGAL.md Normal file
View file

@ -0,0 +1,51 @@
# Legal Notice
PokeBuddy CLI is an independent open-source developer tool.
It is designed as a terminal companion system featuring original programmable creatures that react to coding activity, Git events, test results, prompts, builds and local development workflows.
## No affiliation
PokeBuddy CLI is not affiliated with, endorsed by, sponsored by, authorized by, or associated with Nintendo, The Pokémon Company, Game Freak, Creatures Inc., Anthropic, Claude Code, or any of their subsidiaries, affiliates or partners.
## Meaning of POKE
In this project, **POKE** means:
> Programmable Open-source Kinetic Entities
The project uses the word “poke” in the ordinary English sense of interacting with, tapping, nudging or prompting a terminal companion.
## Original content
All companions, names, ASCII art, flavor text, stats, rarity systems, commands, project lore and code are original creations for PokeBuddy CLI unless otherwise stated.
This repository does not include or intentionally reference official characters, character names, sprites, logos, music, sound effects, maps, items, artwork, game assets or protected designs from third-party franchises.
## Trademark notice
All trademarks, registered trademarks, product names, company names and logos mentioned externally or by users belong to their respective owners.
Use of any third-party name in this repository, if present, is for identification, compatibility, attribution or legal notice purposes only and does not imply affiliation.
## Contribution restrictions
Contributors must not submit pull requests, issues, assets, ASCII art, names or documentation containing copyrighted or trademarked material from third-party franchises.
This includes, but is not limited to:
- official character names;
- derivative names based on official characters;
- official sprites or modified sprites;
- ASCII art recreating protected characters;
- logos or modified logos;
- music, sound effects or game assets;
- terminology that implies official affiliation.
Maintainers may remove, reject or rewrite any contribution that appears to violate third-party intellectual property rights or creates avoidable confusion.
## Takedown requests
If you believe this project includes material that infringes your rights, please contact the maintainers through the repository issue tracker or the contact information listed in the repository.
The maintainers will review good-faith requests and act where appropriate.

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Felipe Domingues
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

186
README.md Normal file
View file

@ -0,0 +1,186 @@
<p align="center">
<img src="assets/brand/pokebuddy_cli_terminal_companions_in_neon_style.png" alt="PokeBuddy CLI hero" width="100%" />
</p>
# PokeBuddy CLI
> Tiny terminal companions you can poke while you code.
**POKE = Programmable Open-source Kinetic Entities**
PokeBuddy CLI is a local-first, open-source terminal companion system for developers. Hatch tiny programmable companions, poke them during coding sessions, let them react to commits, tests, logs, prompts, builds and the beautiful chaos of shipping software.
It is part CLI toy, part dev mascot, part collectible terminal ritual. A small spark of delight living between your prompt and your next commit.
<p align="center">
<img alt="MIT License" src="https://img.shields.io/badge/license-MIT-blue" />
<img alt="Node" src="https://img.shields.io/badge/node-%3E%3D18-00ff90" />
<img alt="Status" src="https://img.shields.io/badge/status-alpha-ffd60a" />
<img alt="Offline first" src="https://img.shields.io/badge/offline--first-yes-00e6ff" />
</p>
---
## Preview
<p align="center">
<img src="assets/brand/pokebuddy_creature_dex_terminal_ui.png" alt="PokeBuddy Creature Dex" width="100%" />
</p>
## Why?
Most developer tools are useful. Few are memorable.
PokeBuddy adds a tiny living layer to the terminal: a companion that reacts, remembers, grows and makes your workflow feel a little more alive.
```bash
$ pokebuddy hatch
$ pokebuddy list
$ pokebuddy poke zapbit
$ pokebuddy status
```
---
## Features
- **Terminal native**: runs directly in your shell.
- **Offline-first**: no account, no server, no tracking.
- **Local state**: companion data lives in `~/.pokebuddy/state.json`.
- **Collectible companions**: six launch companions with types, rarity, stats and personality.
- **Zero runtime dependencies**: built with Node.js standard library.
- **Extensible foundation**: designed for future plugins, Git hooks and editor integrations.
---
## Install
### From source
```bash
git clone https://github.com/pokebuddy-cli/pokebuddy.git
cd pokebuddy
npm install
npm link
pokebuddy hatch
```
### Local development
```bash
npm test
npm run lint
node ./bin/pokebuddy.js hatch
```
---
## Core commands
| Command | Description |
|---|---|
| `pokebuddy hatch` | Hatch your first companion. |
| `pokebuddy hatch --force` | Discover another companion. |
| `pokebuddy hatch --all` | Unlock all launch companions locally. |
| `pokebuddy list` | List discovered companions. |
| `pokebuddy dex` | Show all known companions. |
| `pokebuddy show [name]` | Show a companion card. |
| `pokebuddy poke [name]` | Interact with a companion. |
| `pokebuddy feed [name] [thing]` | Feed context, cache or any offering. |
| `pokebuddy status` | Show local state and active companion. |
| `pokebuddy rename <name> <alias>` | Give a companion a custom alias. |
| `pokebuddy legal` | Print legal notice. |
| `pokebuddy reset --yes` | Reset local state. |
---
## Launch companions
| Companion | Type | Rarity | Personality |
|---|---|---|---|
| **Zapbit** | Logic / Spark | Rare | Hyper-charged logic companion. |
| **Diskettex** | Storage / Memory | Epic | Never forgets a byte. |
| **NullWisp** | Void / Debug | Mythic | Haunts hidden bugs. |
| **PromptMoth** | AI / Prompt | Epic | Drawn to glowing context. |
| **CacheGoblin** | Cache / Chaos | Uncommon | Makes fast things suspicious. |
| **MergeDrake** | Git / Fire | Rare | Guardian of branches. |
---
## Example output
```text
$ pokebuddy poke zapbit
Zapbit sparks happily. Great commit. Clean and efficient.
Energy: 60% Mood: happy XP: 12 Level: 1
```
```text
$ pokebuddy list
● Zapbit ⚡ happy lv.1 xp.12
Diskettex 💾 idle lv.1 xp.0
```
---
## Roadmap
- [x] Launch companions
- [x] Local hatch/list/poke/status loop
- [x] ASCII/ANSI-inspired terminal cards
- [ ] Git hook reactions
- [ ] `pokebuddy watch`
- [ ] Plugin API
- [ ] Companion creation kit
- [ ] Animated terminal states
- [ ] Exportable companion cards
- [ ] VS Code / Cursor extension experiments
See [`ROADMAP.md`](ROADMAP.md) for the full plan.
---
## Brand assets
| Asset | Path |
|---|---|
| Brand identity guide | `assets/brand/pokebuddy_cli_brand_identity_guide.png` |
| Companion guide | `assets/brand/pokebuddy_cli_companion_guide_poster.png` |
| README hero kit | `assets/brand/pokebuddy_cli_terminal_companions_in_neon_style.png` |
| Launch poster & merch | `assets/brand/pokebuddy_cli_launch_poster_and_merch.png` |
---
## Legal notice
PokeBuddy CLI is an independent open-source project.
PokeBuddy CLI is not affiliated with, endorsed by, sponsored by, authorized by, or officially connected with Nintendo, The Pokémon Company, Game Freak, Creatures Inc., Anthropic, or Claude Code.
“POKE” in PokeBuddy means **Programmable Open-source Kinetic Entities** and is used as part of the projects original identity around interactive terminal companions.
All companions, names, ASCII art, lore, code and visual concepts in this repository are original to this project unless otherwise stated.
See [`LEGAL.md`](LEGAL.md) for the full notice.
---
## Contributing
Contributions are welcome. Please read:
- [`CONTRIBUTING.md`](CONTRIBUTING.md)
- [`docs/CREATURE_GUIDELINES.md`](docs/CREATURE_GUIDELINES.md)
- [`docs/NAMING_GUIDELINES.md`](docs/NAMING_GUIDELINES.md)
- [`LEGAL.md`](LEGAL.md)
When in doubt: make it original, make it weird, make it developer-flavored.
---
## License
MIT. See [`LICENSE`](LICENSE).
Made with a glowing cursor and too much affection for terminals.

50
ROADMAP.md Normal file
View file

@ -0,0 +1,50 @@
# PokeBuddy CLI Roadmap
## v0.1.0 — Spark
- [x] CLI foundation
- [x] Local state
- [x] Six launch companions
- [x] Hatch/list/dex/show/poke/feed/status commands
- [x] Legal and contribution docs
- [x] Brand assets
## v0.2.0 — Terminal Life
- [ ] `pokebuddy watch`
- [ ] idle reactions
- [ ] mood decay and energy recovery
- [ ] terminal-safe compact mode
- [ ] companion card export
## v0.3.0 — Git Reactions
- [ ] Git hook installer
- [ ] Commit message reactions
- [ ] Branch change reactions
- [ ] Merge conflict detection
- [ ] CI status parsing
## v0.4.0 — Companion Studio
- [ ] Companion schema
- [ ] `pokebuddy create-companion`
- [ ] custom ASCII/ANSI loader
- [ ] companion packs
- [ ] validation rules for naming and legal safety
## v0.5.0 — Plugin API
- [ ] JS plugin interface
- [ ] event hooks
- [ ] local plugin registry
- [ ] plugin examples
- [ ] plugin safety docs
## v1.0.0 — Stable Buddy Core
- [ ] Stable command API
- [ ] Stable local state schema
- [ ] Companion pack docs
- [ ] Full test coverage for core flows
- [ ] Release automation

27
SECURITY.md Normal file
View file

@ -0,0 +1,27 @@
# Security Policy
PokeBuddy CLI is local-first and stores user state at:
```text
~/.pokebuddy/state.json
```
It does not require network access, accounts or telemetry.
## Supported versions
| Version | Supported |
|---|---|
| 0.x | Yes |
## Reporting a vulnerability
Please report security issues privately when possible. If a private channel is not yet listed, open a minimal GitHub issue requesting security contact without publishing exploit details.
## Security principles
- No telemetry by default.
- No hidden network calls.
- No shell command execution from companion data.
- Companion plugins must be explicit and user-installed.
- Local state should remain human-readable.

View file

@ -0,0 +1,8 @@
.-""""-.
.-╯ ◉ ◉ ╰-.
/ ▿ \
│ ┌────────┐ │
│ │ CACHE │ │
╲ └──┬──┬──┘
╲___│__│___

View file

@ -0,0 +1,8 @@
╔════════╗
║ ▣ ↑ ║
║ ║
║ ◉ ◉ ║
║ ▿ ║
║ ┌────┐ ║
╚═╧════╧═╝

View file

@ -0,0 +1,7 @@
/\___/\
___/ ◉ ◉ \___
/ ╱╲ \
│ ┌─╯╰─┐ │
╲____│ GIT│____
└─┬──┘
🔥 /_\ 🔥

View file

@ -0,0 +1,8 @@
0 1 0
.─────────.
.╯ ◉ ◉ ╰.
( NULL )
╲ ─────
╰──╮ ╭──╯
╰╮ ╭╯
1 ╰───╯ 0

View file

@ -0,0 +1,8 @@
╲\ ││ /
╭──╲\──╳──/╱──╮
◉ ◉ ╲
│ ▽ │
╲ ┌────────┐
╰───│PROMPT │───╯
└──┬──┬──┘
__╲

7
assets/ascii/zapbit.txt Normal file
View file

@ -0,0 +1,7 @@
✦ ⚡ ✦
/\__/\
___/ ◉ ◉\___
/ \ ▽ / \
/____/\_0101_/\\____\
/ || \
⚡/___||___\⚡

View file

@ -0,0 +1,14 @@
{
"project": "PokeBuddy CLI",
"assets": [
"assets/brand/pokebuddy_cli_brand_identity_guide.png",
"assets/brand/pokebuddy_cli_companion_guide_poster.png",
"assets/brand/pokebuddy_cli_terminal_companions_in_neon_style.png",
"assets/brand/pokebuddy_cli_launch_poster_and_merch.png",
"assets/brand/pokebuddy_creature_dex_terminal_ui.png",
"assets/brand/pokebuddy_cli_neon_terminal_companions.png",
"assets/brand/pokebuddy_cli_brand_kit_showcase.png",
"assets/brand/pokebuddy_terminal_companions_for_coders.png"
],
"note": "Generated concept art assets for README, launch, brand direction and community visuals."
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

10
bin/pokebuddy.js Executable file
View file

@ -0,0 +1,10 @@
#!/usr/bin/env node
import { run } from '../src/cli.js';
run(process.argv.slice(2)).catch((error) => {
console.error(`\nPokeBuddy crashed: ${error.message}\n`);
if (process.env.POKEBUDDY_DEBUG === '1') {
console.error(error.stack);
}
process.exit(1);
});

58
docs/ART_DIRECTION.md Normal file
View file

@ -0,0 +1,58 @@
# Art Direction
PokeBuddy uses a cyber-retro terminal style: neon, pixel, ANSI-inspired, dark UI and expressive companions.
## Keywords
- terminal native
- pixel art
- ANSI-inspired
- cyber-retro
- neon green
- local-first
- collectible companions
- developer tool culture
## Palette
| Token | Hex |
|---|---|
| Accent Green | `#00FF90` |
| Cyan | `#00E6FF` |
| Yellow | `#FFD60A` |
| Magenta | `#FF4DFF` |
| Orange | `#FF6A3D` |
| Purple | `#A78BFA` |
| Gray | `#A1A1AA` |
| White | `#FFFFFF` |
| Base | `#0B0F14` |
| Surface | `#11161C` |
## Typography
Recommended:
- Headings: Space Grotesk
- Terminal/code: JetBrains Mono
For pure terminal output, rely on the users configured monospace font.
## Logo
Use the terminal prompt mark as the core logo:
```text
>_
```
Keep it simple, kinetic and legible.
## Companion art
Recommended workflow:
1. Sketch as pixel art.
2. Convert to ANSI/block style where needed.
3. Keep a compact terminal version.
4. Keep a high-detail README/brand version.
5. Never use official third-party sprites or derivatives.

29
docs/BRAND.md Normal file
View file

@ -0,0 +1,29 @@
# Brand System
PokeBuddy CLI is a playful developer tool with a serious product spine.
## Core message
> Tiny terminal companions you can poke while you code.
## Expanded message
PokeBuddy CLI brings living terminal companions to your workflow. They react to your commands, remember your progress and make development feel more alive.
## Voice
- playful, but not childish;
- technical, but not dry;
- warm, but not noisy;
- memorable, but not legally reckless.
## Taglines
- Tiny terminal companions you can poke while you code.
- POKE. CODE. REPEAT.
- Your code. Their company. Endless possibilities.
- Tiny friends. Big commits.
## Boilerplate
PokeBuddy CLI is an open-source, local-first terminal companion system for developers. Hatch collectible companions, interact with them from your shell and let them react to your coding workflow.

67
docs/COMMANDS.md Normal file
View file

@ -0,0 +1,67 @@
# Commands
## `pokebuddy hatch`
Hatches a companion and stores it locally.
```bash
pokebuddy hatch
pokebuddy hatch --force
pokebuddy hatch --all
```
## `pokebuddy list`
Lists discovered companions.
```bash
pokebuddy list
```
## `pokebuddy dex`
Shows every known launch companion.
```bash
pokebuddy dex
```
## `pokebuddy show [name]`
Shows a detailed companion card.
```bash
pokebuddy show zapbit
```
## `pokebuddy poke [name]`
Interacts with a companion and increases XP.
```bash
pokebuddy poke zapbit
```
## `pokebuddy feed [name] [thing]`
Feeds context, cache, file paths or playful offerings.
```bash
pokebuddy feed cachegoblin ./build/cache
```
## `pokebuddy status`
Shows local companion state.
```bash
pokebuddy status
```
## `pokebuddy rename <name> <alias>`
Gives a discovered companion a local alias.
```bash
pokebuddy rename zapbit Sparkinho
```

View file

@ -0,0 +1,69 @@
# Creature Guidelines
PokeBuddy companions should feel original, developer-native and terminal-friendly.
## Design principles
A good companion is:
- memorable at small size;
- readable in terminal output;
- strongly associated with a dev concept;
- cute enough to like;
- weird enough to remember;
- original enough to publish safely.
## Companion anatomy
Each companion should define:
- `id`
- `name`
- `type`
- `rarity`
- `personality`
- `flavor`
- `quote`
- `likes`
- `dislikes`
- `stats`
- `reactions`
- `ascii`
## Types
Recommended types:
- Logic
- Spark
- Storage
- Memory
- Void
- Debug
- AI
- Prompt
- Cache
- Chaos
- Git
- Fire
- Infra
- Shell
- Network
- Data
- Security
- Build
- Test
- Docs
## Rarities
- Uncommon
- Rare
- Epic
- Mythic
## Avoid
Do not create companions that copy or closely resemble protected characters, recognizable franchise silhouettes, official sprites, official names or modified official names.
Keep it original. Keep it dev-flavored.

48
docs/NAMING_GUIDELINES.md Normal file
View file

@ -0,0 +1,48 @@
# Naming Guidelines
Companion names should be short, original and developer-flavored.
## Good patterns
Blend a technical concept with a creature, object or vibe:
- `Zap` + `bit` → Zapbit
- `Diskette` + `x` → Diskettex
- `Null` + `Wisp` → NullWisp
- `Prompt` + `Moth` → PromptMoth
- `Cache` + `Goblin` → CacheGoblin
- `Merge` + `Drake` → MergeDrake
## Good names
- LintFox
- CronCrab
- TokenBat
- EnvImp
- JsonGecko
- ApiOtter
- DockerMole
- Shellfin
- ForkSprite
- BugSlug
## Avoid
Avoid names that are:
- direct references to third-party characters;
- modified versions of third-party character names;
- too close to protected brands;
- hard to pronounce;
- too generic.
## Test
Ask:
1. Is this name searchable?
2. Does it evoke a dev concept?
3. Would it still work without any outside reference?
4. Could a contributor draw it without copying anything?
If yes, it probably belongs here.

34
docs/PLUGIN_API.md Normal file
View file

@ -0,0 +1,34 @@
# Plugin API Draft
The plugin API is planned for a future release. This document is a draft.
## Goals
Plugins should be able to:
- listen to PokeBuddy events;
- add reactions;
- add custom commands;
- create companion packs;
- integrate with Git hooks, CI, editors and terminals.
## Draft shape
```js
export default {
name: 'my-plugin',
version: '0.1.0',
onEvent(event, context) {
if (event.type === 'commit') {
return context.say('Nice commit. Tiny goblin approved.');
}
}
};
```
## Safety rules
- Plugins must be explicit.
- No hidden network calls.
- No arbitrary shell execution by default.
- Plugin permissions should be visible to users.

View file

@ -0,0 +1,11 @@
// Future plugin API example. This file is illustrative only.
export default {
name: 'commit-cheerleader',
version: '0.1.0',
onEvent(event, context) {
if (event.type === 'git:commit') {
return context.say('Your companion approves this commit. Probably.');
}
}
};

50
package.json Normal file
View file

@ -0,0 +1,50 @@
{
"name": "pokebuddy",
"version": "0.1.0",
"description": "Tiny terminal companions you can poke while you code.",
"type": "module",
"bin": {
"pokebuddy": "./bin/pokebuddy.js"
},
"scripts": {
"start": "node ./bin/pokebuddy.js",
"dev": "node ./bin/pokebuddy.js",
"test": "node --test",
"lint": "node --check ./bin/pokebuddy.js && node --check ./src/*.js",
"prepare:local": "npm link"
},
"keywords": [
"cli",
"terminal",
"developer-tools",
"virtual-pet",
"coding-companion",
"git",
"ascii-art",
"ansi",
"open-source",
"devtools",
"offline-first"
],
"author": "Felipe Domingues",
"license": "MIT",
"engines": {
"node": ">=18"
},
"files": [
"bin",
"src",
"assets/ascii",
"README.md",
"LEGAL.md",
"LICENSE"
],
"repository": {
"type": "git",
"url": "https://github.com/pokebuddy-cli/pokebuddy.git"
},
"bugs": {
"url": "https://github.com/pokebuddy-cli/pokebuddy/issues"
},
"homepage": "https://github.com/pokebuddy-cli/pokebuddy#readme"
}

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);
}

22
test/smoke.test.js Normal file
View file

@ -0,0 +1,22 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { creatures, getCreatureByName } from '../src/creatures.js';
import { creatureCard, dex } from '../src/render.js';
test('ships the six launch companions', () => {
assert.equal(creatures.length, 6);
assert.ok(getCreatureByName('Zapbit'));
assert.ok(getCreatureByName('diskettex'));
});
test('renders creature cards', () => {
const card = creatureCard(getCreatureByName('NullWisp'));
assert.match(card, /NullWisp/);
assert.match(card, /Void/);
});
test('renders dex output', () => {
const output = dex();
assert.match(output, /PromptMoth/);
assert.match(output, /MergeDrake/);
});