burnrate 0.1.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +507 -0
  3. package/dist/cli/format.d.ts +13 -0
  4. package/dist/cli/format.js +319 -0
  5. package/dist/cli/index.d.ts +7 -0
  6. package/dist/cli/index.js +1121 -0
  7. package/dist/cli/setup.d.ts +8 -0
  8. package/dist/cli/setup.js +143 -0
  9. package/dist/core/async-engine.d.ts +411 -0
  10. package/dist/core/async-engine.js +2274 -0
  11. package/dist/core/async-worldgen.d.ts +19 -0
  12. package/dist/core/async-worldgen.js +221 -0
  13. package/dist/core/engine.d.ts +154 -0
  14. package/dist/core/engine.js +1104 -0
  15. package/dist/core/pathfinding.d.ts +38 -0
  16. package/dist/core/pathfinding.js +146 -0
  17. package/dist/core/types.d.ts +489 -0
  18. package/dist/core/types.js +359 -0
  19. package/dist/core/worldgen.d.ts +22 -0
  20. package/dist/core/worldgen.js +292 -0
  21. package/dist/db/database.d.ts +83 -0
  22. package/dist/db/database.js +829 -0
  23. package/dist/db/turso-database.d.ts +177 -0
  24. package/dist/db/turso-database.js +1586 -0
  25. package/dist/mcp/server.d.ts +7 -0
  26. package/dist/mcp/server.js +1877 -0
  27. package/dist/server/api.d.ts +8 -0
  28. package/dist/server/api.js +1234 -0
  29. package/dist/server/async-tick-server.d.ts +5 -0
  30. package/dist/server/async-tick-server.js +63 -0
  31. package/dist/server/errors.d.ts +78 -0
  32. package/dist/server/errors.js +156 -0
  33. package/dist/server/rate-limit.d.ts +22 -0
  34. package/dist/server/rate-limit.js +134 -0
  35. package/dist/server/tick-server.d.ts +9 -0
  36. package/dist/server/tick-server.js +114 -0
  37. package/dist/server/validation.d.ts +194 -0
  38. package/dist/server/validation.js +114 -0
  39. package/package.json +65 -0
