Problem: The original goal system converted total portfolio value to the target currency. This meant having 0 ZEC but equivalent BTC would show "ZEC goal met" — misleading for users who want to accumulate a specific asset.
Solution: Two distinct goal types:
- Holdings goals: Compare actual asset holdings to target ("I want to own 10 ZEC")
- Value goals: Compare total portfolio value converted to target currency ("I want portfolio worth 0.1 BTC")
- IDs are simple integers starting at 1
next_goal_idonly increments, never decrements- Deleted goal IDs are never reused
- Ensures stable references for users managing multiple goals
{
"version": 2,
"goals": [
{ "id": 1, "type": "holdings", "asset": "zec", "operator": ">=", "target": "10", "created_at": 1737108600000 }
],
"next_goal_id": 2
}- Goals stored as array (not object) for ordering and ID management
- Target stored as string for decimal.js precision
- Timestamps as epoch ms to avoid timezone issues
- Automatic, non-interactive (works in scripts/CI)
- Creates
.backupfile before migrating - Old goals converted to "value" type (preserves original behavior)
- One-time warning printed to stderr
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;- Interactive wizard when TTY detected and no arguments
- List goals when non-TTY (for scripts/pipes)
shieldstack goal btc 0.1 defaults to "value" type for backwards compatibility with v0.1.2/v0.1.3 behavior.
All file writes use tmp-then-rename pattern:
await fs.writeFile('config.json.tmp', data);
await fs.rename('config.json.tmp', 'config.json');Prevents corruption if process interrupted mid-write.
- Use decimal.js for all calculations
- Convert to primitives before storage (never store Decimal objects)
- Target amounts stored as strings in config for precision
- Single API source for MVP simplicity
- 5-minute cache for current prices
- 24-hour cache for historical data
- Graceful degradation: use cache on failure, warn on stderr
- User-Agent header includes package version
Progress percentages can exceed 100% (goal exceeded), but bars clamp at 100% to prevent visual overflow.
Dashboard sorts goals: unmet first, then by created_at (oldest first).