vibemancer 1.0.36 → 1.0.38
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/README.md +31 -1
- package/dist/{chunk-MCU2B4PZ.js → chunk-UKMSXLZR.js} +9 -4
- package/dist/chunk-UKMSXLZR.js.map +1 -0
- package/dist/cli.js +330 -114
- package/dist/cli.js.map +1 -1
- package/dist/{oauth-client-AX3HNK6P.js → oauth-client-WUOWASU4.js} +4 -2
- package/package.json +2 -2
- package/dist/chunk-MCU2B4PZ.js.map +0 -1
- /package/dist/{oauth-client-AX3HNK6P.js.map → oauth-client-WUOWASU4.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
resolveOpponent
|
|
7
7
|
} from "./chunk-CMTEJ5MU.js";
|
|
8
8
|
import {
|
|
9
|
+
AUTH_TIMEOUT_MS,
|
|
9
10
|
MCP_BASE_URL,
|
|
10
11
|
NotLoggedInError,
|
|
11
12
|
clearCredentials,
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
getAccessToken,
|
|
15
16
|
loadCredentials,
|
|
16
17
|
registerClient
|
|
17
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-UKMSXLZR.js";
|
|
18
19
|
|
|
19
20
|
// src/cli-version.ts
|
|
20
21
|
import fs from "fs";
|
|
@@ -42,6 +43,18 @@ function cliVersion() {
|
|
|
42
43
|
return "unknown";
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
// src/flag-numbers.ts
|
|
47
|
+
var DECIMAL = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
48
|
+
function parseNumberFlag(flag, raw, kind) {
|
|
49
|
+
const expected = kind === "integer" ? "a whole number" : "a number";
|
|
50
|
+
const refuse = () => ({ ok: false, message: `${flag} must be ${expected}, got "${raw}".` });
|
|
51
|
+
if (!DECIMAL.test(raw)) return refuse();
|
|
52
|
+
const value = Number(raw);
|
|
53
|
+
if (!Number.isFinite(value)) return refuse();
|
|
54
|
+
if (kind === "integer" && !Number.isInteger(value)) return refuse();
|
|
55
|
+
return { ok: true, value };
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
// src/commands/dev.ts
|
|
46
59
|
import { execFile } from "child_process";
|
|
47
60
|
|
|
@@ -328,9 +341,28 @@ function respond(res, status, data) {
|
|
|
328
341
|
res.end(JSON.stringify(data));
|
|
329
342
|
}
|
|
330
343
|
|
|
344
|
+
// src/project-root.ts
|
|
345
|
+
import fs3 from "fs";
|
|
346
|
+
import path3 from "path";
|
|
347
|
+
var PROJECT_MARKER = "vibemancer.json";
|
|
348
|
+
function findProjectRoot(startDir) {
|
|
349
|
+
let current = path3.resolve(startDir);
|
|
350
|
+
for (; ; ) {
|
|
351
|
+
const marker = path3.join(current, PROJECT_MARKER);
|
|
352
|
+
try {
|
|
353
|
+
if (fs3.statSync(marker).isFile()) return current;
|
|
354
|
+
} catch {
|
|
355
|
+
}
|
|
356
|
+
const parent = path3.dirname(current);
|
|
357
|
+
if (parent === current) break;
|
|
358
|
+
current = parent;
|
|
359
|
+
}
|
|
360
|
+
return path3.resolve(startDir);
|
|
361
|
+
}
|
|
362
|
+
|
|
331
363
|
// src/commands/dev.ts
|
|
332
364
|
async function runDev(options) {
|
|
333
|
-
const projectDir = process.cwd();
|
|
365
|
+
const projectDir = findProjectRoot(process.cwd());
|
|
334
366
|
const bots = await discoverAllBots(projectDir);
|
|
335
367
|
if (bots.length === 0) {
|
|
336
368
|
console.log("No bots discovered in src/. Add a .ts file with a PascalCase export, e.g.:");
|
|
@@ -373,7 +405,7 @@ async function runTest(_options) {
|
|
|
373
405
|
const code = await new Promise((resolve) => {
|
|
374
406
|
const child = spawn("npx vitest run", {
|
|
375
407
|
stdio: "inherit",
|
|
376
|
-
cwd: process.cwd(),
|
|
408
|
+
cwd: findProjectRoot(process.cwd()),
|
|
377
409
|
shell: true
|
|
378
410
|
});
|
|
379
411
|
child.on("error", () => resolve(1));
|
|
@@ -385,8 +417,8 @@ async function runTest(_options) {
|
|
|
385
417
|
}
|
|
386
418
|
|
|
387
419
|
// src/commands/fight.ts
|
|
388
|
-
import
|
|
389
|
-
import
|
|
420
|
+
import fs4 from "fs";
|
|
421
|
+
import path4 from "path";
|
|
390
422
|
import { BotBundle, sandboxFight, sandboxSimulate, scoreFight, runBundleFight } from "@vibemancer/core";
|
|
391
423
|
|
|
392
424
|
// src/commands/fight-errors.ts
|
|
@@ -551,7 +583,7 @@ async function runFight(options) {
|
|
|
551
583
|
return runFullFight(options);
|
|
552
584
|
}
|
|
553
585
|
async function runSingleFight(options) {
|
|
554
|
-
const projectDir = process.cwd();
|
|
586
|
+
const projectDir = findProjectRoot(process.cwd());
|
|
555
587
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
556
588
|
console.log(`
|
|
557
589
|
Bot: ${botInfo.exportName}`);
|
|
@@ -594,7 +626,7 @@ async function runSingleFight(options) {
|
|
|
594
626
|
}
|
|
595
627
|
}
|
|
596
628
|
async function runFullFight(options) {
|
|
597
|
-
const projectDir = process.cwd();
|
|
629
|
+
const projectDir = findProjectRoot(process.cwd());
|
|
598
630
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
599
631
|
const userBundle = new BotBundle(botInfo.sourcePath, botInfo.exportName);
|
|
600
632
|
const opponents = getBuiltinBotNames();
|
|
@@ -662,13 +694,13 @@ async function runFullFight(options) {
|
|
|
662
694
|
console.log("");
|
|
663
695
|
}
|
|
664
696
|
function getHistoryPath(projectDir) {
|
|
665
|
-
return
|
|
697
|
+
return path4.join(projectDir, ".vibemancer", "history.json");
|
|
666
698
|
}
|
|
667
699
|
function loadHistory(projectDir) {
|
|
668
700
|
const historyPath = getHistoryPath(projectDir);
|
|
669
|
-
if (!
|
|
701
|
+
if (!fs4.existsSync(historyPath)) return [];
|
|
670
702
|
try {
|
|
671
|
-
const raw =
|
|
703
|
+
const raw = fs4.readFileSync(historyPath, "utf-8");
|
|
672
704
|
return JSON.parse(raw);
|
|
673
705
|
} catch {
|
|
674
706
|
return [];
|
|
@@ -676,10 +708,10 @@ function loadHistory(projectDir) {
|
|
|
676
708
|
}
|
|
677
709
|
function saveHistory(projectDir, history, current) {
|
|
678
710
|
const historyPath = getHistoryPath(projectDir);
|
|
679
|
-
const dir =
|
|
680
|
-
|
|
711
|
+
const dir = path4.dirname(historyPath);
|
|
712
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
681
713
|
const updated = [...history.slice(-19), current];
|
|
682
|
-
|
|
714
|
+
fs4.writeFileSync(historyPath, JSON.stringify(updated, null, " ") + "\n");
|
|
683
715
|
}
|
|
684
716
|
function showDiff(current, previous) {
|
|
685
717
|
const prevMap = new Map(previous.map((e) => [e.opponent, e]));
|
|
@@ -738,7 +770,7 @@ function describeTickCap(winner, ticks, maxTicks) {
|
|
|
738
770
|
return `NOTE: this trace stopped at its ${maxTicks}-tick cap, not because the match was a draw. A real match runs ${FULL_MATCH_TICKS.toLocaleString("en-US")} ticks, so you are seeing the first ${Math.round(maxTicks / FULL_MATCH_TICKS * 100)}% of the fight and the outcome above is NOT the result. Re-run with --ticks N to see further.`;
|
|
739
771
|
}
|
|
740
772
|
async function runTrace(options) {
|
|
741
|
-
const projectDir = process.cwd();
|
|
773
|
+
const projectDir = findProjectRoot(process.cwd());
|
|
742
774
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
743
775
|
const distance = options.distance ?? 600;
|
|
744
776
|
let result;
|
|
@@ -804,7 +836,7 @@ function summarisePairing(bot1Name, bot2Name, result) {
|
|
|
804
836
|
};
|
|
805
837
|
}
|
|
806
838
|
async function runTournament(options) {
|
|
807
|
-
const projectDir = process.cwd();
|
|
839
|
+
const projectDir = findProjectRoot(process.cwd());
|
|
808
840
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
809
841
|
const userBundle = new BotBundle3(botInfo.sourcePath, botInfo.exportName);
|
|
810
842
|
const opponentNames = options.opponents && options.opponents.length > 0 ? options.opponents : getBuiltinBotNames();
|
|
@@ -975,7 +1007,7 @@ function resolveOpponentNames(opponents) {
|
|
|
975
1007
|
return opponents;
|
|
976
1008
|
}
|
|
977
1009
|
async function runOptimize(options) {
|
|
978
|
-
const projectDir = process.cwd();
|
|
1010
|
+
const projectDir = findProjectRoot(process.cwd());
|
|
979
1011
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
980
1012
|
const source = readFileSync(botInfo.sourcePath, "utf-8");
|
|
981
1013
|
const params = parseParams(source);
|
|
@@ -1060,11 +1092,11 @@ function escapeRegex(s) {
|
|
|
1060
1092
|
}
|
|
1061
1093
|
|
|
1062
1094
|
// src/commands/build.ts
|
|
1063
|
-
import
|
|
1064
|
-
import
|
|
1095
|
+
import fs5 from "fs";
|
|
1096
|
+
import path5 from "path";
|
|
1065
1097
|
import { BotBundle as BotBundle5, compileMatchBundle } from "@vibemancer/core";
|
|
1066
1098
|
async function runBuild(options) {
|
|
1067
|
-
const projectDir = process.cwd();
|
|
1099
|
+
const projectDir = findProjectRoot(process.cwd());
|
|
1068
1100
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
1069
1101
|
const coreSourceDir = findCoreSourceDir();
|
|
1070
1102
|
const userBundle = new BotBundle5(botInfo.sourcePath, botInfo.exportName);
|
|
@@ -1079,9 +1111,9 @@ async function runBuild(options) {
|
|
|
1079
1111
|
});
|
|
1080
1112
|
const elapsed = Date.now() - start;
|
|
1081
1113
|
const outFile = options.output ?? `dist/${botInfo.exportName}-vs-${options.opponent}.js`;
|
|
1082
|
-
const outDir =
|
|
1083
|
-
|
|
1084
|
-
|
|
1114
|
+
const outDir = path5.dirname(path5.resolve(outFile));
|
|
1115
|
+
fs5.mkdirSync(outDir, { recursive: true });
|
|
1116
|
+
fs5.writeFileSync(path5.resolve(outFile), bundle);
|
|
1085
1117
|
const sizeKb = (bundle.length / 1024).toFixed(1);
|
|
1086
1118
|
console.log(` Output: ${outFile} (${sizeKb} KB)`);
|
|
1087
1119
|
console.log(` Compiled in ${elapsed}ms
|
|
@@ -1091,15 +1123,44 @@ async function runBuild(options) {
|
|
|
1091
1123
|
// src/commands/bots.ts
|
|
1092
1124
|
import { BOT_GROUPS, ALL_BOTS } from "@vibemancer/core";
|
|
1093
1125
|
|
|
1126
|
+
// src/auth/gateway-failure.ts
|
|
1127
|
+
var MAX_BODY = 500;
|
|
1128
|
+
function trimBody(body) {
|
|
1129
|
+
const text = body.trim();
|
|
1130
|
+
return text.length > MAX_BODY ? `${text.slice(0, MAX_BODY)}\u2026` : text;
|
|
1131
|
+
}
|
|
1132
|
+
function retryAfterSeconds(retryAfter) {
|
|
1133
|
+
if (retryAfter === null) return null;
|
|
1134
|
+
const seconds = Number(retryAfter.trim());
|
|
1135
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return null;
|
|
1136
|
+
return Math.ceil(seconds);
|
|
1137
|
+
}
|
|
1138
|
+
function describeGatewayFailure(operation, status, retryAfter, body) {
|
|
1139
|
+
if (status === 429) {
|
|
1140
|
+
const seconds = retryAfterSeconds(retryAfter);
|
|
1141
|
+
if (seconds === null) return "Rate limited. Wait a moment and try again.";
|
|
1142
|
+
return `Rate limited. Try again in ${seconds} ${seconds === 1 ? "second" : "seconds"}.`;
|
|
1143
|
+
}
|
|
1144
|
+
if (status === 401 || status === 403) {
|
|
1145
|
+
return `${operation} was refused: your session is missing or expired. Run: vibemancer login`;
|
|
1146
|
+
}
|
|
1147
|
+
if (status >= 500 && status !== 501 && status !== 505) {
|
|
1148
|
+
const detail = trimBody(body);
|
|
1149
|
+
return `${operation} failed: the server had a problem (${status}). Try again in a moment.` + (detail ? `
|
|
1150
|
+
Server said: ${detail}` : "");
|
|
1151
|
+
}
|
|
1152
|
+
return `${operation} failed (${status}): ${trimBody(body)}`;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1094
1155
|
// src/auth/env-headers.ts
|
|
1095
1156
|
import os from "os";
|
|
1096
1157
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1097
|
-
import
|
|
1158
|
+
import path6 from "path";
|
|
1098
1159
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1099
1160
|
function findCliVersionFrom(here) {
|
|
1100
1161
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
1101
1162
|
try {
|
|
1102
|
-
const parsed = JSON.parse(readFileSync2(
|
|
1163
|
+
const parsed = JSON.parse(readFileSync2(path6.resolve(here, rel), "utf8"));
|
|
1103
1164
|
if (typeof parsed !== "object" || parsed === null) continue;
|
|
1104
1165
|
if (!("name" in parsed) || !("version" in parsed)) continue;
|
|
1105
1166
|
const { name, version } = parsed;
|
|
@@ -1111,7 +1172,7 @@ function findCliVersionFrom(here) {
|
|
|
1111
1172
|
}
|
|
1112
1173
|
function readCliVersion() {
|
|
1113
1174
|
try {
|
|
1114
|
-
return findCliVersionFrom(
|
|
1175
|
+
return findCliVersionFrom(path6.dirname(fileURLToPath2(import.meta.url)));
|
|
1115
1176
|
} catch {
|
|
1116
1177
|
return "";
|
|
1117
1178
|
}
|
|
@@ -1155,7 +1216,7 @@ async function uploadBot(payload) {
|
|
|
1155
1216
|
body: JSON.stringify(payload),
|
|
1156
1217
|
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
|
|
1157
1218
|
});
|
|
1158
|
-
if (!res.ok) throw new Error(
|
|
1219
|
+
if (!res.ok) throw new Error(describeGatewayFailure("Upload", res.status, res.headers.get("retry-after"), await res.text()));
|
|
1159
1220
|
const data = await res.json();
|
|
1160
1221
|
const wizardId = typeof data === "object" && data !== null && "wizardId" in data && typeof data.wizardId === "string" ? data.wizardId : "";
|
|
1161
1222
|
const message = typeof data === "object" && data !== null && "message" in data && typeof data.message === "string" ? data.message : "Uploaded.";
|
|
@@ -1186,7 +1247,7 @@ async function listMyBotsPage() {
|
|
|
1186
1247
|
headers: { Authorization: `Bearer ${token}`, ...envHeaders() },
|
|
1187
1248
|
signal: AbortSignal.timeout(READ_TIMEOUT_MS)
|
|
1188
1249
|
});
|
|
1189
|
-
if (!res.ok) throw new Error(
|
|
1250
|
+
if (!res.ok) throw new Error(describeGatewayFailure("Listing your bots", res.status, res.headers.get("retry-after"), await res.text()));
|
|
1190
1251
|
const data = await res.json();
|
|
1191
1252
|
const bots = typeof data === "object" && data !== null && "bots" in data ? data.bots : null;
|
|
1192
1253
|
if (!Array.isArray(bots)) throw new Error("The server returned an unexpected response while listing your bots. This is not the same as having none - try again, and report it if it persists.");
|
|
@@ -1205,7 +1266,7 @@ async function pullBot(name) {
|
|
|
1205
1266
|
headers: { Authorization: `Bearer ${token}`, ...envHeaders() },
|
|
1206
1267
|
signal: AbortSignal.timeout(READ_TIMEOUT_MS)
|
|
1207
1268
|
});
|
|
1208
|
-
if (!res.ok) throw new Error(
|
|
1269
|
+
if (!res.ok) throw new Error(describeGatewayFailure("Pull", res.status, res.headers.get("retry-after"), await res.text()));
|
|
1209
1270
|
const data = await res.json();
|
|
1210
1271
|
const source = typeof data === "object" && data !== null && "sourceCode" in data && typeof data.sourceCode === "string" ? data.sourceCode : "";
|
|
1211
1272
|
if (source) {
|
|
@@ -1376,55 +1437,101 @@ function getStyleDescription(bot) {
|
|
|
1376
1437
|
}
|
|
1377
1438
|
|
|
1378
1439
|
// src/commands/upload.ts
|
|
1379
|
-
import
|
|
1440
|
+
import fs7 from "fs";
|
|
1380
1441
|
|
|
1381
1442
|
// src/pull-safety.ts
|
|
1382
1443
|
import { createHash } from "crypto";
|
|
1383
|
-
import
|
|
1384
|
-
import
|
|
1444
|
+
import fs6 from "fs";
|
|
1445
|
+
import path7 from "path";
|
|
1385
1446
|
function hashContent(content) {
|
|
1386
|
-
const
|
|
1447
|
+
const BYTE_ORDER_MARK = String.fromCharCode(65279);
|
|
1448
|
+
const withoutBom = content.startsWith(BYTE_ORDER_MARK) ? content.slice(1) : content;
|
|
1449
|
+
const normalised = withoutBom.replace(/\r\n/g, "\n").trimEnd();
|
|
1387
1450
|
return createHash("sha256").update(normalised, "utf8").digest("hex");
|
|
1388
1451
|
}
|
|
1389
1452
|
function pullDecision(situation) {
|
|
1390
1453
|
if (!situation.fileExists) return "write";
|
|
1391
|
-
|
|
1392
|
-
const local = situation.localContent ?? "";
|
|
1454
|
+
const local = situation.localContent;
|
|
1393
1455
|
if (hashContent(local) === hashContent(situation.incomingContent)) return "skip-identical";
|
|
1456
|
+
if (situation.force) return "write";
|
|
1394
1457
|
if (situation.recordedHash === void 0) return "refuse-unknown";
|
|
1395
1458
|
return hashContent(local) === situation.recordedHash ? "write" : "refuse-modified";
|
|
1396
1459
|
}
|
|
1397
1460
|
var STATE_FILE = ".vibemancer-state.json";
|
|
1398
1461
|
function statePath(projectDir) {
|
|
1399
|
-
return
|
|
1462
|
+
return path7.join(projectDir, STATE_FILE);
|
|
1400
1463
|
}
|
|
1401
|
-
function
|
|
1464
|
+
function readRaw(projectDir) {
|
|
1402
1465
|
try {
|
|
1403
|
-
const parsed = JSON.parse(
|
|
1404
|
-
if (typeof parsed !== "object" || parsed === null) return {};
|
|
1466
|
+
const parsed = JSON.parse(fs6.readFileSync(statePath(projectDir), "utf-8"));
|
|
1467
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
1405
1468
|
const out = {};
|
|
1406
|
-
for (const [
|
|
1469
|
+
for (const [key, value] of Object.entries(parsed)) out[key] = value;
|
|
1407
1470
|
return out;
|
|
1408
1471
|
} catch {
|
|
1409
1472
|
return {};
|
|
1410
1473
|
}
|
|
1411
1474
|
}
|
|
1475
|
+
var NOTE_PREFIX = "_";
|
|
1476
|
+
var README_NOTE = "Written by the vibemancer CLI: hashes of what it last wrote here, so `vibemancer pull` can tell its own output from your edits. Commit it. Safe to delete - the cost is one --force, never data.";
|
|
1477
|
+
function readState(projectDir) {
|
|
1478
|
+
const out = {};
|
|
1479
|
+
for (const [k, v] of Object.entries(readRaw(projectDir))) {
|
|
1480
|
+
if (!k.startsWith(NOTE_PREFIX) && typeof v === "string") out[k] = v;
|
|
1481
|
+
}
|
|
1482
|
+
return out;
|
|
1483
|
+
}
|
|
1484
|
+
function stateKey(projectDir, filePath) {
|
|
1485
|
+
const canonical = (p) => {
|
|
1486
|
+
try {
|
|
1487
|
+
return fs6.realpathSync.native(p);
|
|
1488
|
+
} catch {
|
|
1489
|
+
return path7.resolve(p);
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
return path7.relative(canonical(projectDir), canonical(filePath)).split(path7.sep).join("/");
|
|
1493
|
+
}
|
|
1412
1494
|
function recordedHashFor(projectDir, filePath) {
|
|
1413
|
-
|
|
1495
|
+
const state = readState(projectDir);
|
|
1496
|
+
const key = stateKey(projectDir, filePath);
|
|
1497
|
+
if (key in state) return state[key];
|
|
1498
|
+
return legacyRecordFor(key, state, (candidateKey) => {
|
|
1499
|
+
const asFile = path7.join(projectDir, candidateKey);
|
|
1500
|
+
return fs6.existsSync(asFile) && stateKey(projectDir, asFile) === candidateKey;
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
function legacyRecordFor(key, state, isDistinctFile) {
|
|
1504
|
+
const folded = key.toLowerCase();
|
|
1505
|
+
for (const [k, v] of Object.entries(state)) {
|
|
1506
|
+
if (k === key || k.toLowerCase() !== folded) continue;
|
|
1507
|
+
if (isDistinctFile(k)) continue;
|
|
1508
|
+
return v;
|
|
1509
|
+
}
|
|
1510
|
+
return void 0;
|
|
1414
1511
|
}
|
|
1415
1512
|
function recordWritten(projectDir, filePath, content) {
|
|
1513
|
+
const tmp = `${statePath(projectDir)}.${process.pid}.tmp`;
|
|
1416
1514
|
try {
|
|
1417
|
-
const state =
|
|
1418
|
-
state
|
|
1419
|
-
|
|
1515
|
+
const state = readRaw(projectDir);
|
|
1516
|
+
state._readme = README_NOTE;
|
|
1517
|
+
const key = stateKey(projectDir, filePath);
|
|
1518
|
+
const folded = key.toLowerCase();
|
|
1519
|
+
for (const k of Object.keys(state)) if (k !== key && k.toLowerCase() === folded) delete state[k];
|
|
1520
|
+
state[key] = hashContent(content);
|
|
1521
|
+
fs6.writeFileSync(tmp, JSON.stringify(state, null, 2) + String.fromCharCode(10));
|
|
1522
|
+
fs6.renameSync(tmp, statePath(projectDir));
|
|
1420
1523
|
} catch {
|
|
1524
|
+
try {
|
|
1525
|
+
fs6.unlinkSync(tmp);
|
|
1526
|
+
} catch {
|
|
1527
|
+
}
|
|
1421
1528
|
}
|
|
1422
1529
|
}
|
|
1423
1530
|
|
|
1424
1531
|
// src/commands/upload.ts
|
|
1425
1532
|
var MAX_BUNDLE_BYTES = 500 * 1024;
|
|
1426
1533
|
async function runUpload(options) {
|
|
1427
|
-
const projectDir = process.cwd();
|
|
1534
|
+
const projectDir = findProjectRoot(process.cwd());
|
|
1428
1535
|
const botInfo = await discoverBot(projectDir, options.bot);
|
|
1429
1536
|
console.log(`
|
|
1430
1537
|
Compiling ${botInfo.exportName}...`);
|
|
@@ -1437,7 +1544,7 @@ async function runUpload(options) {
|
|
|
1437
1544
|
}
|
|
1438
1545
|
let sourceCode = "";
|
|
1439
1546
|
try {
|
|
1440
|
-
sourceCode =
|
|
1547
|
+
sourceCode = fs7.readFileSync(botInfo.sourcePath, "utf-8");
|
|
1441
1548
|
} catch {
|
|
1442
1549
|
}
|
|
1443
1550
|
console.log(` Wizard: ${botInfo.exportName}`);
|
|
@@ -1450,7 +1557,7 @@ async function runUpload(options) {
|
|
|
1450
1557
|
sourceCode,
|
|
1451
1558
|
active: options.active
|
|
1452
1559
|
});
|
|
1453
|
-
if (sourceCode) recordWritten(process.cwd(), botInfo.sourcePath, sourceCode);
|
|
1560
|
+
if (sourceCode) recordWritten(findProjectRoot(process.cwd()), botInfo.sourcePath, sourceCode);
|
|
1454
1561
|
console.log(` \u2713 ${result.message}`);
|
|
1455
1562
|
if (result.wizardId) console.log(` Wizard ID: ${result.wizardId}`);
|
|
1456
1563
|
console.log(result.active === false ? ` "${botInfo.exportName}" is retired and no longer competing.
|
|
@@ -1468,20 +1575,44 @@ async function runUpload(options) {
|
|
|
1468
1575
|
}
|
|
1469
1576
|
|
|
1470
1577
|
// src/commands/pull.ts
|
|
1471
|
-
import
|
|
1472
|
-
import
|
|
1578
|
+
import fs8 from "fs";
|
|
1579
|
+
import path8 from "path";
|
|
1580
|
+
var LocalProjectError = class extends Error {
|
|
1581
|
+
constructor(message) {
|
|
1582
|
+
super(message);
|
|
1583
|
+
this.name = "LocalProjectError";
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
function adviceFor(err, name) {
|
|
1587
|
+
if (err instanceof LocalProjectError) return void 0;
|
|
1588
|
+
return name ? " See your bots with: vibemancer bots --mine" : " Upload first with: vibemancer upload";
|
|
1589
|
+
}
|
|
1473
1590
|
function pullTargetPath(projectDir, name) {
|
|
1474
1591
|
const trimmed = name.trim();
|
|
1475
1592
|
if (!trimmed) throw new Error("A bot name is required.");
|
|
1476
1593
|
if (/[\u0000-\u001f\u007f]/.test(trimmed)) throw new Error(`Refusing a bot name with a control character: ${JSON.stringify(name)}`);
|
|
1477
|
-
if (trimmed !==
|
|
1594
|
+
if (trimmed !== path8.basename(trimmed) || trimmed === "." || trimmed === ".." || trimmed.includes("/") || trimmed.includes("\\")) {
|
|
1478
1595
|
throw new Error(`Refusing a bot name that is not a plain file name: ${JSON.stringify(name)}`);
|
|
1479
1596
|
}
|
|
1480
|
-
return
|
|
1597
|
+
return path8.resolve(projectDir, "src", `${trimmed}.ts`);
|
|
1598
|
+
}
|
|
1599
|
+
function isInsideProject(projectDir, target) {
|
|
1600
|
+
const real = (p) => {
|
|
1601
|
+
try {
|
|
1602
|
+
return fs8.realpathSync.native(p);
|
|
1603
|
+
} catch {
|
|
1604
|
+
return path8.resolve(p);
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
const resolved = fs8.existsSync(target) ? real(target) : path8.join(real(path8.dirname(target)), path8.basename(target));
|
|
1608
|
+
const root = real(projectDir);
|
|
1609
|
+
if (resolved === root) return true;
|
|
1610
|
+
const relative = path8.relative(root, resolved);
|
|
1611
|
+
return relative !== "" && !relative.startsWith("..") && !path8.isAbsolute(relative);
|
|
1481
1612
|
}
|
|
1482
1613
|
function readProjectConfig(projectDir) {
|
|
1483
1614
|
try {
|
|
1484
|
-
const raw =
|
|
1615
|
+
const raw = fs8.readFileSync(path8.join(projectDir, "vibemancer.json"), "utf-8");
|
|
1485
1616
|
const parsed = JSON.parse(raw);
|
|
1486
1617
|
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
1487
1618
|
const bot = parsed.bot;
|
|
@@ -1500,50 +1631,56 @@ function pullDestination(projectDir, name, config) {
|
|
|
1500
1631
|
const configuredExport = config?.export;
|
|
1501
1632
|
if (!configured) return { path: fallback };
|
|
1502
1633
|
if (configuredExport && configuredExport.toLowerCase() === name.trim().toLowerCase()) {
|
|
1503
|
-
|
|
1634
|
+
const configuredPath = path8.resolve(projectDir, configured);
|
|
1635
|
+
if (!isInsideProject(projectDir, configuredPath)) {
|
|
1636
|
+
throw new LocalProjectError(`Refusing to write outside the project: this project's vibemancer.json points "bot" at ${JSON.stringify(configured)}, which resolves outside ${projectDir}. Fix the path in vibemancer.json, or pass --bot with a path inside the project.`);
|
|
1637
|
+
}
|
|
1638
|
+
return { path: configuredPath };
|
|
1504
1639
|
}
|
|
1505
1640
|
return {
|
|
1506
1641
|
path: fallback,
|
|
1507
1642
|
warning: `Note: this project's vibemancer.json uses ${configuredExport ?? "another bot"} (${configured}), so upload/fight will keep using that, not the file just written.`
|
|
1508
1643
|
};
|
|
1509
1644
|
}
|
|
1645
|
+
function writeGuarded(projectDir, target, sourceCode, force) {
|
|
1646
|
+
const isNew = !fs8.existsSync(target);
|
|
1647
|
+
const decision = pullDecision(isNew ? { fileExists: false, incomingContent: sourceCode, force } : {
|
|
1648
|
+
fileExists: true,
|
|
1649
|
+
localContent: fs8.readFileSync(target, "utf-8"),
|
|
1650
|
+
incomingContent: sourceCode,
|
|
1651
|
+
recordedHash: recordedHashFor(projectDir, target),
|
|
1652
|
+
force
|
|
1653
|
+
});
|
|
1654
|
+
if (decision === "skip-identical") {
|
|
1655
|
+
console.log(` Already up to date: ${target}`);
|
|
1656
|
+
recordWritten(projectDir, target, sourceCode);
|
|
1657
|
+
return "identical";
|
|
1658
|
+
}
|
|
1659
|
+
if (decision !== "write") {
|
|
1660
|
+
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";
|
|
1661
|
+
console.error("");
|
|
1662
|
+
console.error(` Refusing to overwrite ${target}: ${why}.`);
|
|
1663
|
+
console.error(" Save or commit it first, or re-run with --force to replace it.");
|
|
1664
|
+
console.error("");
|
|
1665
|
+
process.exitCode = 1;
|
|
1666
|
+
return "refused";
|
|
1667
|
+
}
|
|
1668
|
+
fs8.mkdirSync(path8.dirname(target), { recursive: true });
|
|
1669
|
+
fs8.writeFileSync(target, sourceCode);
|
|
1670
|
+
recordWritten(projectDir, target, sourceCode);
|
|
1671
|
+
console.log(` \u2713 ${isNew ? "Created" : "Updated"} ${target}`);
|
|
1672
|
+
console.log(` ${(sourceCode.length / 1024).toFixed(1)} KB written`);
|
|
1673
|
+
return "written";
|
|
1674
|
+
}
|
|
1510
1675
|
async function pullByName(projectDir, name, force) {
|
|
1511
1676
|
pullTargetPath(projectDir, name);
|
|
1512
1677
|
console.log("");
|
|
1513
1678
|
console.log(` Pulling latest source for ${name}...`);
|
|
1514
1679
|
try {
|
|
1515
1680
|
const pulled = await pullBot(name);
|
|
1516
|
-
const sourceCode = pulled.sourceCode;
|
|
1517
1681
|
const destination = pullDestination(projectDir, pulled.name, readProjectConfig(projectDir));
|
|
1518
|
-
const
|
|
1519
|
-
|
|
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);
|
|
1544
|
-
console.log(` \u2713 ${isNew ? "Created" : "Updated"} ${target}`);
|
|
1545
|
-
console.log(` ${(sourceCode.length / 1024).toFixed(1)} KB written`);
|
|
1546
|
-
if (destination.warning) console.log(` ${destination.warning}`);
|
|
1682
|
+
const outcome = writeGuarded(projectDir, destination.path, pulled.sourceCode, force);
|
|
1683
|
+
if (outcome !== "refused" && destination.warning) console.log(` ${destination.warning}`);
|
|
1547
1684
|
} catch (err) {
|
|
1548
1685
|
reportPullFailure(err, name);
|
|
1549
1686
|
}
|
|
@@ -1555,27 +1692,24 @@ function reportPullFailure(err, name) {
|
|
|
1555
1692
|
console.error("");
|
|
1556
1693
|
} else {
|
|
1557
1694
|
console.error(` Pull failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1558
|
-
|
|
1695
|
+
const advice = adviceFor(err, name);
|
|
1696
|
+
if (advice) console.error(advice);
|
|
1559
1697
|
console.error("");
|
|
1560
1698
|
}
|
|
1561
1699
|
process.exitCode = 1;
|
|
1562
1700
|
}
|
|
1563
1701
|
async function runPull(options) {
|
|
1564
|
-
const projectDir = process.cwd();
|
|
1702
|
+
const projectDir = findProjectRoot(process.cwd());
|
|
1565
1703
|
if (options.name) {
|
|
1566
1704
|
await pullByName(projectDir, options.name, options.force === true);
|
|
1567
1705
|
return;
|
|
1568
1706
|
}
|
|
1569
1707
|
const botInfo = await discoverBot(projectDir, options.bot, { allowMissingFile: true });
|
|
1570
|
-
const isNew = !fs7.existsSync(botInfo.sourcePath);
|
|
1571
1708
|
console.log(`
|
|
1572
1709
|
Pulling latest source for ${botInfo.exportName}...`);
|
|
1573
1710
|
try {
|
|
1574
1711
|
const sourceCode = await pullSource(botInfo.exportName);
|
|
1575
|
-
|
|
1576
|
-
fs7.writeFileSync(botInfo.sourcePath, sourceCode);
|
|
1577
|
-
console.log(` \u2713 ${isNew ? "Created" : "Updated"} ${botInfo.sourcePath}`);
|
|
1578
|
-
console.log(` ${(sourceCode.length / 1024).toFixed(1)} KB written`);
|
|
1712
|
+
writeGuarded(projectDir, botInfo.sourcePath, sourceCode, options.force === true);
|
|
1579
1713
|
} catch (err) {
|
|
1580
1714
|
reportPullFailure(err);
|
|
1581
1715
|
}
|
|
@@ -1690,7 +1824,8 @@ async function revokeSession(token) {
|
|
|
1690
1824
|
const res = await fetch(`${MCP_BASE_URL}/revoke`, {
|
|
1691
1825
|
method: "POST",
|
|
1692
1826
|
headers: { "Content-Type": "application/json" },
|
|
1693
|
-
body: JSON.stringify({ token })
|
|
1827
|
+
body: JSON.stringify({ token }),
|
|
1828
|
+
signal: AbortSignal.timeout(AUTH_TIMEOUT_MS)
|
|
1694
1829
|
});
|
|
1695
1830
|
return res.ok;
|
|
1696
1831
|
} catch {
|
|
@@ -1713,7 +1848,7 @@ async function runLogout() {
|
|
|
1713
1848
|
|
|
1714
1849
|
// src/commands/feedback.ts
|
|
1715
1850
|
async function defaultTokenLookup() {
|
|
1716
|
-
const { getAccessToken: getAccessToken2 } = await import("./oauth-client-
|
|
1851
|
+
const { getAccessToken: getAccessToken2 } = await import("./oauth-client-WUOWASU4.js");
|
|
1717
1852
|
return await getAccessToken2();
|
|
1718
1853
|
}
|
|
1719
1854
|
async function runFeedback(options, tokenLookup = defaultTokenLookup) {
|
|
@@ -1767,7 +1902,19 @@ async function runFeedback(options, tokenLookup = defaultTokenLookup) {
|
|
|
1767
1902
|
}
|
|
1768
1903
|
|
|
1769
1904
|
// src/commands/missile-calc.ts
|
|
1770
|
-
import { calculateMissileCastTime, validateMissileConfig, GCD_DURATION, TICKS_PER_SECOND, calculateMissileRadius } from "@vibemancer/core";
|
|
1905
|
+
import { calculateMissileCastTime, validateMissileConfig, GCD_DURATION, MATCH_DURATION, TICKS_PER_SECOND, calculateMissileRadius } from "@vibemancer/core";
|
|
1906
|
+
function describeAdjustments(raw, used) {
|
|
1907
|
+
const fields = ["damage", "speed", "duration", "turnRate"];
|
|
1908
|
+
const changed = fields.filter((f) => raw[f] !== used[f]);
|
|
1909
|
+
if (changed.length === 0) return null;
|
|
1910
|
+
const lines = changed.map((f) => ` ${f}: you asked for ${raw[f]}, the engine will use ${used[f]}`);
|
|
1911
|
+
return [" Adjusted to the engine's limits:", ...lines].join("\n");
|
|
1912
|
+
}
|
|
1913
|
+
function describeCastability(castFrames) {
|
|
1914
|
+
if (castFrames < MATCH_DURATION) return null;
|
|
1915
|
+
return ` NOTE: this cast takes ${castFrames} frames and a match is only ${MATCH_DURATION} - it never completes.
|
|
1916
|
+
The DPS above is theoretical: the missile is never launched.`;
|
|
1917
|
+
}
|
|
1771
1918
|
function runMissileCalc(options) {
|
|
1772
1919
|
const rawConfig = {
|
|
1773
1920
|
damage: options.damage,
|
|
@@ -1783,6 +1930,7 @@ function runMissileCalc(options) {
|
|
|
1783
1930
|
const dps = config.damage / totalCycleSec;
|
|
1784
1931
|
const range = config.speed * config.duration;
|
|
1785
1932
|
const radius = calculateMissileRadius(config.damage);
|
|
1933
|
+
const notes = [describeAdjustments(rawConfig, config), describeCastability(castFrames)].filter((n) => n !== null).join("\n");
|
|
1786
1934
|
console.log(`
|
|
1787
1935
|
Missile Calculator
|
|
1788
1936
|
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
@@ -1793,13 +1941,13 @@ function runMissileCalc(options) {
|
|
|
1793
1941
|
Eff. DPS: ${dps.toFixed(2)} HP/s
|
|
1794
1942
|
Range: ${range} units (speed \xD7 duration)
|
|
1795
1943
|
Hitbox: ${radius.toFixed(2)} radius
|
|
1796
|
-
`);
|
|
1944
|
+
${notes ? notes + "\n" : ""}`);
|
|
1797
1945
|
}
|
|
1798
1946
|
|
|
1799
1947
|
// src/auth/telemetry.ts
|
|
1800
1948
|
import os2 from "os";
|
|
1801
|
-
import
|
|
1802
|
-
import
|
|
1949
|
+
import fs9 from "fs";
|
|
1950
|
+
import path9 from "path";
|
|
1803
1951
|
var TIMEOUT_MS = 1e3;
|
|
1804
1952
|
function isTelemetryEnabled(env) {
|
|
1805
1953
|
const off = (value) => {
|
|
@@ -1819,13 +1967,13 @@ function isTelemetryEnabled(env) {
|
|
|
1819
1967
|
}
|
|
1820
1968
|
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";
|
|
1821
1969
|
function noticePath() {
|
|
1822
|
-
return
|
|
1970
|
+
return path9.join(os2.homedir(), ".vibemancer", "telemetry-notice-shown");
|
|
1823
1971
|
}
|
|
1824
1972
|
function showNoticeOnce(marker = noticePath()) {
|
|
1825
1973
|
try {
|
|
1826
|
-
if (
|
|
1827
|
-
|
|
1828
|
-
|
|
1974
|
+
if (fs9.existsSync(marker)) return false;
|
|
1975
|
+
fs9.mkdirSync(path9.dirname(marker), { recursive: true });
|
|
1976
|
+
fs9.writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString());
|
|
1829
1977
|
console.log(TELEMETRY_NOTICE);
|
|
1830
1978
|
return true;
|
|
1831
1979
|
} catch {
|
|
@@ -1857,18 +2005,24 @@ function parseFlag(flag) {
|
|
|
1857
2005
|
}
|
|
1858
2006
|
return void 0;
|
|
1859
2007
|
}
|
|
1860
|
-
function
|
|
2008
|
+
function parseNumericFlag(flag, fallback, kind) {
|
|
1861
2009
|
const str = parseFlag(flag);
|
|
1862
2010
|
if (str === void 0) return fallback;
|
|
1863
|
-
const
|
|
1864
|
-
if (
|
|
1865
|
-
console.error(`Error: ${
|
|
2011
|
+
const parsed = parseNumberFlag(flag, str, kind);
|
|
2012
|
+
if (!parsed.ok) {
|
|
2013
|
+
console.error(`Error: ${parsed.message}`);
|
|
1866
2014
|
process.exit(1);
|
|
1867
2015
|
}
|
|
1868
|
-
return
|
|
2016
|
+
return parsed.value;
|
|
1869
2017
|
}
|
|
1870
|
-
function
|
|
1871
|
-
|
|
2018
|
+
function parseIntFlag(flag, fallback) {
|
|
2019
|
+
return parseNumericFlag(flag, fallback, "integer");
|
|
2020
|
+
}
|
|
2021
|
+
function parseFloatFlag(flag, fallback) {
|
|
2022
|
+
return parseNumericFlag(flag, fallback, "number");
|
|
2023
|
+
}
|
|
2024
|
+
function helpText() {
|
|
2025
|
+
return `
|
|
1872
2026
|
Vibemancer CLI - Development tools for wizard bots
|
|
1873
2027
|
|
|
1874
2028
|
Usage:
|
|
@@ -1908,6 +2062,27 @@ Trace options:
|
|
|
1908
2062
|
--opponent <name> Opponent: a built-in name OR handle/botname (required)
|
|
1909
2063
|
--seed <n> Random seed (default: 1)
|
|
1910
2064
|
--distance <n> Spawn distance (default: 600)
|
|
2065
|
+
--ticks <n> How many ticks to trace (default: 3000, a full match is 30000)
|
|
2066
|
+
|
|
2067
|
+
Bots options:
|
|
2068
|
+
--mine List YOUR uploaded bots instead of the built-ins
|
|
2069
|
+
--name <bot> Show one bot by name
|
|
2070
|
+
|
|
2071
|
+
Pull options:
|
|
2072
|
+
--name <bot> Pull by name, with no local file needed
|
|
2073
|
+
--bot <path> Write to this path instead of the project's configured bot
|
|
2074
|
+
--force Overwrite local changes the CLI did not write. Do not reach for
|
|
2075
|
+
this by default: pull refuses precisely when the local file may
|
|
2076
|
+
hold work that was never uploaded.
|
|
2077
|
+
|
|
2078
|
+
Login options:
|
|
2079
|
+
--no-browser Print the URL instead of opening a browser, and paste the code back
|
|
2080
|
+
|
|
2081
|
+
Missile-calc options:
|
|
2082
|
+
--damage <n> Hit point damage (default: 15)
|
|
2083
|
+
--speed <n> Units per frame (default: 7; the engine's minimum is 1.5)
|
|
2084
|
+
--duration <n> Lifetime in frames (default: 200; range = speed x duration)
|
|
2085
|
+
--turnRate <n> Degrees per frame the missile can turn (default: 0 = straight)
|
|
1911
2086
|
|
|
1912
2087
|
Upload options:
|
|
1913
2088
|
--bot <path> Path to your bot source file
|
|
@@ -1947,7 +2122,10 @@ Examples:
|
|
|
1947
2122
|
vibemancer build --opponent Battlemage
|
|
1948
2123
|
vibemancer feedback "missiles go through walls sometimes"
|
|
1949
2124
|
vibemancer missile-calc --damage 15 --speed 7 --duration 200 --turnRate 3
|
|
1950
|
-
|
|
2125
|
+
`;
|
|
2126
|
+
}
|
|
2127
|
+
function printHelp() {
|
|
2128
|
+
console.log(helpText());
|
|
1951
2129
|
}
|
|
1952
2130
|
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
1953
2131
|
"--bot",
|
|
@@ -1985,7 +2163,11 @@ var KNOWN_FLAGS = {
|
|
|
1985
2163
|
// per-bot concept. The flag was accepted, dropped, and the run reported success for a bot
|
|
1986
2164
|
// path that did not exist. A flag that cannot work has to be refused, not swallowed.
|
|
1987
2165
|
test: [],
|
|
1988
|
-
|
|
2166
|
+
// --distance is NOT here, and that is the fix rather than an oversight: it is documented
|
|
2167
|
+
// under Trace options, the fight branch never read it, and runFight has no such option. It
|
|
2168
|
+
// was accepted and dropped, so `fight --distance 300` silently ran at the default 600. A
|
|
2169
|
+
// flag that cannot work has to be refused by name, exactly as the `test: []` note says.
|
|
2170
|
+
fight: ["--bot", "--opponent", "--seed"],
|
|
1989
2171
|
trace: ["--bot", "--opponent", "--seed", "--distance", "--ticks"],
|
|
1990
2172
|
tournament: ["--bot"],
|
|
1991
2173
|
optimize: ["--bot", "--steps", "--rounds", "--opponents", "--seed"],
|
|
@@ -1998,7 +2180,15 @@ var KNOWN_FLAGS = {
|
|
|
1998
2180
|
// flag was accepted and dropped, so `pull --nmae Wiz2` fell through to file discovery and
|
|
1999
2181
|
// reported "could not find bot source file" about a flag the user had spelled wrong.
|
|
2000
2182
|
pull: ["--bot", "--name", "--force"],
|
|
2001
|
-
"missile-calc": ["--damage", "--speed", "--duration", "--turnRate"]
|
|
2183
|
+
"missile-calc": ["--damage", "--speed", "--duration", "--turnRate"],
|
|
2184
|
+
// login/logout were absent, so a typo'd flag was silently ignored - the same defect the
|
|
2185
|
+
// fight and upload entries above were added for.
|
|
2186
|
+
login: ["--no-browser"],
|
|
2187
|
+
logout: []
|
|
2188
|
+
// feedback is DELIBERATELY absent, and it is the one command that must stay that way: its
|
|
2189
|
+
// argument is a free-text message, joined from every remaining token. Validating flags here
|
|
2190
|
+
// would reject 'missiles go through walls --sometimes' as an unknown flag, turning a bug
|
|
2191
|
+
// report into an error. A command whose argument is prose cannot also police prose.
|
|
2002
2192
|
};
|
|
2003
2193
|
var BOOLEAN_FLAGS = ["--mine", "--inactive", "--active", "--no-browser", "--force"];
|
|
2004
2194
|
function flagsMissingValues(command2, argv) {
|
|
@@ -2026,6 +2216,19 @@ function unknownFlags(command2, argv) {
|
|
|
2026
2216
|
}
|
|
2027
2217
|
return found;
|
|
2028
2218
|
}
|
|
2219
|
+
function duplicateFlags(argv) {
|
|
2220
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2221
|
+
const repeated = /* @__PURE__ */ new Set();
|
|
2222
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2223
|
+
const arg = argv[i];
|
|
2224
|
+
if (!arg.startsWith("--")) continue;
|
|
2225
|
+
const previous = i > 0 ? argv[i - 1] : "";
|
|
2226
|
+
if (VALUE_FLAGS.has(previous)) continue;
|
|
2227
|
+
if (seen.has(arg)) repeated.add(arg);
|
|
2228
|
+
seen.add(arg);
|
|
2229
|
+
}
|
|
2230
|
+
return [...repeated];
|
|
2231
|
+
}
|
|
2029
2232
|
function editDistance(a, b) {
|
|
2030
2233
|
const rows = [];
|
|
2031
2234
|
for (let i = 0; i <= a.length; i++) rows.push([i, ...new Array(b.length).fill(0)]);
|
|
@@ -2080,6 +2283,14 @@ async function main() {
|
|
|
2080
2283
|
Run "vibemancer --help" to see the flags each command takes.`);
|
|
2081
2284
|
process.exit(1);
|
|
2082
2285
|
}
|
|
2286
|
+
const repeated = duplicateFlags(args.slice(1));
|
|
2287
|
+
if (repeated.length > 0) {
|
|
2288
|
+
for (const flag of repeated) {
|
|
2289
|
+
console.error(`Error: ${flag} was given more than once, and only the first would be used.`);
|
|
2290
|
+
}
|
|
2291
|
+
console.error("Remove the extra one so there is no doubt which value you meant.");
|
|
2292
|
+
process.exit(1);
|
|
2293
|
+
}
|
|
2083
2294
|
const valueless = flagsMissingValues(command, args.slice(1));
|
|
2084
2295
|
if (valueless.length > 0) {
|
|
2085
2296
|
for (const flag of valueless) {
|
|
@@ -2125,7 +2336,7 @@ ${list}
|
|
|
2125
2336
|
}
|
|
2126
2337
|
const bot = parseFlag("--bot");
|
|
2127
2338
|
const seed = parseFlag("--seed") !== void 0 ? parseIntFlag("--seed", 0) : void 0;
|
|
2128
|
-
const distance = parseFlag("--distance") !== void 0 ?
|
|
2339
|
+
const distance = parseFlag("--distance") !== void 0 ? parseFloatFlag("--distance", 600) : void 0;
|
|
2129
2340
|
const maxTicks = parseFlag("--ticks") !== void 0 ? parseIntFlag("--ticks", 3e3) : void 0;
|
|
2130
2341
|
await runTrace({ opponent, bot, seed, distance, maxTicks });
|
|
2131
2342
|
break;
|
|
@@ -2205,10 +2416,12 @@ ${list}
|
|
|
2205
2416
|
}
|
|
2206
2417
|
case "missile-calc": {
|
|
2207
2418
|
runMissileCalc({
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2419
|
+
// Fractional: MISSILE_MIN_SPEED is 1.5, so an integer-only --speed could not express
|
|
2420
|
+
// the engine's own minimum. The engine floors duration itself.
|
|
2421
|
+
damage: parseFloatFlag("--damage", 15),
|
|
2422
|
+
speed: parseFloatFlag("--speed", 7),
|
|
2423
|
+
duration: parseFloatFlag("--duration", 200),
|
|
2424
|
+
turnRate: parseFloatFlag("--turnRate", 0)
|
|
2212
2425
|
});
|
|
2213
2426
|
break;
|
|
2214
2427
|
}
|
|
@@ -2231,8 +2444,11 @@ main().catch((error) => {
|
|
|
2231
2444
|
process.exit(1);
|
|
2232
2445
|
});
|
|
2233
2446
|
export {
|
|
2447
|
+
KNOWN_FLAGS,
|
|
2448
|
+
duplicateFlags,
|
|
2234
2449
|
flagHint,
|
|
2235
2450
|
flagsMissingValues,
|
|
2451
|
+
helpText,
|
|
2236
2452
|
suggestFlag,
|
|
2237
2453
|
unknownFlags,
|
|
2238
2454
|
wantsHelp
|