vibemancer 1.0.35 → 1.0.36

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").replace(/\s+$/, "");
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,23 +1500,47 @@ 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
+ async function pullByName(projectDir, name, force) {
1511
+ pullTargetPath(projectDir, name);
1462
1512
  console.log("");
1463
1513
  console.log(` Pulling latest source for ${name}...`);
1464
1514
  try {
1465
- const sourceCode = await pullSource(name);
1466
- fs6.mkdirSync(path6.dirname(target), { recursive: true });
1467
- fs6.writeFileSync(target, sourceCode);
1515
+ const pulled = await pullBot(name);
1516
+ const sourceCode = pulled.sourceCode;
1517
+ const destination = pullDestination(projectDir, pulled.name, readProjectConfig(projectDir));
1518
+ const target = destination.path;
1519
+ const isNew = !fs7.existsSync(target);
1520
+ const decision = pullDecision({
1521
+ fileExists: !isNew,
1522
+ localContent: isNew ? void 0 : fs7.readFileSync(target, "utf-8"),
1523
+ incomingContent: sourceCode,
1524
+ recordedHash: recordedHashFor(projectDir, target),
1525
+ force
1526
+ });
1527
+ if (decision === "skip-identical") {
1528
+ console.log(` Already up to date: ${target}`);
1529
+ recordWritten(projectDir, target, sourceCode);
1530
+ return;
1531
+ }
1532
+ if (decision !== "write") {
1533
+ 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";
1534
+ console.error("");
1535
+ console.error(` Refusing to overwrite ${target}: ${why}.`);
1536
+ console.error(" Save or commit it first, or re-run with --force to replace it.");
1537
+ console.error("");
1538
+ process.exitCode = 1;
1539
+ return;
1540
+ }
1541
+ fs7.mkdirSync(path7.dirname(target), { recursive: true });
1542
+ fs7.writeFileSync(target, sourceCode);
1543
+ recordWritten(projectDir, target, sourceCode);
1468
1544
  console.log(` \u2713 ${isNew ? "Created" : "Updated"} ${target}`);
1469
1545
  console.log(` ${(sourceCode.length / 1024).toFixed(1)} KB written`);
1470
1546
  if (destination.warning) console.log(` ${destination.warning}`);
@@ -1487,17 +1563,17 @@ function reportPullFailure(err, name) {
1487
1563
  async function runPull(options) {
1488
1564
  const projectDir = process.cwd();
1489
1565
  if (options.name) {
1490
- await pullByName(projectDir, options.name);
1566
+ await pullByName(projectDir, options.name, options.force === true);
1491
1567
  return;
1492
1568
  }
1493
1569
  const botInfo = await discoverBot(projectDir, options.bot, { allowMissingFile: true });
1494
- const isNew = !fs6.existsSync(botInfo.sourcePath);
1570
+ const isNew = !fs7.existsSync(botInfo.sourcePath);
1495
1571
  console.log(`
1496
1572
  Pulling latest source for ${botInfo.exportName}...`);
1497
1573
  try {
1498
1574
  const sourceCode = await pullSource(botInfo.exportName);
1499
- fs6.mkdirSync(path6.dirname(botInfo.sourcePath), { recursive: true });
1500
- fs6.writeFileSync(botInfo.sourcePath, sourceCode);
1575
+ fs7.mkdirSync(path7.dirname(botInfo.sourcePath), { recursive: true });
1576
+ fs7.writeFileSync(botInfo.sourcePath, sourceCode);
1501
1577
  console.log(` \u2713 ${isNew ? "Created" : "Updated"} ${botInfo.sourcePath}`);
1502
1578
  console.log(` ${(sourceCode.length / 1024).toFixed(1)} KB written`);
1503
1579
  } catch (err) {
@@ -1722,8 +1798,8 @@ function runMissileCalc(options) {
1722
1798
 
1723
1799
  // src/auth/telemetry.ts
1724
1800
  import os2 from "os";
1725
- import fs7 from "fs";
1726
- import path7 from "path";
1801
+ import fs8 from "fs";
1802
+ import path8 from "path";
1727
1803
  var TIMEOUT_MS = 1e3;
1728
1804
  function isTelemetryEnabled(env) {
1729
1805
  const off = (value) => {
@@ -1743,13 +1819,13 @@ function isTelemetryEnabled(env) {
1743
1819
  }
1744
1820
  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
1821
  function noticePath() {
1746
- return path7.join(os2.homedir(), ".vibemancer", "telemetry-notice-shown");
1822
+ return path8.join(os2.homedir(), ".vibemancer", "telemetry-notice-shown");
1747
1823
  }
1748
1824
  function showNoticeOnce(marker = noticePath()) {
1749
1825
  try {
1750
- if (fs7.existsSync(marker)) return false;
1751
- fs7.mkdirSync(path7.dirname(marker), { recursive: true });
1752
- fs7.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
1826
+ if (fs8.existsSync(marker)) return false;
1827
+ fs8.mkdirSync(path8.dirname(marker), { recursive: true });
1828
+ fs8.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
1753
1829
  console.log(TELEMETRY_NOTICE);
1754
1830
  return true;
1755
1831
  } catch {
@@ -1812,7 +1888,8 @@ Commands:
1812
1888
  logout Sign out
1813
1889
  upload Upload bot to VibeMancer for online competition
1814
1890
  pull Download latest source code from VibeMancer
1815
- (--name <bot> restores it with no local file needed)
1891
+ (--name <bot> restores it with no local file needed;
1892
+ --force overwrites local changes it did not write)
1816
1893
  feedback Submit a bug report or suggestion
1817
1894
  missile-calc Calculate missile cast time and DPS for a config
1818
1895
 
@@ -1920,10 +1997,10 @@ var KNOWN_FLAGS = {
1920
1997
  // pull was absent here for the same reason upload was, with the same consequence: a typo'd
1921
1998
  // flag was accepted and dropped, so `pull --nmae Wiz2` fell through to file discovery and
1922
1999
  // reported "could not find bot source file" about a flag the user had spelled wrong.
1923
- pull: ["--bot", "--name"],
2000
+ pull: ["--bot", "--name", "--force"],
1924
2001
  "missile-calc": ["--damage", "--speed", "--duration", "--turnRate"]
1925
2002
  };
1926
- var BOOLEAN_FLAGS = ["--mine", "--inactive", "--active", "--no-browser"];
2003
+ var BOOLEAN_FLAGS = ["--mine", "--inactive", "--active", "--no-browser", "--force"];
1927
2004
  function flagsMissingValues(command2, argv) {
1928
2005
  const known = KNOWN_FLAGS[command2];
1929
2006
  if (!known) return [];
@@ -2110,7 +2187,7 @@ ${list}
2110
2187
  case "pull": {
2111
2188
  const bot = parseFlag("--bot");
2112
2189
  const name = parseFlag("--name");
2113
- await runPull({ bot, name });
2190
+ await runPull({ bot, name, force: args.includes("--force") });
2114
2191
  break;
2115
2192
  }
2116
2193
  case "login": {