@scm-js/cli 0.8.9 → 0.8.11

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.
@@ -1,4 +1,4 @@
1
- Third-party notices for scmjs 0.8.9
1
+ Third-party notices for scmjs 0.8.11
2
2
 
3
3
  scmjs is MIT-licensed (LICENSE). It is built into one file from scmJS's own code and the
4
4
  packages below, whose license texts this file carries. Who wrote what, and where the
package/dist/scmjs.mjs CHANGED
@@ -6856,13 +6856,44 @@ document.getElementById("go").onclick = go;
6856
6856
  }
6857
6857
  //#endregion
6858
6858
  //#region cli/src/about.ts
6859
- /** `scmjs about`: this build, who made it, the machine it runs on, the desktop app and Docker, and where everything is. */
6860
- var VERSION = "0.8.9";
6861
- var COMMIT = "0469680";
6859
+ /** `scmjs about`: this build and whether npm has a newer one, who made it, the machine it runs on, the desktop app and Docker, and where everything is. */
6860
+ var VERSION = "0.8.11";
6861
+ var COMMIT = "71d8282";
6862
6862
  var BUILT = "2026-11-06";
6863
- function aboutReport() {
6863
+ var PACKAGE = "@scm-js/cli";
6864
+ /** a < b, for x.y.z versions (a prerelease or build suffix is ignored). */
6865
+ function olderThan(a, b) {
6866
+ const n = (v) => v.split(/[-+]/)[0].split(".").map((x) => Number(x) || 0);
6867
+ const [x, y] = [n(a), n(b)];
6868
+ for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) < (y[i] ?? 0);
6869
+ return false;
6870
+ }
6871
+ /** The newest version on npm, or null when npm could not be asked (offline, slow): two seconds at most. */
6872
+ async function newestVersion() {
6873
+ try {
6874
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE}/latest`, {
6875
+ signal: AbortSignal.timeout(2e3),
6876
+ headers: { accept: "application/json" }
6877
+ });
6878
+ if (!res.ok) return null;
6879
+ const v = (await res.json()).version;
6880
+ return typeof v === "string" ? v : null;
6881
+ } catch {
6882
+ return null;
6883
+ }
6884
+ }
6885
+ /** About's "Newest" row: the update to install, this one, or what npm has when this is newer (a build from a clone). */
6886
+ function newestLine(version, latest) {
6887
+ if (latest === null) return import_picocolors.default.dim("could not ask npm");
6888
+ if (olderThan(version, latest)) return `${import_picocolors.default.yellow(latest)}${dot}npm install -g ${PACKAGE}@latest`;
6889
+ return import_picocolors.default.dim(latest === version ? "this one" : `${latest} on npm`);
6890
+ }
6891
+ async function aboutReport() {
6892
+ const latest = await newestVersion();
6864
6893
  return {
6865
6894
  version: VERSION,
6895
+ latest,
6896
+ outdated: latest !== null && olderThan("0.8.11", latest),
6866
6897
  commit: COMMIT,
6867
6898
  built: BUILT,
6868
6899
  node: process.version,
@@ -6902,6 +6933,7 @@ function printAbout(r) {
6902
6933
  r.commit && `commit ${r.commit}`,
6903
6934
  r.built && `built ${r.built}`
6904
6935
  ].filter(Boolean).join(dot)],
6936
+ ["Newest", newestLine(r.version, r.latest)],
6905
6937
  ["Node", `${r.node}${dot}${r.platform}`],
6906
6938
  ["Desktop app", r.desktopApp ?? import_picocolors.default.dim("not found (scmjs … open uses the browser)")],
6907
6939
  ["Docker", r.docker ? `${r.docker}` : import_picocolors.default.dim("not found (scmjs bot needs it)")],
@@ -6921,6 +6953,68 @@ function printAbout(r) {
6921
6953
  ];
6922
6954
  }
6923
6955
  //#endregion
6956
+ //#region cli/src/banner.ts
6957
+ /**
6958
+ * The app mark, the wireframe globe of `public/favicon.svg` and the splash screen, in braille (2 × 4 dots a
6959
+ * character): the splash's sphere (`src/components/splash/starfield.ts`) with 6 rings and 8 segments, turned 0.3 rad
6960
+ * about y and −0.45 about x, its near half and its outline, on 32 × 32 dots.
6961
+ */
6962
+ var GLOBE = [
6963
+ " ⣠⡴⢶⡿⣿⢿⢿⣲⢦⣄ ",
6964
+ " ⣠⠞⣡⠟⠯⢤⡧⠬⢷⡴⠛⠮⡳⣄ ",
6965
+ "⢰⢳⡼⠁ ⡞ ⢣ ⣘⣾⡆",
6966
+ "⡇⡼⠉⠙⠒⢦⠧⠤⠤⠤⠼⡖⠋⠁⠘⣾",
6967
+ "⡷⣇ ⢸ ⢧ ⣠⣿",
6968
+ "⠸⣏⠙⠒⠦⢼⠤⠤⠤⠤⠤⢼⠒⠋⢡⠇",
6969
+ " ⠙⢦⣀⡀⢸ ⣞⣀⡴⠋ ",
6970
+ " ⠙⠻⠭⣯⣓⣒⣫⠽⠟⠋ "
6971
+ ];
6972
+ /** The same globe in ASCII, for a console without braille (the old Windows one, where ui.ts's marks fall back too). */
6973
+ var GLOBE_ASCII = [
6974
+ " .-'''-. ",
6975
+ " .'/ | \\'. ",
6976
+ " /-+---+---+-\\ ",
6977
+ " | | | | | ",
6978
+ " |==+===+===+==| ",
6979
+ " \\-+---+---+-/ ",
6980
+ " '.\\ | /.' ",
6981
+ " '-...-' "
6982
+ ];
6983
+ /** The logo's violet (#a06cf0), or the terminal's magenta without 24-bit colour. */
6984
+ var violet = (s) => truecolor ? rgb("#a06cf0", s) : import_picocolors.default.magenta(s);
6985
+ /**
6986
+ * `text` beside the globe, centred on its height; the text alone when the terminal is narrower than both. `width` is
6987
+ * the terminal's (80 when it is not one).
6988
+ */
6989
+ function banner(text, { width = 80, ascii = !unicode } = {}) {
6990
+ const art = ascii ? GLOBE_ASCII : GLOBE;
6991
+ const artWidth = Math.max(...art.map((l) => l.length));
6992
+ const indent = " ", gap = " ";
6993
+ if (2 + artWidth + 3 + Math.max(...text.map((l) => visible(l).length)) > width) return text;
6994
+ const top = Math.max(0, Math.floor((art.length - text.length) / 2));
6995
+ return art.map((l, i) => text[i - top] ? `${indent}${violet(l.padEnd(artWidth))}${gap}${text[i - top]}` : `${indent}${violet(l.trimEnd())}`);
6996
+ }
6997
+ /** The overview, in place of commander's: the banner, the command groups as commander lays them out, the options. */
6998
+ function rootHelp(cmd, helper, version, opts = {}) {
6999
+ const termWidth = helper.padWidth(cmd, helper);
7000
+ const out = banner([
7001
+ `${import_picocolors.default.bold(import_picocolors.default.cyan(cmd.name()))} ${import_picocolors.default.bold(version)}`,
7002
+ "scmJS on the command line",
7003
+ import_picocolors.default.dim("StarCraft: Brood War maps, replays, plugins and bots"),
7004
+ "",
7005
+ `${helper.styleTitle("Usage:")} ${helper.styleUsage(helper.commandUsage(cmd))}`,
7006
+ `${helper.styleTitle("Docs:")} ${import_picocolors.default.cyan("https://docs.scmjs.dev")}`
7007
+ ], {
7008
+ width: helper.helpWidth ?? 80,
7009
+ ...opts
7010
+ });
7011
+ out.push("");
7012
+ const groups = helper.groupItems([...cmd.commands], helper.visibleCommands(cmd), (sub) => sub.helpGroup() || "Commands:");
7013
+ for (const [heading, commands] of groups) out.push(...helper.formatItemList(heading, commands.map((sub) => helper.formatItem(helper.styleSubcommandTerm(helper.subcommandTerm(sub)), termWidth, helper.styleSubcommandDescription(helper.subcommandDescription(sub)), helper)), helper));
7014
+ out.push(`${helper.styleTitle("Options:")} ${helper.visibleOptions(cmd).map((o) => helper.styleOptionTerm(helper.optionTerm(o))).join(" ")}`, "");
7015
+ return out.join("\n");
7016
+ }
7017
+ //#endregion
6924
7018
  //#region src/formats/chk/binary.ts
6925
7019
  /** Little-endian readers/writers over a Uint8Array. StarCraft data is LE throughout. */
6926
7020
  var Reader$1 = class {
@@ -12976,10 +13070,18 @@ async function restoreBuiltMap(chk, extras) {
12976
13070
  };
12977
13071
  }
12978
13072
  //#endregion
12979
- //#region src/services/mapIo.ts
12980
- /** Read a picked or dropped file into a parsed document. */
12981
- async function openMapFile(file, handle = null) {
12982
- const loaded = await loadMap(new Uint8Array(await file.arrayBuffer()));
13073
+ //#region src/editor/mapFile.ts
13074
+ /**
13075
+ * A map file as bytes, in and out — the half of opening and saving that needs no browser.
13076
+ *
13077
+ * `openMapBytes` is what File ▸ Open, a drop, the command line and `@scm-js/core` all run:
13078
+ * the archive read, the scenario parsed, the other members kept, a built map restored to the
13079
+ * map it was built from, and the warnings the editor would show. `writeMapBytes` is the
13080
+ * reverse. The pickers, handles and downloads around them are `services/mapIo.ts`.
13081
+ */
13082
+ /** Read a map file's bytes (.scm, .scx or a bare .chk) into a parsed scenario and the rest of its archive. */
13083
+ async function openMapBytes(bytes, fileName) {
13084
+ const loaded = await loadMap(bytes);
12983
13085
  const problems = [];
12984
13086
  let members = {
12985
13087
  extras: /* @__PURE__ */ new Map(),
@@ -13025,8 +13127,7 @@ async function openMapFile(file, handle = null) {
13025
13127
  scenario,
13026
13128
  extras,
13027
13129
  stored,
13028
- fileName: file.name,
13029
- handle,
13130
+ fileName,
13030
13131
  origin: loaded.scenarioInfo,
13031
13132
  builtBy
13032
13133
  };
@@ -13677,7 +13778,7 @@ function kindOf(name, bytes) {
13677
13778
  }
13678
13779
  async function loadMapInput(input) {
13679
13780
  try {
13680
- return await openMapFile(new File([input.bytes], input.name));
13781
+ return await openMapBytes(input.bytes, input.name);
13681
13782
  } catch (e) {
13682
13783
  throw new CliError(`${input.name} is not a map this can read: ${e instanceof Error ? e.message : e}`);
13683
13784
  }
@@ -16215,31 +16316,23 @@ for (const stream of [process.stdout, process.stderr]) stream.on("error", (e) =>
16215
16316
  if (e.code === "EPIPE") process.exit(process.exitCode ?? 0);
16216
16317
  else throw e;
16217
16318
  });
16218
- var program = new Command$1("scmjs").description("StarCraft: Brood War maps, replays, plugins and bots from the command line").version(VERSION, "-v, --version", "print the version").helpOption("-h, --help", "show help for a command").addOption(new Option("--no-color", "plain text, no colour (also NO_COLOR=1)")).showSuggestionAfterError().helpCommand("help [command]", "show help for a command").configureOutput({ outputError: (text, write) => write(text.replace(/^error: /, `${mark.error} `)) }).showHelpAfterError(import_picocolors.default.dim("(scmjs --help lists the commands)")).configureHelp({
16319
+ var program = new Command$1("scmjs").description("StarCraft: Brood War maps, replays, plugins and bots from the command line").version(VERSION, "-v, --version", "print the version").helpOption("-h, --help", "show help for a command").addOption(new Option("--no-color", "plain text, no colour (also NO_COLOR=1)")).showSuggestionAfterError().commandsGroup("This tool:").helpCommand("help [command]", "show help for a command").configureOutput({ outputError: (text, write) => write(text.replace(/^error: /, `${mark.error} `)) }).showHelpAfterError(import_picocolors.default.dim("(scmjs --help lists the commands)")).configureHelp({
16219
16320
  styleTitle: (s) => import_picocolors.default.bold(s),
16220
16321
  styleCommandText: (s) => import_picocolors.default.cyan(s),
16221
16322
  styleSubcommandText: (s) => import_picocolors.default.cyan(s),
16222
16323
  styleArgumentText: (s) => import_picocolors.default.yellow(s),
16223
- styleOptionText: (s) => import_picocolors.default.green(s)
16224
- }).addHelpText("after", () => `
16225
- ${import_picocolors.default.bold("Examples:")}
16226
- ${import_picocolors.default.dim("$")} scmjs info "(2)Destination.scx" what a map or a replay holds
16227
- ${import_picocolors.default.dim("$")} scmjs map check *.scx Check Map on every map here
16228
- ${import_picocolors.default.dim("$")} scmjs replay open game.rep watch it in the editor
16229
- ${import_picocolors.default.dim("$")} scmjs plugin new "Base Check" a plugin for the editor to start from
16230
- ${import_picocolors.default.dim("$")} scmjs bot new a BWAPI bot to start from, for scmjs.dev/play
16231
- ${import_picocolors.default.dim("$")} scmjs about this version, and where things are
16232
-
16233
- ${import_picocolors.default.dim("Docs: https://docs.scmjs.dev")}`);
16234
- program.command("about").description("this version of scmjs, and where things are").addOption(json).action((o) => {
16235
- const r = aboutReport();
16236
- print(...o.json ? [JSON.stringify(r, null, 2)] : printAbout(r));
16324
+ styleOptionText: (s) => import_picocolors.default.green(s),
16325
+ subcommandTerm: (cmd) => {
16326
+ const subs = cmd.commands.map((c) => c.name()).filter((n) => n !== "help");
16327
+ return subs.length ? `${cmd.name()} ${subs.join("|")}` : new Help$1().subcommandTerm(cmd).replace(" [options]", "");
16328
+ },
16329
+ formatHelp: (cmd, helper) => cmd.parent ? Help$1.prototype.formatHelp.call(helper, cmd, helper) : rootHelp(cmd, helper, VERSION)
16237
16330
  });
16238
- program.command("info").description("what a map, replay or bot file holds").argument("<files...>", "maps (.scm .scx .chk), replays (.rep) or bots (.scmbot)").addOption(json).action(async (files, o) => void await eachFile(files, !!o.json, async (input) => {
16331
+ program.command("info").description("what a map, replay or bot file holds").helpGroup("Maps and replays:").argument("<files...>", "maps (.scm .scx .chk), replays (.rep) or bots (.scmbot)").addOption(json).action(async (files, o) => void await eachFile(files, !!o.json, async (input) => {
16239
16332
  const kind = expectKind(input, "map", "replay", "bot");
16240
16333
  return kind === "map" ? mapInfo(input) : kind === "replay" ? replayInfo(input) : botInfo(input);
16241
16334
  }));
16242
- var map = program.command("map").description("read, check and open maps");
16335
+ var map = program.command("map").description("read, check and open maps").helpGroup("Maps and replays:");
16243
16336
  map.command("info").description("a map's size, players, units, triggers and its check").argument("<maps...>").addOption(json).action(async (files, o) => void await eachFile(files, !!o.json, mapInfo));
16244
16337
  map.command("check").description("Check Map, as the editor's: errors, then warnings").argument("<maps...>").addOption(json).option("--all", "list notes too, not only errors and warnings").option("--strict", "fail on warnings as well as errors").addHelpText("after", `\nExits 1 when a map has errors (or warnings, with --strict): fit for a CI step.`).action(async (files, o) => {
16245
16338
  const reports = await eachFile(files, !!o.json, async (input) => {
@@ -16266,14 +16359,14 @@ map.command("open").description("open a map in the scmJS app, or the editor in y
16266
16359
  expectKind(input, "map");
16267
16360
  await openInScmjs(input, o);
16268
16361
  });
16269
- var replay = program.command("replay").description("read and open replays");
16362
+ var replay = program.command("replay").description("read and open replays").helpGroup("Maps and replays:");
16270
16363
  replay.command("info").description("who played, on what map, for how long, at what APM").argument("<replays...>").addOption(json).action(async (files, o) => void await eachFile(files, !!o.json, replayInfo));
16271
16364
  replay.command("open").description("watch a replay in the scmJS app, or the editor in your browser").argument("<replay>").option("--browser", "the editor in the browser, even with the app installed").addOption(new Option("--editor <url>", "the editor's address (SCMJS_EDITOR_URL)").hideHelp()).action(async (file, o) => {
16272
16365
  const input = await readInput(file);
16273
16366
  expectKind(input, "replay");
16274
16367
  await openInScmjs(input, o);
16275
16368
  });
16276
- var plugin = program.command("plugin").description("write a plugin for the editor: start one, check it, run it");
16369
+ var plugin = program.command("plugin").description("start, check and run a plugin for the editor").helpGroup("Plugins and bots:");
16277
16370
  plugin.command("new").description("a plugin to start from, in a new folder").argument("[name]", "the plugin's name, such as \"Base Check\"").option("--dir <folder>", "the folder to write it in (default plugin-<id>)").action((name, o) => {
16278
16371
  const made = newPlugin({
16279
16372
  name,
@@ -16297,7 +16390,7 @@ plugin.command("dev").description("the editor with your plugin, reloaded on ever
16297
16390
  editor: o.editor
16298
16391
  });
16299
16392
  });
16300
- var bot = program.command("bot").description("build your own BWAPI bot for scmjs.dev/play (needs Docker)").addHelpText("after", () => `
16393
+ var bot = program.command("bot").description("build a BWAPI bot for scmjs.dev/play (Docker)").helpGroup("Plugins and bots:").addHelpText("after", () => `
16301
16394
  Run these in your bot's folder. The toolchain is a Docker image, downloaded the first time.
16302
16395
  A built bot is added on the play page: Options → Community bots → Add a bot from a file…
16303
16396
 
@@ -16314,8 +16407,13 @@ bot.command("build").description("out/<key>.scmbot, to add on the play page").ac
16314
16407
  bot.command("update").description("the newest toolchain, the one the play page expects").action(async () => {
16315
16408
  await updateBot();
16316
16409
  });
16317
- try {
16318
- await program.parseAsync();
16410
+ program.command("about").description("this version, any newer one, and links").helpGroup("This tool:").addOption(json).action(async (o) => {
16411
+ const r = await aboutReport();
16412
+ print(...o.json ? [JSON.stringify(r, null, 2)] : printAbout(r));
16413
+ });
16414
+ if (!process.env.VITEST) try {
16415
+ if (process.argv.length <= 2) program.outputHelp();
16416
+ else await program.parseAsync();
16319
16417
  } catch (e) {
16320
16418
  if (e instanceof CliError) {
16321
16419
  fail(e);
@@ -16326,4 +16424,4 @@ try {
16326
16424
  }
16327
16425
  }
16328
16426
  //#endregion
16329
- export {};
16427
+ export { program };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scm-js/cli",
3
- "version": "0.8.9",
3
+ "version": "0.8.11",
4
4
  "description": "StarCraft: Brood War maps, replays and bots from the command line: what a map or replay holds, Check Map, opening them in scmJS, and building a BWAPI bot for the play page",
5
5
  "keywords": [
6
6
  "starcraft",