@@ -0,0 +1,1121 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BURNRATE CLI
4
+ * A logistics war MMO for Claude Code
5
+ * The front doesn't feed itself.
6
+ */
7
+ // Route 'setup' command to the setup script before loading heavy dependencies
8
+ if (process.argv[2] === 'setup') {
9
+ await import('./setup.js');
10
+ process.exit(0);
11
+ }
12
+ import { Command } from 'commander';
13
+ import { GameDatabase } from '../db/database.js';
14
+ import { GameEngine } from '../core/engine.js';
15
+ import { generateWorld, seedMarkets } from '../core/worldgen.js';
16
+ import { formatView, formatZone, formatRoutes, formatMarket, formatShipments, formatEvents, formatHelp } from './format.js';
17
+ import { TIER_LIMITS } from '../core/types.js';
18
+ import path from 'path';
19
+ import os from 'os';
20
+ import fs from 'fs';
21
+ // Database path
22
+ const DB_DIR = path.join(os.homedir(), '.burnrate');
23
+ const DB_PATH = path.join(DB_DIR, 'game.db');
24
+ // Ensure directory exists
25
+ if (!fs.existsSync(DB_DIR)) {
26
+ fs.mkdirSync(DB_DIR, { recursive: true });
27
+ }
28
+ // Initialize
29
+ const db = new GameDatabase(DB_PATH);
30
+ const engine = new GameEngine(db);
31
+ // Check if world needs initialization
32
+ const zones = db.getAllZones();
33
+ if (zones.length === 0) {
34
+ console.log('Initializing new world for Season 1...');
35
+ generateWorld(db);
36
+ seedMarkets(db);
37
+ console.log('World created. Welcome to BURNRATE.\n');
38
+ }
39
+ // Get or create player session
40
+ function getPlayer(name) {
41
+ const configPath = path.join(DB_DIR, 'player.json');
42
+ if (fs.existsSync(configPath)) {
43
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
44
+ return db.getPlayer(config.playerId);
45
+ }
46
+ if (!name) {
47
+ return null;
48
+ }
49
+ // Create new player
50
+ const hubs = db.getAllZones().filter(z => z.type === 'hub');
51
+ const startZone = hubs[0];
52
+ const player = db.createPlayer(name, startZone.id);
53
+ fs.writeFileSync(configPath, JSON.stringify({ playerId: player.id }));
54
+ return player;
55
+ }
56
+ const program = new Command();
57
+ program
58
+ .name('burnrate')
59
+ .description('A logistics war MMO for Claude Code. The front doesn\'t feed itself.')
60
+ .version('0.1.0');
61
+ // ============================================================================
62
+ // VIEW COMMANDS
63
+ // ============================================================================
64
+ program
65
+ .command('view')
66
+ .description('View the world state and your status')
67
+ .argument('[target]', 'What to view: zone, routes, market, shipments, events')
68
+ .argument('[id]', 'Target ID (zone name, resource, etc.)')
69
+ .action((target, id) => {
70
+ const player = getPlayer();
71
+ if (!target) {
72
+ // Main dashboard
73
+ if (!player) {
74
+ console.log('No player found. Run: burnrate join <name>');
75
+ return;
76
+ }
77
+ console.log(formatView(db, player));
78
+ return;
79
+ }
80
+ switch (target) {
81
+ case 'zone': {
82
+ const zoneName = id;
83
+ const allZones = db.getAllZones();
84
+ const zone = zoneName
85
+ ? allZones.find(z => z.name.toLowerCase() === zoneName.toLowerCase())
86
+ : player ? db.getZone(player.locationId) : allZones[0];
87
+ if (!zone) {
88
+ console.log(`Zone not found: ${zoneName}`);
89
+ return;
90
+ }
91
+ console.log(formatZone(db, zone));
92
+ break;
93
+ }
94
+ case 'routes': {
95
+ const fromZone = player ? db.getZone(player.locationId) : null;
96
+ if (!fromZone) {
97
+ console.log('Join the game first: burnrate join <name>');
98
+ return;
99
+ }
100
+ console.log(formatRoutes(db, fromZone.id));
101
+ break;
102
+ }
103
+ case 'market': {
104
+ const resource = id;
105
+ const zoneId = player?.locationId;
106
+ if (!zoneId) {
107
+ console.log('Join the game first: burnrate join <name>');
108
+ return;
109
+ }
110
+ console.log(formatMarket(db, zoneId, resource));
111
+ break;
112
+ }
113
+ case 'shipments': {
114
+ if (!player) {
115
+ console.log('Join the game first: burnrate join <name>');
116
+ return;
117
+ }
118
+ const shipments = db.getPlayerShipments(player.id);
119
+ console.log(formatShipments(shipments));
120
+ break;
121
+ }
122
+ case 'events': {
123
+ const limit = id ? parseInt(id) : 20;
124
+ const events = db.getEvents({ limit, actorId: player?.id });
125
+ console.log(formatEvents(events));
126
+ break;
127
+ }
128
+ default:
129
+ console.log(`Unknown view target: ${target}`);
130
+ console.log('Try: zone, routes, market, shipments, events');
131
+ }
132
+ });
133
+ // ============================================================================
134
+ // JOIN COMMAND
135
+ // ============================================================================
136
+ program
137
+ .command('join')
138
+ .description('Join the game with a new character')
139
+ .argument('<name>', 'Your player name')
140
+ .action((name) => {
141
+ const existing = db.getPlayerByName(name);
142
+ if (existing) {
143
+ console.log(`Name "${name}" is taken. Choose another.`);
144
+ return;
145
+ }
146
+ const player = getPlayer(name);
147
+ if (player) {
148
+ console.log(`\nWelcome to BURNRATE, ${player.name}!`);
149
+ console.log(`\nYou start at ${db.getZone(player.locationId)?.name} with:`);
150
+ console.log(` ${player.inventory.credits} credits`);
151
+ console.log(` Courier license (can ship up to 10 units)`);
152
+ console.log(`\nRun 'burnrate view' to see the world.`);
153
+ console.log(`Run 'burnrate help' for commands.\n`);
154
+ }
155
+ });
156
+ // ============================================================================
157
+ // ROUTE COMMAND
158
+ // ============================================================================
159
+ program
160
+ .command('route')
161
+ .description('View direct routes from a zone')
162
+ .argument('[from]', 'Zone name (defaults to your location)')
163
+ .action((from) => {
164
+ const player = getPlayer();
165
+ const allZones = db.getAllZones();
166
+ let fromZone;
167
+ if (from) {
168
+ fromZone = allZones.find(z => z.name.toLowerCase() === from.toLowerCase());
169
+ if (!fromZone) {
170
+ console.log(`Zone not found: ${from}`);
171
+ return;
172
+ }
173
+ }
174
+ else if (player) {
175
+ fromZone = db.getZone(player.locationId);
176
+ }
177
+ else {
178
+ console.log('Specify a zone or join the game first');
179
+ return;
180
+ }
181
+ if (!fromZone)
182
+ return;
183
+ const routes = db.getRoutesFromZone(fromZone.id);
184
+ if (routes.length === 0) {
185
+ console.log(`\nNo routes from ${fromZone.name}\n`);
186
+ return;
187
+ }
188
+ console.log(`\n┌─ ROUTES FROM: ${fromZone.name} ─────────────────────────────────┐`);
189
+ console.log(`│ Destination │ Dist │ Risk │ Choke │`);
190
+ console.log(`├──────────────────────┼──────┼───────┼───────┤`);
191
+ for (const route of routes) {
192
+ const toZone = db.getZone(route.toZoneId);
193
+ const name = (toZone?.name || 'Unknown').padEnd(20).slice(0, 20);
194
+ const dist = route.distance.toString().padStart(4);
195
+ const risk = (Math.round(route.baseRisk * 100) + '%').padStart(5);
196
+ const choke = route.chokepointRating.toFixed(1).padStart(5);
197
+ console.log(`│ ${name} │ ${dist} │ ${risk} │ ${choke} │`);
198
+ }
199
+ console.log(`└──────────────────────┴──────┴───────┴───────┘`);
200
+ console.log(`\nMulti-hop routing? Build your own pathfinder.\n`);
201
+ });
202
+ // ============================================================================
203
+ // SHIP COMMAND
204
+ // ============================================================================
205
+ program
206
+ .command('ship')
207
+ .description('Create a shipment to move cargo')
208
+ .requiredOption('--to <zone>', 'Destination zone')
209
+ .requiredOption('--cargo <cargo>', 'Cargo in format "resource:amount,resource:amount"')
210
+ .option('--type <type>', 'Shipment type: courier, freight, convoy', 'courier')
211
+ .option('--via <zones>', 'Intermediate zones for multi-hop (comma-separated)')
212
+ .action((options) => {
213
+ const player = getPlayer();
214
+ if (!player) {
215
+ console.log('Join the game first: burnrate join <name>');
216
+ return;
217
+ }
218
+ // Parse cargo
219
+ const cargo = {};
220
+ for (const part of options.cargo.split(',')) {
221
+ const [resource, amount] = part.split(':');
222
+ cargo[resource.trim()] = parseInt(amount);
223
+ }
224
+ // Find zones
225
+ const allZones = db.getAllZones();
226
+ const fromZone = db.getZone(player.locationId);
227
+ const toZone = allZones.find(z => z.name.toLowerCase() === options.to.toLowerCase());
228
+ if (!fromZone) {
229
+ console.log('Could not determine your location');
230
+ return;
231
+ }
232
+ if (!toZone) {
233
+ console.log(`Zone not found: ${options.to}`);
234
+ return;
235
+ }
236
+ // Build path
237
+ const path = [fromZone.id];
238
+ if (options.via) {
239
+ // Multi-hop: player specified waypoints
240
+ for (const waypoint of options.via.split(',')) {
241
+ const zone = allZones.find(z => z.name.toLowerCase() === waypoint.trim().toLowerCase());
242
+ if (!zone) {
243
+ console.log(`Waypoint not found: ${waypoint}`);
244
+ return;
245
+ }
246
+ path.push(zone.id);
247
+ }
248
+ }
249
+ path.push(toZone.id);
250
+ // Validate all legs have direct routes
251
+ for (let i = 0; i < path.length - 1; i++) {
252
+ const routes = db.getRoutesBetween(path[i], path[i + 1]);
253
+ if (routes.length === 0) {
254
+ const fromName = db.getZone(path[i])?.name || path[i];
255
+ const toName = db.getZone(path[i + 1])?.name || path[i + 1];
256
+ console.log(`\n✗ No direct route: ${fromName} → ${toName}`);
257
+ console.log(` Use 'burnrate route ${fromName}' to see connections\n`);
258
+ return;
259
+ }
260
+ }
261
+ const result = engine.createShipmentWithPath(player.id, options.type, path, cargo);
262
+ if (result.success && result.shipment) {
263
+ const pathNames = path.map(id => db.getZone(id)?.name.split('.').pop() || id.slice(0, 6));
264
+ console.log(`\nShipment #${result.shipment.id.slice(0, 8)} created`);
265
+ console.log(`├─ Type: ${result.shipment.type}`);
266
+ console.log(`├─ Route: ${pathNames.join(' → ')}`);
267
+ console.log(`├─ Hops: ${path.length - 1}`);
268
+ console.log(`└─ Status: In transit\n`);
269
+ }
270
+ else {
271
+ console.log(`\n✗ ${result.error}\n`);
272
+ }
273
+ });
274
+ // ============================================================================
275
+ // TRADE COMMANDS
276
+ // ============================================================================
277
+ program
278
+ .command('buy')
279
+ .description('Place a buy order')
280
+ .argument('<resource>', 'Resource to buy')
281
+ .argument('<quantity>', 'Amount to buy')
282
+ .option('--limit <price>', 'Max price per unit')
283
+ .action((resource, quantity, options) => {
284
+ const player = getPlayer();
285
+ if (!player) {
286
+ console.log('Join the game first: burnrate join <name>');
287
+ return;
288
+ }
289
+ const price = options.limit ? parseInt(options.limit) : 100; // Default high limit
290
+ const qty = parseInt(quantity);
291
+ const result = engine.placeOrder(player.id, player.locationId, resource, 'buy', price, qty);
292
+ if (result.success) {
293
+ console.log(`\n✓ Buy order placed: ${qty} ${resource} at ${price} cr/unit\n`);
294
+ }
295
+ else {
296
+ console.log(`\n✗ ${result.error}\n`);
297
+ }
298
+ });
299
+ program
300
+ .command('sell')
301
+ .description('Place a sell order')
302
+ .argument('<resource>', 'Resource to sell')
303
+ .argument('<quantity>', 'Amount to sell')
304
+ .option('--limit <price>', 'Min price per unit')
305
+ .action((resource, quantity, options) => {
306
+ const player = getPlayer();
307
+ if (!player) {
308
+ console.log('Join the game first: burnrate join <name>');
309
+ return;
310
+ }
311
+ const price = options.limit ? parseInt(options.limit) : 1; // Default low limit
312
+ const qty = parseInt(quantity);
313
+ const result = engine.placeOrder(player.id, player.locationId, resource, 'sell', price, qty);
314
+ if (result.success) {
315
+ console.log(`\n✓ Sell order placed: ${qty} ${resource} at ${price} cr/unit\n`);
316
+ }
317
+ else {
318
+ console.log(`\n✗ ${result.error}\n`);
319
+ }
320
+ });
321
+ // ============================================================================
322
+ // SCAN COMMAND
323
+ // ============================================================================
324
+ program
325
+ .command('scan')
326
+ .description('Gather intel on a zone or route')
327
+ .argument('<target>', 'Zone name or route ID')
328
+ .action((target) => {
329
+ const player = getPlayer();
330
+ if (!player) {
331
+ console.log('Join the game first: burnrate join <name>');
332
+ return;
333
+ }
334
+ // Try to find as zone first
335
+ const allZones = db.getAllZones();
336
+ const zone = allZones.find(z => z.name.toLowerCase() === target.toLowerCase());
337
+ if (zone) {
338
+ const result = engine.scan(player.id, 'zone', zone.id);
339
+ if (result.success && result.intel) {
340
+ console.log(`\n┌─ INTEL: ${zone.name} ───────────────────────────┐`);
341
+ console.log(`│ Type: ${result.intel.type}`);
342
+ console.log(`│ Owner: ${result.intel.owner || 'Neutral'}`);
343
+ console.log(`│ Supply: ${result.intel.supplyState} (${Math.round(result.intel.supplyLevel)}%)`);
344
+ console.log(`│ Market Activity: ${result.intel.marketActivity} orders`);
345
+ console.log(`└───────────────────────────────────────────────────┘\n`);
346
+ }
347
+ else {
348
+ console.log(`\n✗ ${result.error}\n`);
349
+ }
350
+ }
351
+ else {
352
+ console.log(`Zone not found: ${target}`);
353
+ }
354
+ });
355
+ // ============================================================================
356
+ // SUPPLY COMMAND
357
+ // ============================================================================
358
+ program
359
+ .command('supply')
360
+ .description('Deposit Supply Units to a zone')
361
+ .argument('<amount>', 'Amount of SU to deposit')
362
+ .action((amount) => {
363
+ const player = getPlayer();
364
+ if (!player) {
365
+ console.log('Join the game first: burnrate join <name>');
366
+ return;
367
+ }
368
+ const result = engine.depositSU(player.id, player.locationId, parseInt(amount));
369
+ if (result.success) {
370
+ const zone = db.getZone(player.locationId);
371
+ console.log(`\n✓ Deposited ${amount} SU to ${zone?.name}`);
372
+ console.log(` New stockpile: ${zone?.suStockpile} SU\n`);
373
+ }
374
+ else {
375
+ console.log(`\n✗ ${result.error}\n`);
376
+ }
377
+ });
378
+ // ============================================================================
379
+ // TICK COMMAND (for testing/development)
380
+ // ============================================================================
381
+ program
382
+ .command('tick')
383
+ .description('Process a game tick (dev/testing)')
384
+ .option('--count <n>', 'Number of ticks to process', '1')
385
+ .action((options) => {
386
+ const count = parseInt(options.count);
387
+ for (let i = 0; i < count; i++) {
388
+ const result = engine.processTick();
389
+ if (i === count - 1) {
390
+ console.log(`\nTick ${result.tick} processed. ${result.events.length} events.\n`);
391
+ }
392
+ }
393
+ });
394
+ // ============================================================================
395
+ // DEV COMMANDS
396
+ // ============================================================================
397
+ program
398
+ .command('dev:grant')
399
+ .description('Grant resources to yourself (dev/testing)')
400
+ .argument('<resource>', 'Resource to grant (or "starter" for a starter pack)')
401
+ .argument('[amount]', 'Amount to grant', '100')
402
+ .action((resource, amount) => {
403
+ const player = getPlayer();
404
+ if (!player) {
405
+ console.log('Join the game first: burnrate join <name>');
406
+ return;
407
+ }
408
+ const newInventory = { ...player.inventory };
409
+ if (resource === 'starter') {
410
+ // Starter pack for testing
411
+ newInventory.credits += 5000;
412
+ newInventory.ore += 100;
413
+ newInventory.fuel += 100;
414
+ newInventory.grain += 100;
415
+ newInventory.fiber += 100;
416
+ newInventory.rations += 50;
417
+ newInventory.parts += 50;
418
+ newInventory.ammo += 50;
419
+ console.log('\n✓ Starter pack granted: 5000 cr + raw materials + supplies\n');
420
+ }
421
+ else if (resource === 'credits') {
422
+ newInventory.credits += parseInt(amount);
423
+ console.log(`\n✓ Granted ${amount} credits\n`);
424
+ }
425
+ else if (resource in newInventory) {
426
+ newInventory[resource] += parseInt(amount);
427
+ console.log(`\n✓ Granted ${amount} ${resource}\n`);
428
+ }
429
+ else {
430
+ console.log(`Unknown resource: ${resource}`);
431
+ return;
432
+ }
433
+ db.updatePlayer(player.id, { inventory: newInventory });
434
+ });
435
+ program
436
+ .command('dev:move')
437
+ .description('Teleport to a zone (dev/testing)')
438
+ .argument('<zone>', 'Zone name to move to')
439
+ .action((zoneName) => {
440
+ const player = getPlayer();
441
+ if (!player) {
442
+ console.log('Join the game first: burnrate join <name>');
443
+ return;
444
+ }
445
+ const allZones = db.getAllZones();
446
+ const zone = allZones.find(z => z.name.toLowerCase() === zoneName.toLowerCase());
447
+ if (!zone) {
448
+ console.log(`Zone not found: ${zoneName}`);
449
+ console.log('Available zones:', allZones.map(z => z.name).join(', '));
450
+ return;
451
+ }
452
+ db.updatePlayer(player.id, { locationId: zone.id });
453
+ console.log(`\n✓ Teleported to ${zone.name}\n`);
454
+ });
455
+ // ============================================================================
456
+ // FACTION COMMANDS
457
+ // ============================================================================
458
+ program
459
+ .command('faction')
460
+ .description('Faction management')
461
+ .argument('<action>', 'Action: create, info, join, leave, deposit, members')
462
+ .argument('[args...]', 'Action arguments')
463
+ .action((action, args) => {
464
+ const player = getPlayer();
465
+ if (!player) {
466
+ console.log('Join the game first: burnrate join <name>');
467
+ return;
468
+ }
469
+ switch (action) {
470
+ case 'create': {
471
+ if (args.length < 2) {
472
+ console.log('Usage: burnrate faction create <name> <tag>');
473
+ return;
474
+ }
475
+ const [name, tag] = args;
476
+ if (player.factionId) {
477
+ console.log('You are already in a faction. Leave first.');
478
+ return;
479
+ }
480
+ const faction = db.createFaction(name, tag.toUpperCase(), player.id);
481
+ console.log(`\n✓ Faction "${faction.name}" [${faction.tag}] created!`);
482
+ console.log(` You are the Founder.\n`);
483
+ break;
484
+ }
485
+ case 'info': {
486
+ const factionId = player.factionId || args[0];
487
+ if (!factionId) {
488
+ console.log('You are not in a faction. Specify faction ID or join one.');
489
+ return;
490
+ }
491
+ const faction = db.getFaction(factionId);
492
+ if (!faction) {
493
+ console.log('Faction not found.');
494
+ return;
495
+ }
496
+ console.log(`\n┌─ FACTION: ${faction.name} [${faction.tag}] ───────────────────────┐`);
497
+ console.log(`│ Members: ${faction.members.length}/50`);
498
+ console.log(`│ Controlled Zones: ${faction.controlledZones.length}`);
499
+ console.log(`│ Treasury: ${faction.treasury.credits} cr`);
500
+ console.log(`│ Upgrades:`);
501
+ console.log(`│ Relay Network: ${faction.upgrades.relayNetwork}`);
502
+ console.log(`│ Route Fortification: ${faction.upgrades.routeFortification}`);
503
+ console.log(`│ Production Bonus: ${faction.upgrades.productionBonus}`);
504
+ console.log(`└────────────────────────────────────────────────────────────┘\n`);
505
+ break;
506
+ }
507
+ case 'members': {
508
+ if (!player.factionId) {
509
+ console.log('You are not in a faction.');
510
+ return;
511
+ }
512
+ const faction = db.getFaction(player.factionId);
513
+ if (!faction)
514
+ return;
515
+ console.log(`\n┌─ MEMBERS: ${faction.name} ────────────────────────────────────┐`);
516
+ for (const member of faction.members) {
517
+ const memberPlayer = db.getPlayer(member.playerId);
518
+ const rankIcon = member.rank === 'founder' ? '★' : member.rank === 'officer' ? '◆' : '○';
519
+ console.log(`│ ${rankIcon} ${memberPlayer?.name.padEnd(20)} ${member.rank.padEnd(10)} │`);
520
+ }
521
+ console.log(`└────────────────────────────────────────────────────────────┘\n`);
522
+ break;
523
+ }
524
+ case 'join': {
525
+ // For now, just join by faction name (in real game, would need invite)
526
+ if (args.length < 1) {
527
+ console.log('Usage: burnrate faction join <faction-name>');
528
+ return;
529
+ }
530
+ if (player.factionId) {
531
+ console.log('You are already in a faction. Leave first.');
532
+ return;
533
+ }
534
+ const factions = db.getAllFactions();
535
+ const faction = factions.find(f => f.name.toLowerCase() === args[0].toLowerCase());
536
+ if (!faction) {
537
+ console.log('Faction not found.');
538
+ return;
539
+ }
540
+ if (faction.members.length >= 50) {
541
+ console.log('Faction is full (50 members max).');
542
+ return;
543
+ }
544
+ db.addFactionMember(faction.id, player.id, 'member');
545
+ console.log(`\n✓ Joined ${faction.name} [${faction.tag}] as Member\n`);
546
+ break;
547
+ }
548
+ case 'leave': {
549
+ if (!player.factionId) {
550
+ console.log('You are not in a faction.');
551
+ return;
552
+ }
553
+ const faction = db.getFaction(player.factionId);
554
+ if (!faction)
555
+ return;
556
+ if (faction.founderId === player.id) {
557
+ console.log('Founders cannot leave. Transfer ownership first or disband.');
558
+ return;
559
+ }
560
+ db.removeFactionMember(faction.id, player.id);
561
+ console.log(`\n✓ Left ${faction.name}\n`);
562
+ break;
563
+ }
564
+ case 'deposit': {
565
+ if (!player.factionId) {
566
+ console.log('You are not in a faction.');
567
+ return;
568
+ }
569
+ if (args.length < 1) {
570
+ console.log('Usage: burnrate faction deposit <amount>');
571
+ return;
572
+ }
573
+ const amount = parseInt(args[0]);
574
+ if (player.inventory.credits < amount) {
575
+ console.log('Insufficient credits.');
576
+ return;
577
+ }
578
+ const faction = db.getFaction(player.factionId);
579
+ if (!faction)
580
+ return;
581
+ // Update player
582
+ const newPlayerInv = { ...player.inventory };
583
+ newPlayerInv.credits -= amount;
584
+ db.updatePlayer(player.id, { inventory: newPlayerInv });
585
+ // Update faction treasury (need to add this to db)
586
+ console.log(`\n✓ Deposited ${amount} cr to ${faction.name} treasury\n`);
587
+ break;
588
+ }
589
+ case 'intel': {
590
+ if (!player.factionId) {
591
+ console.log('You are not in a faction.');
592
+ return;
593
+ }
594
+ const faction = db.getFaction(player.factionId);
595
+ if (!faction)
596
+ return;
597
+ const limit = args[0] ? parseInt(args[0]) : 20;
598
+ const intelReports = db.getFactionIntel(player.factionId, limit);
599
+ const tick = db.getCurrentTick();
600
+ console.log(`\n┌─ FACTION INTEL: ${faction.name} [${faction.tag}] ─────────────────────┐`);
601
+ if (intelReports.length === 0) {
602
+ console.log(`│ No intel gathered. Use 'burnrate scan <zone>' to gather. │`);
603
+ }
604
+ else {
605
+ console.log(`│ Target │ Age │ Quality │ Key Info │`);
606
+ console.log(`├──────────────────────┼──────┼─────────┼─────────────────────┤`);
607
+ for (const report of intelReports) {
608
+ const age = tick - report.gatheredAt;
609
+ const ageStr = `${age}t`.padStart(4);
610
+ const qualityStr = `${report.signalQuality}%`.padStart(4);
611
+ let targetName = report.targetId.slice(0, 18);
612
+ let keyInfo = '';
613
+ if (report.targetType === 'zone') {
614
+ const zone = db.getZone(report.targetId);
615
+ targetName = (zone?.name || report.targetId).slice(0, 18).padEnd(18);
616
+ const data = report.data;
617
+ keyInfo = `${data.supplyState || '?'} ${data.suStockpile || 0}SU`;
618
+ }
619
+ else {
620
+ targetName = 'Route'.padEnd(18);
621
+ const data = report.data;
622
+ keyInfo = data.raiderPresence ? `Raiders! str:${data.raiderStrength}` : 'Clear';
623
+ }
624
+ console.log(`│ ${targetName.padEnd(20)} │ ${ageStr} │ ${qualityStr} │ ${keyInfo.padEnd(19)} │`);
625
+ }
626
+ }
627
+ console.log(`└──────────────────────┴──────┴─────────┴─────────────────────┘`);
628
+ console.log(`\nIntel decays over time. Fresh scans yield better data.\n`);
629
+ break;
630
+ }
631
+ default:
632
+ console.log('Unknown faction action. Try: create, info, join, leave, deposit, members, intel');
633
+ }
634
+ });
635
+ // ============================================================================
636
+ // CONTRACT COMMANDS
637
+ // ============================================================================
638
+ program
639
+ .command('contracts')
640
+ .description('View and manage contracts')
641
+ .argument('[action]', 'Action: list, post, accept, complete')
642
+ .argument('[args...]', 'Action arguments')
643
+ .action((action, args) => {
644
+ const player = getPlayer();
645
+ if (!player) {
646
+ console.log('Join the game first: burnrate join <name>');
647
+ return;
648
+ }
649
+ const tick = db.getCurrentTick();
650
+ if (!action || action === 'list') {
651
+ // List available contracts
652
+ const contracts = db.getOpenContracts();
653
+ console.log(`\n┌─ AVAILABLE CONTRACTS ────────────────────────────────────────┐`);
654
+ if (contracts.length === 0) {
655
+ console.log(`│ No open contracts │`);
656
+ }
657
+ else {
658
+ for (const c of contracts.slice(0, 10)) {
659
+ const deadline = c.deadline - tick;
660
+ const typeStr = c.type.toUpperCase().padEnd(8);
661
+ const rewardStr = `${c.reward.credits} cr`.padEnd(10);
662
+ console.log(`│ ${c.id.slice(0, 8)} ${typeStr} ${rewardStr} ${deadline}t left │`);
663
+ }
664
+ }
665
+ console.log(`└──────────────────────────────────────────────────────────────┘\n`);
666
+ return;
667
+ }
668
+ switch (action) {
669
+ case 'post': {
670
+ // Post a new haul contract
671
+ if (args.length < 5) {
672
+ console.log('Usage: burnrate contracts post <type> <from> <to> <resource:qty> <reward>');
673
+ console.log('Example: burnrate contracts post haul Hub.Central Factory.North fuel:20 500');
674
+ return;
675
+ }
676
+ const [type, from, to, cargoStr, rewardStr] = args;
677
+ if (type !== 'haul') {
678
+ console.log('Currently only "haul" contracts are supported.');
679
+ return;
680
+ }
681
+ const allZones = db.getAllZones();
682
+ const fromZone = allZones.find(z => z.name.toLowerCase() === from.toLowerCase());
683
+ const toZone = allZones.find(z => z.name.toLowerCase() === to.toLowerCase());
684
+ if (!fromZone || !toZone) {
685
+ console.log('Zone not found.');
686
+ return;
687
+ }
688
+ const [resource, qtyStr] = cargoStr.split(':');
689
+ const qty = parseInt(qtyStr);
690
+ const reward = parseInt(rewardStr);
691
+ if (player.inventory.credits < reward) {
692
+ console.log('Insufficient credits to fund contract reward.');
693
+ return;
694
+ }
695
+ // Deduct reward from player
696
+ const newInv = { ...player.inventory };
697
+ newInv.credits -= reward;
698
+ db.updatePlayer(player.id, { inventory: newInv });
699
+ const contract = db.createContract({
700
+ type: 'haul',
701
+ posterId: player.id,
702
+ posterType: 'player',
703
+ acceptedBy: null,
704
+ details: {
705
+ fromZoneId: fromZone.id,
706
+ toZoneId: toZone.id,
707
+ resource: resource,
708
+ quantity: qty
709
+ },
710
+ deadline: tick + 100,
711
+ reward: { credits: reward, reputation: 10 },
712
+ status: 'open',
713
+ createdAt: tick
714
+ });
715
+ console.log(`\n✓ Contract posted: ${contract.id.slice(0, 8)}`);
716
+ console.log(` Haul ${qty} ${resource} from ${fromZone.name} to ${toZone.name}`);
717
+ console.log(` Reward: ${reward} cr Deadline: 100 ticks\n`);
718
+ break;
719
+ }
720
+ case 'accept': {
721
+ if (args.length < 1) {
722
+ console.log('Usage: burnrate contracts accept <contract-id>');
723
+ return;
724
+ }
725
+ const contracts = db.getOpenContracts();
726
+ const contract = contracts.find(c => c.id.startsWith(args[0]));
727
+ if (!contract) {
728
+ console.log('Contract not found or already taken.');
729
+ return;
730
+ }
731
+ db.updateContract(contract.id, { status: 'active', acceptedBy: player.id });
732
+ console.log(`\n✓ Contract ${contract.id.slice(0, 8)} accepted\n`);
733
+ break;
734
+ }
735
+ default:
736
+ console.log('Unknown action. Try: list, post, accept');
737
+ }
738
+ });
739
+ // ============================================================================
740
+ // PRODUCE COMMAND
741
+ // ============================================================================
742
+ program
743
+ .command('produce')
744
+ .description('Convert resources at a Factory')
745
+ .argument('<output>', 'What to produce: metal, chemicals, rations, textiles, ammo, medkits, parts, comms, escort, raider')
746
+ .argument('<quantity>', 'How many units to produce')
747
+ .action((output, quantity) => {
748
+ const player = getPlayer();
749
+ if (!player) {
750
+ console.log('Join the game first: burnrate join <name>');
751
+ return;
752
+ }
753
+ const result = engine.produce(player.id, output, parseInt(quantity));
754
+ if (result.success) {
755
+ if (result.units && result.units.length > 0) {
756
+ // Unit production
757
+ const unit = result.units[0];
758
+ console.log(`\n✓ Produced ${result.produced} ${output} unit(s)`);
759
+ console.log(` Strength: ${unit.strength} Speed: ${unit.speed} Maintenance: ${unit.maintenance} cr/tick`);
760
+ console.log(` Use 'burnrate units' to view your units\n`);
761
+ }
762
+ else {
763
+ // Resource production
764
+ console.log(`\n✓ Produced ${result.produced} ${output}\n`);
765
+ }
766
+ }
767
+ else {
768
+ console.log(`\n✗ ${result.error}\n`);
769
+ }
770
+ });
771
+ // ============================================================================
772
+ // EXTRACT COMMAND
773
+ // ============================================================================
774
+ program
775
+ .command('extract')
776
+ .description('Extract raw resources from a Field')
777
+ .argument('<quantity>', 'How many units to extract')
778
+ .action((quantity) => {
779
+ const player = getPlayer();
780
+ if (!player) {
781
+ console.log('Join the game first: burnrate join <name>');
782
+ return;
783
+ }
784
+ const result = engine.extract(player.id, parseInt(quantity));
785
+ if (result.success && result.extracted) {
786
+ console.log(`\n✓ Extracted ${result.extracted.amount} ${result.extracted.resource}\n`);
787
+ }
788
+ else {
789
+ console.log(`\n✗ ${result.error}\n`);
790
+ }
791
+ });
792
+ // ============================================================================
793
+ // CAPTURE COMMAND
794
+ // ============================================================================
795
+ program
796
+ .command('capture')
797
+ .description('Capture a neutral or collapsed zone for your faction')
798
+ .action(() => {
799
+ const player = getPlayer();
800
+ if (!player) {
801
+ console.log('Join the game first: burnrate join <name>');
802
+ return;
803
+ }
804
+ const zone = db.getZone(player.locationId);
805
+ if (!zone) {
806
+ console.log('Zone not found');
807
+ return;
808
+ }
809
+ const result = engine.captureZone(player.id, player.locationId);
810
+ if (result.success) {
811
+ const faction = player.factionId ? db.getFaction(player.factionId) : null;
812
+ console.log(`\n✓ ${zone.name} captured for ${faction?.name || 'your faction'}!\n`);
813
+ }
814
+ else {
815
+ console.log(`\n✗ ${result.error}\n`);
816
+ }
817
+ });
818
+ // ============================================================================
819
+ // UNITS COMMAND
820
+ // ============================================================================
821
+ program
822
+ .command('units')
823
+ .description('View and manage your combat units')
824
+ .argument('[action]', 'Action: list, escort, raider')
825
+ .argument('[args...]', 'Action arguments')
826
+ .action((action, args) => {
827
+ const player = getPlayer();
828
+ if (!player) {
829
+ console.log('Join the game first: burnrate join <name>');
830
+ return;
831
+ }
832
+ const units = db.getPlayerUnits(player.id);
833
+ if (!action || action === 'list') {
834
+ console.log(`\n┌─ YOUR UNITS ─────────────────────────────────────────────┐`);
835
+ if (units.length === 0) {
836
+ console.log(`│ No units. Hire at Hubs: burnrate hire escort │`);
837
+ }
838
+ else {
839
+ for (const u of units) {
840
+ const location = db.getZone(u.locationId);
841
+ const assignStr = u.assignmentId ? `→ ${u.assignmentId.slice(0, 8)}` : 'idle';
842
+ console.log(`│ ${u.id.slice(0, 8)} ${u.type.padEnd(8)} str:${u.strength.toString().padEnd(3)} ${assignStr.padEnd(14)} ${location?.name.slice(0, 15) || ''} │`);
843
+ }
844
+ }
845
+ console.log(`└───────────────────────────────────────────────────────────┘\n`);
846
+ return;
847
+ }
848
+ switch (action) {
849
+ case 'escort': {
850
+ // Assign escort to shipment
851
+ if (args.length < 2) {
852
+ console.log('Usage: burnrate units escort <unit-id> <shipment-id>');
853
+ return;
854
+ }
855
+ const [unitId, shipmentId] = args;
856
+ const unit = units.find(u => u.id.startsWith(unitId));
857
+ if (!unit) {
858
+ console.log('Unit not found. Use "burnrate units" to list your units.');
859
+ return;
860
+ }
861
+ const shipments = db.getPlayerShipments(player.id);
862
+ const shipment = shipments.find(s => s.id.startsWith(shipmentId));
863
+ if (!shipment) {
864
+ console.log('Shipment not found. Use "burnrate view shipments" to list.');
865
+ return;
866
+ }
867
+ const result = engine.assignEscort(player.id, unit.id, shipment.id);
868
+ if (result.success) {
869
+ console.log(`\n✓ Escort ${unit.id.slice(0, 8)} assigned to shipment ${shipment.id.slice(0, 8)}\n`);
870
+ }
871
+ else {
872
+ console.log(`\n✗ ${result.error}\n`);
873
+ }
874
+ break;
875
+ }
876
+ case 'raider': {
877
+ // Deploy raider on route
878
+ if (args.length < 2) {
879
+ console.log('Usage: burnrate units raider <unit-id> <from-zone> <to-zone>');
880
+ console.log('Example: burnrate units raider abc123 Hub.Central Factory.North');
881
+ return;
882
+ }
883
+ const [unitId, ...routeArgs] = args;
884
+ const unit = units.find(u => u.id.startsWith(unitId));
885
+ if (!unit) {
886
+ console.log('Unit not found. Use "burnrate units" to list your units.');
887
+ return;
888
+ }
889
+ // Find route between zones
890
+ if (routeArgs.length < 2) {
891
+ console.log('Specify both from and to zones.');
892
+ return;
893
+ }
894
+ const allZones = db.getAllZones();
895
+ const fromZone = allZones.find(z => z.name.toLowerCase() === routeArgs[0].toLowerCase());
896
+ const toZone = allZones.find(z => z.name.toLowerCase() === routeArgs[1].toLowerCase());
897
+ if (!fromZone || !toZone) {
898
+ console.log('Zone not found.');
899
+ return;
900
+ }
901
+ const routes = db.getRoutesBetween(fromZone.id, toZone.id);
902
+ if (routes.length === 0) {
903
+ console.log('No route between those zones.');
904
+ return;
905
+ }
906
+ const result = engine.deployRaider(player.id, unit.id, routes[0].id);
907
+ if (result.success) {
908
+ console.log(`\n✓ Raider ${unit.id.slice(0, 8)} deployed on ${fromZone.name} → ${toZone.name}\n`);
909
+ }
910
+ else {
911
+ console.log(`\n✗ ${result.error}\n`);
912
+ }
913
+ break;
914
+ }
915
+ case 'sell': {
916
+ // List unit for sale
917
+ if (args.length < 2) {
918
+ console.log('Usage: burnrate units sell <unit-id> <price>');
919
+ return;
920
+ }
921
+ const [sellUnitId, priceStr] = args;
922
+ const unitToSell = units.find(u => u.id.startsWith(sellUnitId));
923
+ if (!unitToSell) {
924
+ console.log('Unit not found. Use "burnrate units" to list your units.');
925
+ return;
926
+ }
927
+ const price = parseInt(priceStr);
928
+ if (isNaN(price) || price < 1) {
929
+ console.log('Price must be a positive number.');
930
+ return;
931
+ }
932
+ const sellResult = engine.listUnitForSale(player.id, unitToSell.id, price);
933
+ if (sellResult.success) {
934
+ console.log(`\n✓ ${unitToSell.type} ${unitToSell.id.slice(0, 8)} listed for ${price} cr`);
935
+ console.log(` Location: ${db.getZone(unitToSell.locationId)?.name}\n`);
936
+ }
937
+ else {
938
+ console.log(`\n✗ ${sellResult.error}\n`);
939
+ }
940
+ break;
941
+ }
942
+ case 'unsell': {
943
+ // Remove unit from sale
944
+ if (args.length < 1) {
945
+ console.log('Usage: burnrate units unsell <unit-id>');
946
+ return;
947
+ }
948
+ const unitToUnsell = units.find(u => u.id.startsWith(args[0]));
949
+ if (!unitToUnsell) {
950
+ console.log('Unit not found.');
951
+ return;
952
+ }
953
+ const unsellResult = engine.unlistUnit(player.id, unitToUnsell.id);
954
+ if (unsellResult.success) {
955
+ console.log(`\n✓ ${unitToUnsell.type} ${unitToUnsell.id.slice(0, 8)} removed from sale\n`);
956
+ }
957
+ else {
958
+ console.log(`\n✗ ${unsellResult.error}\n`);
959
+ }
960
+ break;
961
+ }
962
+ default:
963
+ console.log('Unknown action. Try: list, escort, raider, sell, unsell');
964
+ }
965
+ });
966
+ // ============================================================================
967
+ // HIRE COMMAND (buy units from marketplace)
968
+ // ============================================================================
969
+ program
970
+ .command('hire')
971
+ .description('Hire (buy) a unit from the marketplace at your current Hub')
972
+ .argument('[unit-id]', 'Unit ID to hire (omit to list available units)')
973
+ .action((unitId) => {
974
+ const player = getPlayer();
975
+ if (!player) {
976
+ console.log('Join the game first: burnrate join <name>');
977
+ return;
978
+ }
979
+ const zone = db.getZone(player.locationId);
980
+ if (!zone || zone.type !== 'hub') {
981
+ console.log('\nUnits can only be hired at Hubs. Travel to a Hub first.\n');
982
+ return;
983
+ }
984
+ const unitsForSale = db.getUnitsForSaleAtZone(player.locationId);
985
+ if (!unitId) {
986
+ // List available units
987
+ console.log(`\n┌─ UNITS FOR HIRE: ${zone.name} ────────────────────────────────┐`);
988
+ if (unitsForSale.length === 0) {
989
+ console.log(`│ No units for sale. Players must produce and list units. │`);
990
+ }
991
+ else {
992
+ console.log(`│ ID │ Type │ Str │ Spd │ Maint │ Price │`);
993
+ console.log(`├───────────┼─────────┼─────┼─────┼───────┼───────────┤`);
994
+ for (const u of unitsForSale) {
995
+ const seller = db.getPlayer(u.playerId);
996
+ const sellerName = seller?.name.slice(0, 10) || 'Unknown';
997
+ console.log(`│ ${u.id.slice(0, 8)} │ ${u.type.padEnd(7)} │ ${u.strength.toString().padStart(3)} │ ${u.speed.toString().padStart(3)} │ ${(u.maintenance + '/t').padStart(5)} │ ${(u.forSalePrice + ' cr').padStart(9)} │`);
998
+ }
999
+ }
1000
+ console.log(`└───────────┴─────────┴─────┴─────┴───────┴───────────┘`);
1001
+ console.log(`\nTo hire: burnrate hire <unit-id>\n`);
1002
+ return;
1003
+ }
1004
+ // Find and hire unit
1005
+ const unit = unitsForSale.find(u => u.id.startsWith(unitId));
1006
+ if (!unit) {
1007
+ console.log(`\nUnit not found or not for sale. Use 'burnrate hire' to list available units.\n`);
1008
+ return;
1009
+ }
1010
+ const result = engine.hireUnit(player.id, unit.id);
1011
+ if (result.success && result.unit) {
1012
+ console.log(`\n✓ Hired ${result.unit.type} for ${unit.forSalePrice} cr`);
1013
+ console.log(` Strength: ${result.unit.strength} Maintenance: ${result.unit.maintenance} cr/tick`);
1014
+ console.log(` Use 'burnrate units' to view your units\n`);
1015
+ }
1016
+ else {
1017
+ console.log(`\n✗ ${result.error}\n`);
1018
+ }
1019
+ });
1020
+ // ============================================================================
1021
+ // STATUS COMMAND
1022
+ // ============================================================================
1023
+ program
1024
+ .command('status')
1025
+ .description('Show your current status')
1026
+ .action(() => {
1027
+ const player = getPlayer();
1028
+ if (!player) {
1029
+ console.log('Join the game first: burnrate join <name>');
1030
+ return;
1031
+ }
1032
+ const zone = db.getZone(player.locationId);
1033
+ const tick = db.getCurrentTick();
1034
+ const limits = TIER_LIMITS[player.tier];
1035
+ console.log(`\n╔═══════════════════════════════════════════════════════╗`);
1036
+ console.log(`║ ${player.name.padEnd(20)} │ ${player.tier.toUpperCase().padEnd(10)} │ Tick ${tick.toString().padStart(6)} ║`);
1037
+ console.log(`╠═══════════════════════════════════════════════════════╣`);
1038
+ console.log(`║ Location: ${(zone?.name || 'Unknown').padEnd(43)} ║`);
1039
+ console.log(`║ Credits: ${player.inventory.credits.toString().padEnd(44)} ║`);
1040
+ console.log(`║ Reputation: ${player.reputation.toString().padEnd(41)} ║`);
1041
+ console.log(`║ Actions: ${player.actionsToday}/${limits.dailyActions} today`.padEnd(56) + `║`);
1042
+ console.log(`╠═══════════════════════════════════════════════════════╣`);
1043
+ console.log(`║ INVENTORY ║`);
1044
+ const inv = player.inventory;
1045
+ console.log(`║ ore: ${inv.ore.toString().padEnd(6)} fuel: ${inv.fuel.toString().padEnd(6)} grain: ${inv.grain.toString().padEnd(6)} fiber: ${inv.fiber.toString().padEnd(4)} ║`);
1046
+ console.log(`║ metal: ${inv.metal.toString().padEnd(4)} chem: ${inv.chemicals.toString().padEnd(6)} rations: ${inv.rations.toString().padEnd(4)} text: ${inv.textiles.toString().padEnd(5)} ║`);
1047
+ console.log(`║ ammo: ${inv.ammo.toString().padEnd(5)} med: ${inv.medkits.toString().padEnd(7)} parts: ${inv.parts.toString().padEnd(6)} comms: ${inv.comms.toString().padEnd(4)} ║`);
1048
+ console.log(`╚═══════════════════════════════════════════════════════╝\n`);
1049
+ });
1050
+ // ============================================================================
1051
+ // SERVER COMMAND
1052
+ // ============================================================================
1053
+ program
1054
+ .command('server')
1055
+ .description('Manage the tick server')
1056
+ .argument('<action>', 'Action: status, start, stop')
1057
+ .option('--interval <ms>', 'Tick interval in milliseconds (start only)', '600000')
1058
+ .action((action, options) => {
1059
+ const statusPath = path.join(DB_DIR, 'server-status.json');
1060
+ const pidPath = path.join(DB_DIR, 'server.pid');
1061
+ switch (action) {
1062
+ case 'status': {
1063
+ if (!fs.existsSync(statusPath)) {
1064
+ console.log('\nServer status: Not running\n');
1065
+ return;
1066
+ }
1067
+ try {
1068
+ const status = JSON.parse(fs.readFileSync(statusPath, 'utf-8'));
1069
+ const lastTick = new Date(status.lastTickTime);
1070
+ const elapsed = Math.floor((Date.now() - lastTick.getTime()) / 1000);
1071
+ console.log(`\n┌─ TICK SERVER STATUS ────────────────────────────────────┐`);
1072
+ console.log(`│ Status: ${status.running ? 'RUNNING' : 'STOPPED'}`.padEnd(58) + `│`);
1073
+ console.log(`│ PID: ${status.pid}`.padEnd(58) + `│`);
1074
+ console.log(`│ Current Tick: ${status.currentTick}`.padEnd(58) + `│`);
1075
+ console.log(`│ Ticks Processed: ${status.tickCount}`.padEnd(58) + `│`);
1076
+ console.log(`│ Tick Interval: ${status.intervalMs / 60000} minutes`.padEnd(58) + `│`);
1077
+ console.log(`│ Last Tick: ${elapsed}s ago`.padEnd(58) + `│`);
1078
+ console.log(`└──────────────────────────────────────────────────────────┘\n`);
1079
+ }
1080
+ catch {
1081
+ console.log('\nServer status: Unknown (corrupted status file)\n');
1082
+ }
1083
+ break;
1084
+ }
1085
+ case 'start': {
1086
+ console.log('\nTo start the tick server, run in a separate terminal:');
1087
+ console.log(` BURNRATE_TICK_INTERVAL=${options.interval} node dist/server/tick-server.js`);
1088
+ console.log('\nOr for testing (1 tick per second):');
1089
+ console.log(' BURNRATE_TICK_INTERVAL=1000 node dist/server/tick-server.js\n');
1090
+ break;
1091
+ }
1092
+ case 'stop': {
1093
+ if (!fs.existsSync(statusPath)) {
1094
+ console.log('\nServer is not running\n');
1095
+ return;
1096
+ }
1097
+ try {
1098
+ const status = JSON.parse(fs.readFileSync(statusPath, 'utf-8'));
1099
+ console.log(`\nTo stop the server, kill process ${status.pid}:`);
1100
+ console.log(` kill ${status.pid}\n`);
1101
+ }
1102
+ catch {
1103
+ console.log('\nCould not read server status\n');
1104
+ }
1105
+ break;
1106
+ }
1107
+ default:
1108
+ console.log('Unknown action. Try: status, start, stop');
1109
+ }
1110
+ });
1111
+ // ============================================================================
1112
+ // HELP COMMAND
1113
+ // ============================================================================
1114
+ program
1115
+ .command('help')
1116
+ .description('Show detailed help')
1117
+ .action(() => {
1118
+ console.log(formatHelp());
1119
+ });
1120
+ // Parse and execute
1121
+ program.parse();