vibemancer 1.0.35 → 1.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1199,7 +1199,7 @@ async function listMyBotsPage() {
1199
1199
  truncated: obj.truncated === true
1200
1200
  };
1201
1201
  }
1202
- async function pullSource(name) {
1202
+ async function pullBot(name) {
1203
1203
  const token = await getAccessToken();
1204
1204
  const res = await fetch(`${MCP_BASE_URL}/api/pull?name=${encodeURIComponent(name)}`, {
1205
1205
  headers: { Authorization: `Bearer ${token}`, ...envHeaders() },
@@ -1208,10 +1208,16 @@ async function pullSource(name) {
1208
1208
  if (!res.ok) throw new Error(`Pull failed (${res.status}): ${await res.text()}`);
1209
1209
  const data = await res.json();
1210
1210
  const source = typeof data === "object" && data !== null && "sourceCode" in data && typeof data.sourceCode === "string" ? data.sourceCode : "";
1211
- if (source) return source;
1211
+ if (source) {
1212
+ const canonical = typeof data === "object" && data !== null && "name" in data && typeof data.name === "string" && data.name ? data.name : name;
1213
+ return { sourceCode: source, name: canonical };
1214
+ }
1212
1215
  const error = typeof data === "object" && data !== null && "error" in data && typeof data.error === "string" ? data.error : "No source returned.";
1213
1216
  throw new Error(error);
1214
1217
  }
1218
+ async function pullSource(name) {
1219
+ return (await pullBot(name)).sourceCode;
1220
+ }
1215
1221
 
1216
1222
  // src/commands/bots.ts
1217
1223
  async function runBots(options) {
@@ -1370,7 +1376,52 @@ function getStyleDescription(bot) {
1370
1376
  }
1371
1377
 
1372
1378
  // src/commands/upload.ts
1379
+ import fs6 from "fs";
1380
+
1381
+ // src/pull-safety.ts
1382
+ import { createHash } from "crypto";
1373
1383
  import fs5 from "fs";
1384
+ import path6 from "path";
1385
+ function hashContent(content) {
1386
+ const normalised = content.replace(/\r\n/g, "\n").trimEnd();
1387
+ return createHash("sha256").update(normalised, "utf8").digest("hex");
1388
+ }
1389
+ function pullDecision(situation) {
1390
+ if (!situation.fileExists) return "write";
1391
+ if (situation.force) return "write";
1392
+ const local = situation.localContent;
1393
+ if (hashContent(local) === hashContent(situation.incomingContent)) return "skip-identical";
1394
+ if (situation.recordedHash === void 0) return "refuse-unknown";
1395
+ return hashContent(local) === situation.recordedHash ? "write" : "refuse-modified";
1396
+ }
1397
+ var STATE_FILE = ".vibemancer-state.json";
1398
+ function statePath(projectDir) {
1399
+ return path6.join(projectDir, STATE_FILE);
1400
+ }
1401
+ function readState(projectDir) {
1402
+ try {
1403
+ const parsed = JSON.parse(fs5.readFileSync(statePath(projectDir), "utf-8"));
1404
+ if (typeof parsed !== "object" || parsed === null) return {};
1405
+ const out = {};
1406
+ for (const [k, v] of Object.entries(parsed)) if (typeof v === "string") out[k] = v;
1407
+ return out;
1408
+ } catch {
1409
+ return {};
1410
+ }
1411
+ }
1412
+ function recordedHashFor(projectDir, filePath) {
1413
+ return readState(projectDir)[path6.relative(projectDir, filePath).split(path6.sep).join("/")];
1414
+ }
1415
+ function recordWritten(projectDir, filePath, content) {
1416
+ try {
1417
+ const state = readState(projectDir);
1418
+ state[path6.relative(projectDir, filePath).split(path6.sep).join("/")] = hashContent(content);
1419
+ fs5.writeFileSync(statePath(projectDir), JSON.stringify(state, null, 2) + String.fromCharCode(10));
1420
+ } catch {
1421
+ }
1422
+ }
1423
+
1424
+ // src/commands/upload.ts
1374
1425
  var MAX_BUNDLE_BYTES = 500 * 1024;
1375
1426
  async function runUpload(options) {
1376
1427
  const projectDir = process.cwd();
@@ -1386,7 +1437,7 @@ async function runUpload(options) {
1386
1437
  }
1387
1438
  let sourceCode = "";
1388
1439
  try {
1389
- sourceCode = fs5.readFileSync(botInfo.sourcePath, "utf-8");
1440
+ sourceCode = fs6.readFileSync(botInfo.sourcePath, "utf-8");
1390
1441
  } catch {
1391
1442
  }
1392
1443
  console.log(` Wizard: ${botInfo.exportName}`);
@@ -1399,6 +1450,7 @@ async function runUpload(options) {
1399
1450
  sourceCode,
1400
1451
  active: options.active
1401
1452
  });
1453
+ if (sourceCode) recordWritten(process.cwd(), botInfo.sourcePath, sourceCode);
1402
1454
  console.log(` \u2713 ${result.message}`);
1403
1455
  if (result.wizardId) console.log(` Wizard ID: ${result.wizardId}`);
1404
1456
  console.log(result.active === false ? ` "${botInfo.exportName}" is retired and no longer competing.
@@ -1416,20 +1468,20 @@ async function runUpload(options) {
1416
1468
  }
1417
1469
 
1418
1470
  // src/commands/pull.ts
1419
- import fs6 from "fs";
1420
- import path6 from "path";
1471
+ import fs7 from "fs";
1472
+ import path7 from "path";
1421
1473
  function pullTargetPath(projectDir, name) {
1422
1474
  const trimmed = name.trim();
1423
1475
  if (!trimmed) throw new Error("A bot name is required.");
1424
1476
  if (/[\u0000-\u001f\u007f]/.test(trimmed)) throw new Error(`Refusing a bot name with a control character: ${JSON.stringify(name)}`);
1425
- if (trimmed !== path6.basename(trimmed) || trimmed === "." || trimmed === ".." || trimmed.includes("/") || trimmed.includes("\\")) {
1477
+ if (trimmed !== path7.basename(trimmed) || trimmed === "." || trimmed === ".." || trimmed.includes("/") || trimmed.includes("\\")) {
1426
1478
  throw new Error(`Refusing a bot name that is not a plain file name: ${JSON.stringify(name)}`);
1427
1479
  }
1428
- return path6.resolve(projectDir, "src", `${trimmed}.ts`);
1480
+ return path7.resolve(projectDir, "src", `${trimmed}.ts`);
1429
1481
  }
1430
1482
  function readProjectConfig(projectDir) {
1431
1483
  try {
1432
- const raw = fs6.readFileSync(path6.join(projectDir, "vibemancer.json"), "utf-8");
1484
+ const raw = fs7.readFileSync(path7.join(projectDir, "vibemancer.json"), "utf-8");
1433
1485
  const parsed = JSON.parse(raw);
1434
1486
  if (typeof parsed !== "object" || parsed === null) return void 0;
1435
1487
  const bot = parsed.bot;
@@ -1448,26 +1500,52 @@ function pullDestination(projectDir, name, config) {
1448
1500
  const configuredExport = config?.export;
1449
1501
  if (!configured) return { path: fallback };
1450
1502
  if (configuredExport && configuredExport.toLowerCase() === name.trim().toLowerCase()) {
1451
- return { path: path6.resolve(projectDir, configured) };
1503
+ return { path: path7.resolve(projectDir, configured) };
1452
1504
  }
1453
1505
  return {
1454
1506
  path: fallback,
1455
1507
  warning: `Note: this project's vibemancer.json uses ${configuredExport ?? "another bot"} (${configured}), so upload/fight will keep using that, not the file just written.`
1456
1508
  };
1457
1509
  }
1458
- async function pullByName(projectDir, name) {
1459
- const destination = pullDestination(projectDir, name, readProjectConfig(projectDir));
1460
- const target = destination.path;
1461
- const isNew = !fs6.existsSync(target);
1510
+ function writeGuarded(projectDir, target, sourceCode, force) {
1511
+ const isNew = !fs7.existsSync(target);
1512
+ const decision = pullDecision(isNew ? { fileExists: false, incomingContent: sourceCode, force } : {
1513
+ fileExists: true,
1514
+ localContent: fs7.readFileSync(target, "utf-8"),
1515
+ incomingContent: sourceCode,
1516
+ recordedHash: recordedHashFor(projectDir, target),
1517
+ force
1518
+ });
1519
+ if (decision === "skip-identical") {
1520
+ console.log(` Already up to date: ${target}`);
1521
+ recordWritten(projectDir, target, sourceCode);
1522
+ return false;
1523
+ }
1524
+ if (decision !== "write") {
1525
+ const why = decision === "refuse-modified" ? "it has changes that were not pulled or uploaded from here" : "this CLI has never written it, so it may be work it did not create";
1526
+ console.error("");
1527
+ console.error(` Refusing to overwrite ${target}: ${why}.`);
1528
+ console.error(" Save or commit it first, or re-run with --force to replace it.");
1529
+ console.error("");
1530
+ process.exitCode = 1;
1531
+ return false;
1532
+ }
1533
+ fs7.mkdirSync(path7.dirname(target), { recursive: true });
1534
+ fs7.writeFileSync(target, sourceCode);
1535
+ recordWritten(projectDir, target, sourceCode);
1536
+ console.log(` \u2713 ${isNew ? "Created" : "Updated"} ${target}`);
1537
+ console.log(` ${(sourceCode.length / 1024).toFixed(1)} KB written`);
1538
+ return true;
1539
+ }
1540
+ async function pullByName(projectDir, name, force) {
1541
+ pullTargetPath(projectDir, name);
1462
1542
  console.log("");
1463
1543
  console.log(` Pulling latest source for ${name}...`);
1464
1544
  try {
1465
- const sourceCode = await pullSource(name);
1466
- fs6.mkdirSync(path6.dirname(target), { recursive: true });
1467
- fs6.writeFileSync(target, sourceCode);
1468
- console.log(` \u2713 ${isNew ? "Created" : "Updated"} ${target}`);
1469
- console.log(` ${(sourceCode.length / 1024).toFixed(1)} KB written`);
1470
- if (destination.warning) console.log(` ${destination.warning}`);
1545
+ const pulled = await pullBot(name);
1546
+ const destination = pullDestination(projectDir, pulled.name, readProjectConfig(projectDir));
1547
+ const wrote = writeGuarded(projectDir, destination.path, pulled.sourceCode, force);
1548
+ if (wrote && destination.warning) console.log(` ${destination.warning}`);
1471
1549
  } catch (err) {
1472
1550
  reportPullFailure(err, name);
1473
1551
  }
@@ -1487,19 +1565,15 @@ function reportPullFailure(err, name) {
1487
1565
  async function runPull(options) {
1488
1566
  const projectDir = process.cwd();
1489
1567
  if (options.name) {
1490
- await pullByName(projectDir, options.name);
1568
+ await pullByName(projectDir, options.name, options.force === true);
1491
1569
  return;
1492
1570
  }
1493
1571
  const botInfo = await discoverBot(projectDir, options.bot, { allowMissingFile: true });
1494
- const isNew = !fs6.existsSync(botInfo.sourcePath);
1495
1572
  console.log(`
1496
1573
  Pulling latest source for ${botInfo.exportName}...`);
1497
1574
  try {
1498
1575
  const sourceCode = await pullSource(botInfo.exportName);
1499
- fs6.mkdirSync(path6.dirname(botInfo.sourcePath), { recursive: true });
1500
- fs6.writeFileSync(botInfo.sourcePath, sourceCode);
1501
- console.log(` \u2713 ${isNew ? "Created" : "Updated"} ${botInfo.sourcePath}`);
1502
- console.log(` ${(sourceCode.length / 1024).toFixed(1)} KB written`);
1576
+ writeGuarded(projectDir, botInfo.sourcePath, sourceCode, options.force === true);
1503
1577
  } catch (err) {
1504
1578
  reportPullFailure(err);
1505
1579
  }
@@ -1722,8 +1796,8 @@ function runMissileCalc(options) {
1722
1796
 
1723
1797
  // src/auth/telemetry.ts
1724
1798
  import os2 from "os";
1725
- import fs7 from "fs";
1726
- import path7 from "path";
1799
+ import fs8 from "fs";
1800
+ import path8 from "path";
1727
1801
  var TIMEOUT_MS = 1e3;
1728
1802
  function isTelemetryEnabled(env) {
1729
1803
  const off = (value) => {
@@ -1743,13 +1817,13 @@ function isTelemetryEnabled(env) {
1743
1817
  }
1744
1818
  var TELEMETRY_NOTICE = " Vibemancer records which commands are run, your OS and version numbers, to decide\n which platforms to support. No code, file paths or personal files are ever sent.\n Opt out any time with VIBEMANCER_TELEMETRY=0 (DO_NOT_TRACK is honoured too).\n";
1745
1819
  function noticePath() {
1746
- return path7.join(os2.homedir(), ".vibemancer", "telemetry-notice-shown");
1820
+ return path8.join(os2.homedir(), ".vibemancer", "telemetry-notice-shown");
1747
1821
  }
1748
1822
  function showNoticeOnce(marker = noticePath()) {
1749
1823
  try {
1750
- if (fs7.existsSync(marker)) return false;
1751
- fs7.mkdirSync(path7.dirname(marker), { recursive: true });
1752
- fs7.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
1824
+ if (fs8.existsSync(marker)) return false;
1825
+ fs8.mkdirSync(path8.dirname(marker), { recursive: true });
1826
+ fs8.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
1753
1827
  console.log(TELEMETRY_NOTICE);
1754
1828
  return true;
1755
1829
  } catch {
@@ -1812,7 +1886,8 @@ Commands:
1812
1886
  logout Sign out
1813
1887
  upload Upload bot to VibeMancer for online competition
1814
1888
  pull Download latest source code from VibeMancer
1815
- (--name <bot> restores it with no local file needed)
1889
+ (--name <bot> restores it with no local file needed;
1890
+ --force overwrites local changes it did not write)
1816
1891
  feedback Submit a bug report or suggestion
1817
1892
  missile-calc Calculate missile cast time and DPS for a config
1818
1893
 
@@ -1920,10 +1995,10 @@ var KNOWN_FLAGS = {
1920
1995
  // pull was absent here for the same reason upload was, with the same consequence: a typo'd
1921
1996
  // flag was accepted and dropped, so `pull --nmae Wiz2` fell through to file discovery and
1922
1997
  // reported "could not find bot source file" about a flag the user had spelled wrong.
1923
- pull: ["--bot", "--name"],
1998
+ pull: ["--bot", "--name", "--force"],
1924
1999
  "missile-calc": ["--damage", "--speed", "--duration", "--turnRate"]
1925
2000
  };
1926
- var BOOLEAN_FLAGS = ["--mine", "--inactive", "--active", "--no-browser"];
2001
+ var BOOLEAN_FLAGS = ["--mine", "--inactive", "--active", "--no-browser", "--force"];
1927
2002
  function flagsMissingValues(command2, argv) {
1928
2003
  const known = KNOWN_FLAGS[command2];
1929
2004
  if (!known) return [];
@@ -2110,7 +2185,7 @@ ${list}
2110
2185
  case "pull": {
2111
2186
  const bot = parseFlag("--bot");
2112
2187
  const name = parseFlag("--name");
2113
- await runPull({ bot, name });
2188
+ await runPull({ bot, name, force: args.includes("--force") });
2114
2189
  break;
2115
2190
  }
2116
2191
  case "login": {