@scm-js/cli 0.8.8 → 0.8.10
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/THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/scmjs.mjs +875 -22
- package/package.json +6 -3
package/dist/scmjs.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import path, { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
2
3
|
import { EventEmitter } from "node:events";
|
|
3
|
-
import childProcess, { execFileSync, spawn } from "node:child_process";
|
|
4
|
-
import
|
|
5
|
-
import fs, { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import childProcess, { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import fs, { existsSync, mkdirSync, readFileSync, readdirSync, statSync, watch, writeFileSync } from "node:fs";
|
|
6
6
|
import process$1 from "node:process";
|
|
7
7
|
import { stripVTControlCharacters } from "node:util";
|
|
8
8
|
import { createServer } from "node:http";
|
|
@@ -6081,6 +6081,8 @@ var catalogues = {
|
|
|
6081
6081
|
"Closed {name} — {next} is in front.": "{name}을(를) 닫음 — {next}이(가) 앞에 있습니다.",
|
|
6082
6082
|
"Closed the scenario — File ▸ New or Open to continue.": "시나리오를 닫음 — 파일 ▸ 새로 만들기 또는 열기로 계속하세요.",
|
|
6083
6083
|
"A copy of {name} to edit — Save asks where to keep it.": "편집할 {name}의 사본 — 저장할 때 위치를 묻습니다.",
|
|
6084
|
+
"Reloaded {name}": "{name}{name|을} 다시 불러왔습니다",
|
|
6085
|
+
"{name} did not build": "{name}{name|을} 빌드하지 못했습니다",
|
|
6084
6086
|
"The link uses the {name} plugin, which is turned off.": "이 링크는 꺼져 있는 {name} 플러그인을 사용합니다.",
|
|
6085
6087
|
"Turn It On": "켜기",
|
|
6086
6088
|
"Preparing workspace": "작업 공간 준비 중",
|
|
@@ -6597,6 +6599,79 @@ var CliError = class extends Error {
|
|
|
6597
6599
|
this.exitCode = exitCode;
|
|
6598
6600
|
}
|
|
6599
6601
|
};
|
|
6602
|
+
var BOT_GUIDE = "https://github.com/scm-js/scmbot";
|
|
6603
|
+
/** Docker's version, or null when there is no Docker to run (not installed, or its engine not started). */
|
|
6604
|
+
function dockerVersion() {
|
|
6605
|
+
const r = spawnSync("docker", [
|
|
6606
|
+
"version",
|
|
6607
|
+
"--format",
|
|
6608
|
+
"{{.Server.Version}}"
|
|
6609
|
+
], {
|
|
6610
|
+
encoding: "utf8",
|
|
6611
|
+
timeout: 1e4
|
|
6612
|
+
});
|
|
6613
|
+
return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : null;
|
|
6614
|
+
}
|
|
6615
|
+
/**
|
|
6616
|
+
* `docker run`'s arguments: the folder mounted at /bot, its name given for `new` (the toolchain sees only /bot), this
|
|
6617
|
+
* command's name for the toolchain's messages, and on Linux and macOS the files written as the person running this
|
|
6618
|
+
* rather than as root.
|
|
6619
|
+
*/
|
|
6620
|
+
function dockerArgs(o) {
|
|
6621
|
+
return [
|
|
6622
|
+
"run",
|
|
6623
|
+
"--rm",
|
|
6624
|
+
...o.tty ? ["-it"] : ["-i"],
|
|
6625
|
+
"-v",
|
|
6626
|
+
`${o.folder}:/bot`,
|
|
6627
|
+
"-e",
|
|
6628
|
+
`SCMBOT_FOLDER=${basename(o.folder)}`,
|
|
6629
|
+
"-e",
|
|
6630
|
+
"SCMBOT_COMMAND=scmjs bot",
|
|
6631
|
+
...o.user ? ["-u", o.user] : [],
|
|
6632
|
+
o.image,
|
|
6633
|
+
...o.args
|
|
6634
|
+
];
|
|
6635
|
+
}
|
|
6636
|
+
var hasImage = (image) => spawnSync("docker", [
|
|
6637
|
+
"image",
|
|
6638
|
+
"inspect",
|
|
6639
|
+
image
|
|
6640
|
+
], { stdio: "ignore" }).status === 0;
|
|
6641
|
+
function needDocker() {
|
|
6642
|
+
if (dockerVersion()) return;
|
|
6643
|
+
const installed = spawnSync("docker", ["--version"], { stdio: "ignore" }).status === 0;
|
|
6644
|
+
throw new CliError(installed ? "Docker is installed but not running" : "scmjs bot needs Docker", installed ? "Start Docker (Docker Desktop on Windows and macOS), then run this again." : "Install it from https://docs.docker.com/get-docker/, then run this again.");
|
|
6645
|
+
}
|
|
6646
|
+
/** Runs the toolchain with `args` in this folder; the toolchain's own output and exit code are the command's. */
|
|
6647
|
+
async function runBot(args, image = process.env.SCMBOT_IMAGE || "ghcr.io/scm-js/scmbot") {
|
|
6648
|
+
needDocker();
|
|
6649
|
+
if (!hasImage(image)) print(import_picocolors.default.dim(`Downloading the bot toolchain (${image}), once. It is a few GB.`));
|
|
6650
|
+
const user = process.platform === "win32" || !process.getuid ? null : `${process.getuid()}:${process.getgid?.()}`;
|
|
6651
|
+
const child = spawn("docker", dockerArgs({
|
|
6652
|
+
folder: process.cwd(),
|
|
6653
|
+
args,
|
|
6654
|
+
image,
|
|
6655
|
+
tty: !!process.stdout.isTTY && !!process.stdin.isTTY,
|
|
6656
|
+
user
|
|
6657
|
+
}), { stdio: "inherit" });
|
|
6658
|
+
const code = await new Promise((resolve) => {
|
|
6659
|
+
child.on("error", () => resolve(1));
|
|
6660
|
+
child.on("close", (c) => resolve(c ?? 1));
|
|
6661
|
+
});
|
|
6662
|
+
if (code) process.exitCode = code;
|
|
6663
|
+
}
|
|
6664
|
+
/** `scmjs bot update`: the newest toolchain, the one the play page expects. */
|
|
6665
|
+
async function updateBot(image = process.env.SCMBOT_IMAGE || "ghcr.io/scm-js/scmbot") {
|
|
6666
|
+
needDocker();
|
|
6667
|
+
print(import_picocolors.default.dim(`Fetching ${image}…`));
|
|
6668
|
+
const child = spawn("docker", ["pull", image], { stdio: "inherit" });
|
|
6669
|
+
if (await new Promise((resolve) => {
|
|
6670
|
+
child.on("error", () => resolve(1));
|
|
6671
|
+
child.on("close", (c) => resolve(c ?? 1));
|
|
6672
|
+
})) throw new CliError("The toolchain could not be fetched", `docker pull ${image} said why, above.`);
|
|
6673
|
+
print(`${mark.ok} The bot toolchain is up to date`);
|
|
6674
|
+
}
|
|
6600
6675
|
//#endregion
|
|
6601
6676
|
//#region cli/src/open.ts
|
|
6602
6677
|
/**
|
|
@@ -6781,18 +6856,50 @@ document.getElementById("go").onclick = go;
|
|
|
6781
6856
|
}
|
|
6782
6857
|
//#endregion
|
|
6783
6858
|
//#region cli/src/about.ts
|
|
6784
|
-
/** `scmjs about`: this build, the machine it runs on, the desktop app, and where everything is. */
|
|
6785
|
-
var VERSION = "0.8.
|
|
6786
|
-
var COMMIT = "
|
|
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.10";
|
|
6861
|
+
var COMMIT = "6cf22f8";
|
|
6787
6862
|
var BUILT = "2026-11-06";
|
|
6788
|
-
|
|
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();
|
|
6789
6893
|
return {
|
|
6790
6894
|
version: VERSION,
|
|
6895
|
+
latest,
|
|
6896
|
+
outdated: latest !== null && olderThan("0.8.10", latest),
|
|
6791
6897
|
commit: COMMIT,
|
|
6792
6898
|
built: BUILT,
|
|
6793
6899
|
node: process.version,
|
|
6794
6900
|
platform: `${process.platform} ${process.arch}`,
|
|
6795
6901
|
desktopApp: findDesktopApp(),
|
|
6902
|
+
docker: dockerVersion(),
|
|
6796
6903
|
pluginApi: 1,
|
|
6797
6904
|
reads: {
|
|
6798
6905
|
maps: [
|
|
@@ -6800,21 +6907,25 @@ function aboutReport() {
|
|
|
6800
6907
|
".scx",
|
|
6801
6908
|
".chk"
|
|
6802
6909
|
],
|
|
6803
|
-
replays: [".rep"]
|
|
6910
|
+
replays: [".rep"],
|
|
6911
|
+
bots: [".scmbot"]
|
|
6804
6912
|
},
|
|
6805
6913
|
links: {
|
|
6806
6914
|
editor: EDITOR_URL,
|
|
6915
|
+
play: "https://scmjs.dev/play/",
|
|
6807
6916
|
docs: "https://docs.scmjs.dev",
|
|
6917
|
+
bots: BOT_GUIDE,
|
|
6808
6918
|
source: "https://github.com/scm-js/scm-js",
|
|
6809
6919
|
issues: "https://github.com/scm-js/scm-js/issues"
|
|
6810
6920
|
},
|
|
6921
|
+
author: "Jeany",
|
|
6811
6922
|
license: "MIT"
|
|
6812
6923
|
};
|
|
6813
6924
|
}
|
|
6814
6925
|
function printAbout(r) {
|
|
6815
6926
|
return [
|
|
6816
6927
|
`${import_picocolors.default.bold(import_picocolors.default.cyan("scmjs"))} ${import_picocolors.default.bold(r.version)}`,
|
|
6817
|
-
import_picocolors.default.dim("StarCraft: Brood War maps and
|
|
6928
|
+
import_picocolors.default.dim("StarCraft: Brood War maps, replays and bots from the command line, by scmJS"),
|
|
6818
6929
|
"",
|
|
6819
6930
|
...rows([
|
|
6820
6931
|
["Version", [
|
|
@@ -6822,15 +6933,21 @@ function printAbout(r) {
|
|
|
6822
6933
|
r.commit && `commit ${r.commit}`,
|
|
6823
6934
|
r.built && `built ${r.built}`
|
|
6824
6935
|
].filter(Boolean).join(dot)],
|
|
6936
|
+
["Newest", newestLine(r.version, r.latest)],
|
|
6825
6937
|
["Node", `${r.node}${dot}${r.platform}`],
|
|
6826
6938
|
["Desktop app", r.desktopApp ?? import_picocolors.default.dim("not found (scmjs … open uses the browser)")],
|
|
6939
|
+
["Docker", r.docker ? `${r.docker}` : import_picocolors.default.dim("not found (scmjs bot needs it)")],
|
|
6827
6940
|
["Plugin API", String(r.pluginApi)],
|
|
6828
|
-
["Reads", `maps ${r.reads.maps.join(" ")}${dot}replays ${r.reads.replays.join(" ")}`],
|
|
6941
|
+
["Reads", `maps ${r.reads.maps.join(" ")}${dot}replays ${r.reads.replays.join(" ")}${dot}bots ${r.reads.bots.join(" ")}`],
|
|
6829
6942
|
null,
|
|
6830
6943
|
["Editor", import_picocolors.default.cyan(r.links.editor)],
|
|
6944
|
+
["Play", import_picocolors.default.cyan(r.links.play)],
|
|
6831
6945
|
["Docs", import_picocolors.default.cyan(r.links.docs)],
|
|
6946
|
+
["Bots", import_picocolors.default.cyan(r.links.bots)],
|
|
6832
6947
|
["Source", import_picocolors.default.cyan(r.links.source)],
|
|
6833
6948
|
["Issues", import_picocolors.default.cyan(r.links.issues)],
|
|
6949
|
+
null,
|
|
6950
|
+
["Made by", import_picocolors.default.bold(r.author)],
|
|
6834
6951
|
["Licence", r.license]
|
|
6835
6952
|
])
|
|
6836
6953
|
];
|
|
@@ -15225,6 +15342,690 @@ function wrap(text, width, most = 4) {
|
|
|
15225
15342
|
return lines.slice(0, most).join("\n") + (lines.length > most ? " …" : "");
|
|
15226
15343
|
}
|
|
15227
15344
|
//#endregion
|
|
15345
|
+
//#region src/plugins/loader.ts
|
|
15346
|
+
var PluginLoadError = class extends Error {
|
|
15347
|
+
constructor(message) {
|
|
15348
|
+
super(message);
|
|
15349
|
+
this.name = "PluginLoadError";
|
|
15350
|
+
}
|
|
15351
|
+
};
|
|
15352
|
+
var ENTRY_EXT = /\.(?:ts|tsx|mts|js|mjs)$/i;
|
|
15353
|
+
function dirOf(url) {
|
|
15354
|
+
return url.slice(0, url.lastIndexOf("/") + 1);
|
|
15355
|
+
}
|
|
15356
|
+
/**
|
|
15357
|
+
* `builtin:name`; `github:owner/repo[@ref][/dir]`; a github.com repository URL (with
|
|
15358
|
+
* an optional `/tree/ref/dir`); or any URL — a `.json` is the manifest, a script file
|
|
15359
|
+
* is the entry, anything else is a directory holding `plugin.json`.
|
|
15360
|
+
*/
|
|
15361
|
+
function parseSpec(spec) {
|
|
15362
|
+
const s = spec.trim();
|
|
15363
|
+
if (s === "") throw new PluginLoadError(t("Enter a plugin location."));
|
|
15364
|
+
const builtin = /^builtin:([\w-]+)$/i.exec(s);
|
|
15365
|
+
if (builtin) return {
|
|
15366
|
+
kind: "builtin",
|
|
15367
|
+
name: builtin[1]
|
|
15368
|
+
};
|
|
15369
|
+
const gh = /^github:([^/@\s]+)\/([^/@\s]+)(?:@([^/\s]+))?(?:\/(.*))?$/i.exec(s);
|
|
15370
|
+
if (gh) return githubSource(gh[1], gh[2], gh[3], gh[4]);
|
|
15371
|
+
const ghUrl = /^https?:\/\/(?:www\.)?github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?(?:\/tree\/([^/\s]+)(?:\/(.*?))?)?\/?$/i.exec(s);
|
|
15372
|
+
if (ghUrl) return githubSource(ghUrl[1], ghUrl[2], ghUrl[3], ghUrl[4]);
|
|
15373
|
+
if (/^https?:\/\//i.test(s)) {
|
|
15374
|
+
let url;
|
|
15375
|
+
try {
|
|
15376
|
+
url = new URL(s);
|
|
15377
|
+
} catch {
|
|
15378
|
+
throw new PluginLoadError(t("Not a valid URL: {url}", { url: s }));
|
|
15379
|
+
}
|
|
15380
|
+
url.hash = "";
|
|
15381
|
+
const path = url.pathname;
|
|
15382
|
+
if (/\.json$/i.test(path)) return {
|
|
15383
|
+
kind: "remote",
|
|
15384
|
+
manifestUrl: url.href,
|
|
15385
|
+
entryUrl: null,
|
|
15386
|
+
base: dirOf(url.href),
|
|
15387
|
+
display: s
|
|
15388
|
+
};
|
|
15389
|
+
if (ENTRY_EXT.test(path)) return {
|
|
15390
|
+
kind: "remote",
|
|
15391
|
+
manifestUrl: null,
|
|
15392
|
+
entryUrl: url.href,
|
|
15393
|
+
base: dirOf(url.href),
|
|
15394
|
+
display: s
|
|
15395
|
+
};
|
|
15396
|
+
const base = url.href.endsWith("/") ? url.href : `${url.href}/`;
|
|
15397
|
+
return {
|
|
15398
|
+
kind: "remote",
|
|
15399
|
+
manifestUrl: `${base}plugin.json`,
|
|
15400
|
+
entryUrl: null,
|
|
15401
|
+
base,
|
|
15402
|
+
display: s
|
|
15403
|
+
};
|
|
15404
|
+
}
|
|
15405
|
+
throw new PluginLoadError(t("Unrecognised plugin location \"{location}\". Paste a GitHub repository link such as https://github.com/owner/repo, the short form github:owner/repo, or a URL to a plugin.json or plugin.ts.", { location: s }));
|
|
15406
|
+
}
|
|
15407
|
+
function githubSource(owner, repo, ref, dir) {
|
|
15408
|
+
const sub = dir ? dir.replace(/^\/+|\/+$/g, "") : "";
|
|
15409
|
+
const base = `https://raw.githubusercontent.com/${owner}/${repo}/${ref ?? "HEAD"}/${sub ? `${sub}/` : ""}`;
|
|
15410
|
+
const web = `https://github.com/${owner}/${repo}${ref || sub ? `/tree/${ref ?? "HEAD"}${sub ? `/${sub}` : ""}` : ""}`;
|
|
15411
|
+
return {
|
|
15412
|
+
kind: "remote",
|
|
15413
|
+
manifestUrl: `${base}plugin.json`,
|
|
15414
|
+
entryUrl: null,
|
|
15415
|
+
base,
|
|
15416
|
+
display: `github:${owner}/${repo}${ref ? `@${ref}` : ""}${sub ? `/${sub}` : ""}`,
|
|
15417
|
+
webUrl: web,
|
|
15418
|
+
github: {
|
|
15419
|
+
owner,
|
|
15420
|
+
repo,
|
|
15421
|
+
ref: ref ?? null,
|
|
15422
|
+
dir: sub
|
|
15423
|
+
}
|
|
15424
|
+
};
|
|
15425
|
+
}
|
|
15426
|
+
function validateManifest(raw, where) {
|
|
15427
|
+
if (!raw || typeof raw !== "object") throw new PluginLoadError(t("{where} is not a JSON object.", { where }));
|
|
15428
|
+
const m = raw;
|
|
15429
|
+
if (typeof m.name !== "string" || m.name.trim() === "") throw new PluginLoadError(t("{where} needs a \"name\".", { where }));
|
|
15430
|
+
const str = (k) => typeof m[k] === "string" ? m[k] : void 0;
|
|
15431
|
+
const out = { name: m.name.trim() };
|
|
15432
|
+
for (const k of [
|
|
15433
|
+
"id",
|
|
15434
|
+
"version",
|
|
15435
|
+
"description",
|
|
15436
|
+
"author",
|
|
15437
|
+
"homepage",
|
|
15438
|
+
"entry",
|
|
15439
|
+
"build",
|
|
15440
|
+
"icon"
|
|
15441
|
+
]) {
|
|
15442
|
+
const v = str(k);
|
|
15443
|
+
if (v !== void 0) out[k] = v;
|
|
15444
|
+
}
|
|
15445
|
+
if (typeof m.api === "number") out.api = m.api;
|
|
15446
|
+
for (const k of ["entry", "build"]) if (out[k] && /^(?:[a-z]+:)?\/\//i.test(out[k])) throw new PluginLoadError(t("{where}: \"{field}\" must be a path relative to the manifest.", {
|
|
15447
|
+
where,
|
|
15448
|
+
field: k
|
|
15449
|
+
}));
|
|
15450
|
+
if (Array.isArray(m.requires)) {
|
|
15451
|
+
const requires = [];
|
|
15452
|
+
for (const r of m.requires) {
|
|
15453
|
+
if (typeof r !== "string" || r.trim() === "") continue;
|
|
15454
|
+
let spec;
|
|
15455
|
+
try {
|
|
15456
|
+
spec = canonicalSpec(parseSpec(r));
|
|
15457
|
+
} catch (err) {
|
|
15458
|
+
throw new PluginLoadError(t("{where}: \"requires\" names a plugin location the editor cannot use, \"{location}\": {error}", {
|
|
15459
|
+
where,
|
|
15460
|
+
location: r,
|
|
15461
|
+
error: err instanceof Error ? err.message : String(err)
|
|
15462
|
+
}));
|
|
15463
|
+
}
|
|
15464
|
+
if (!requires.includes(spec)) requires.push(spec);
|
|
15465
|
+
}
|
|
15466
|
+
if (requires.length > 0) out.requires = requires;
|
|
15467
|
+
}
|
|
15468
|
+
return out;
|
|
15469
|
+
}
|
|
15470
|
+
var IMAGE_EXT = /\.(?:png|svg|jpe?g|gif|webp|avif|ico)$/i;
|
|
15471
|
+
var HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
15472
|
+
/**
|
|
15473
|
+
* A manifest `icon` string as the chrome should show it. Four forms are understood —
|
|
15474
|
+
* a short glyph (an emoji), a `data:image/…` URI, an `http(s)` image, and a path
|
|
15475
|
+
* relative to the manifest — and nothing else is: any other scheme (`javascript:`
|
|
15476
|
+
* above all) and any long non-path string resolve to null and the plugin simply shows
|
|
15477
|
+
* the default icon. `base` is where relative paths resolve from; a built-in has none
|
|
15478
|
+
* (its file comes through `BuiltinPlugin.iconUrl` instead).
|
|
15479
|
+
*/
|
|
15480
|
+
function resolveIcon(icon, base) {
|
|
15481
|
+
const s = icon?.trim();
|
|
15482
|
+
if (!s) return null;
|
|
15483
|
+
if (/^data:image\//i.test(s) || /^https?:\/\//i.test(s)) return {
|
|
15484
|
+
kind: "image",
|
|
15485
|
+
url: s
|
|
15486
|
+
};
|
|
15487
|
+
if (HAS_SCHEME.test(s)) return null;
|
|
15488
|
+
if (s.includes("/") || IMAGE_EXT.test(s)) {
|
|
15489
|
+
if (!base) return null;
|
|
15490
|
+
try {
|
|
15491
|
+
return {
|
|
15492
|
+
kind: "image",
|
|
15493
|
+
url: new URL(s, base).href
|
|
15494
|
+
};
|
|
15495
|
+
} catch {
|
|
15496
|
+
return null;
|
|
15497
|
+
}
|
|
15498
|
+
}
|
|
15499
|
+
return [...s].length <= 4 ? {
|
|
15500
|
+
kind: "text",
|
|
15501
|
+
text: s
|
|
15502
|
+
} : null;
|
|
15503
|
+
}
|
|
15504
|
+
/** The spec as it is stored and shown: the short form for a repository, `builtin:name` for a built-in. */
|
|
15505
|
+
function canonicalSpec(source) {
|
|
15506
|
+
return source.kind === "builtin" ? `builtin:${source.name}` : source.display;
|
|
15507
|
+
}
|
|
15508
|
+
//#endregion
|
|
15509
|
+
//#region cli/src/plugin.ts
|
|
15510
|
+
/**
|
|
15511
|
+
* `scmjs plugin new | check | dev`: writing a plugin for the editor (docs/plugins.md, .claude/internals/cli.md).
|
|
15512
|
+
*
|
|
15513
|
+
* - `new` writes a plugin to start from, set up as the project's own are (Hello World's layout: manifest, source,
|
|
15514
|
+
* Korean catalogue and its test, typings, build, the shared CI).
|
|
15515
|
+
* - `check` reads the plugin as the editor would load it (`validateManifest`, the file it would import, the imports
|
|
15516
|
+
* it would refuse) and says what would go wrong, for a person or a CI step.
|
|
15517
|
+
* - `dev` serves the folder on the loopback address, builds it when it names a build, and opens the editor with
|
|
15518
|
+
* `?plugin-dev=` (src/hooks/usePluginDev.ts), which installs it the usual way the first time and reloads it on
|
|
15519
|
+
* every change the page is told of here.
|
|
15520
|
+
*/
|
|
15521
|
+
/** `Base Check` → `base-check`. */
|
|
15522
|
+
var pluginId = (name) => name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "my-plugin";
|
|
15523
|
+
/** `base-check` or `plugin-base-check` → `Base Check`. */
|
|
15524
|
+
var nameFromFolder = (folder) => basename(folder).replace(/^(?:scmjs-)?plugin-/, "").split(/[-_\s]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ") || "My Plugin";
|
|
15525
|
+
function gitUser() {
|
|
15526
|
+
const r = spawnSync("git", ["config", "user.name"], { encoding: "utf8" });
|
|
15527
|
+
return r.status === 0 ? r.stdout.trim() : "";
|
|
15528
|
+
}
|
|
15529
|
+
/** The files of a new plugin, by path. */
|
|
15530
|
+
function pluginFiles(o) {
|
|
15531
|
+
const json = (v) => JSON.stringify(v, null, 2) + "\n";
|
|
15532
|
+
const lit = JSON.stringify(o.name);
|
|
15533
|
+
return {
|
|
15534
|
+
"plugin.json": json({
|
|
15535
|
+
name: o.name,
|
|
15536
|
+
id: o.id,
|
|
15537
|
+
version: "0.1.0",
|
|
15538
|
+
description: `${o.name}: a plugin for the scmJS map editor.`,
|
|
15539
|
+
...o.author ? { author: o.author } : {},
|
|
15540
|
+
entry: "plugin.ts",
|
|
15541
|
+
build: "dist/plugin.js",
|
|
15542
|
+
icon: "🧩",
|
|
15543
|
+
api: 1
|
|
15544
|
+
}),
|
|
15545
|
+
"plugin.ts": `/**
|
|
15546
|
+
* ${o.name}: a plugin for the scmJS map editor.
|
|
15547
|
+
*
|
|
15548
|
+
* It adds Tools ▸ ${o.name}…, which opens a pane about the map that is open. Change it into your own; the API is
|
|
15549
|
+
* documented at https://docs.scmjs.dev/plugins/.
|
|
15550
|
+
*
|
|
15551
|
+
* npm start the editor with this plugin, reloaded each time you save (scmjs plugin dev)
|
|
15552
|
+
* npm test the tests, including the one that keeps ko.ts complete
|
|
15553
|
+
* npm run check what the editor would make of this folder (scmjs plugin check)
|
|
15554
|
+
*/
|
|
15555
|
+
import type { PluginApi } from "@scm-js/plugin-api";
|
|
15556
|
+
import { KO } from "./ko";
|
|
15557
|
+
|
|
15558
|
+
export default function activate(api: PluginApi) {
|
|
15559
|
+
// The Korean for what the plugin shows (ko.ts). English is the key: pass every string to t() as a literal.
|
|
15560
|
+
api.i18n.register({ ko: KO });
|
|
15561
|
+
const t = api.i18n.t;
|
|
15562
|
+
|
|
15563
|
+
api.menu.add("Tools", {
|
|
15564
|
+
label: ${JSON.stringify(`${o.name}…`)},
|
|
15565
|
+
enabled: () => api.document.isOpen(),
|
|
15566
|
+
run: () => {
|
|
15567
|
+
const info = api.document.info();
|
|
15568
|
+
const units = api.document.scenario()?.units.length ?? 0;
|
|
15569
|
+
api.ui.dialog({
|
|
15570
|
+
title: ${lit},
|
|
15571
|
+
size: "sm",
|
|
15572
|
+
mount(body) {
|
|
15573
|
+
// Plain DOM: the editor gives the plugin an empty element to fill.
|
|
15574
|
+
const name = document.createElement("p");
|
|
15575
|
+
name.textContent = info?.name ?? ""; // the map's own text, never translated
|
|
15576
|
+
const count = document.createElement("p");
|
|
15577
|
+
count.textContent = t("{n, plural, one {# unit} other {# units}} on the map", { n: units });
|
|
15578
|
+
body.append(name, count);
|
|
15579
|
+
},
|
|
15580
|
+
buttons: [{ label: t("Close") }],
|
|
15581
|
+
});
|
|
15582
|
+
},
|
|
15583
|
+
});
|
|
15584
|
+
}
|
|
15585
|
+
`,
|
|
15586
|
+
"ko.ts": `// Korean for every string the plugin shows; English is the key. tests/ko.test.ts keeps it complete.
|
|
15587
|
+
export const KO: Record<string, string> = {
|
|
15588
|
+
"Close": "닫기",
|
|
15589
|
+
"{n, plural, one {# unit} other {# units}} on the map": "맵의 유닛 {n, plural, other {#개}}",
|
|
15590
|
+
};
|
|
15591
|
+
`,
|
|
15592
|
+
"tests/ko.test.ts": `import { readFileSync } from "node:fs";
|
|
15593
|
+
import { describe, expect, it } from "vitest";
|
|
15594
|
+
import { KO } from "../ko";
|
|
15595
|
+
|
|
15596
|
+
// Every string the plugin shows through \`t("…")\`, or hands the editor as \`msg("…")\`, read from the source.
|
|
15597
|
+
// A plugin with more files lists them here.
|
|
15598
|
+
const files = ["plugin.ts"];
|
|
15599
|
+
const sources = files.map((f) => readFileSync(new URL(\`../\${f}\`, import.meta.url), "utf8"));
|
|
15600
|
+
const keys = new Set(sources.flatMap((source) => [...source.matchAll(/\\b(?:t|msg)\\("((?:[^"\\\\]|\\\\.)*)"/g)].map((m) => JSON.parse(\`"\${m[1]}"\`) as string)));
|
|
15601
|
+
|
|
15602
|
+
describe("the Korean catalogue", () => {
|
|
15603
|
+
it("has every string the plugin shows", () => {
|
|
15604
|
+
expect([...keys].filter((k) => !(k in KO))).toEqual([]);
|
|
15605
|
+
});
|
|
15606
|
+
it("has nothing the plugin no longer shows", () => {
|
|
15607
|
+
expect(Object.keys(KO).filter((k) => !keys.has(k))).toEqual([]);
|
|
15608
|
+
});
|
|
15609
|
+
});
|
|
15610
|
+
`,
|
|
15611
|
+
"package.json": json({
|
|
15612
|
+
name: `scmjs-plugin-${o.id}`,
|
|
15613
|
+
version: "0.1.0",
|
|
15614
|
+
private: true,
|
|
15615
|
+
description: `${o.name}: a plugin for the scmJS map editor.`,
|
|
15616
|
+
...o.author ? { author: o.author } : {},
|
|
15617
|
+
license: "MIT",
|
|
15618
|
+
type: "module",
|
|
15619
|
+
scripts: {
|
|
15620
|
+
start: "scmjs plugin dev",
|
|
15621
|
+
check: "scmjs plugin check",
|
|
15622
|
+
build: "esbuild plugin.ts --bundle --format=esm --target=es2022 --platform=browser --outfile=dist/plugin.js --banner:js=\"// Generated by npm run build - do not edit; the source is plugin.ts.\"",
|
|
15623
|
+
typecheck: "tsc -p tsconfig.json",
|
|
15624
|
+
test: "vitest run"
|
|
15625
|
+
},
|
|
15626
|
+
devDependencies: {
|
|
15627
|
+
"@scm-js/cli": `^${VERSION}`,
|
|
15628
|
+
"@scm-js/plugin-api": `^1`,
|
|
15629
|
+
esbuild: "^0.28.2",
|
|
15630
|
+
typescript: "~6.0.2",
|
|
15631
|
+
vitest: "^5.1.0"
|
|
15632
|
+
}
|
|
15633
|
+
}),
|
|
15634
|
+
"tsconfig.json": json({
|
|
15635
|
+
compilerOptions: {
|
|
15636
|
+
target: "ES2022",
|
|
15637
|
+
lib: [
|
|
15638
|
+
"ES2022",
|
|
15639
|
+
"DOM",
|
|
15640
|
+
"DOM.Iterable"
|
|
15641
|
+
],
|
|
15642
|
+
module: "ESNext",
|
|
15643
|
+
moduleResolution: "bundler",
|
|
15644
|
+
types: [],
|
|
15645
|
+
strict: true,
|
|
15646
|
+
noUnusedLocals: true,
|
|
15647
|
+
noUnusedParameters: true,
|
|
15648
|
+
verbatimModuleSyntax: true,
|
|
15649
|
+
erasableSyntaxOnly: true,
|
|
15650
|
+
isolatedModules: true,
|
|
15651
|
+
skipLibCheck: true,
|
|
15652
|
+
noEmit: true
|
|
15653
|
+
},
|
|
15654
|
+
include: ["plugin.ts", "ko.ts"]
|
|
15655
|
+
}),
|
|
15656
|
+
".gitignore": "node_modules/\n*.tsbuildinfo\n",
|
|
15657
|
+
".github/workflows/ci.yml": `# Type-check, test, and build dist/plugin.js, the bundle the editor loads (\`build\` in plugin.json), with the
|
|
15658
|
+
# workflow every scm-js plugin uses: on a push to main it commits the rebuilt bundle, on a v* tag it checks the
|
|
15659
|
+
# committed one matches, and weekly it type-checks against the newest @scm-js/plugin-api.
|
|
15660
|
+
name: CI
|
|
15661
|
+
|
|
15662
|
+
on:
|
|
15663
|
+
push:
|
|
15664
|
+
branches: [main]
|
|
15665
|
+
tags: ["v*"]
|
|
15666
|
+
pull_request:
|
|
15667
|
+
schedule:
|
|
15668
|
+
- cron: "0 6 * * 1"
|
|
15669
|
+
workflow_dispatch:
|
|
15670
|
+
|
|
15671
|
+
permissions:
|
|
15672
|
+
contents: write
|
|
15673
|
+
|
|
15674
|
+
jobs:
|
|
15675
|
+
ci:
|
|
15676
|
+
uses: scm-js/.github/.github/workflows/plugin-ci.yml@main
|
|
15677
|
+
`,
|
|
15678
|
+
"README.md": `# ${o.name}
|
|
15679
|
+
|
|
15680
|
+
A plugin for [scmJS](https://scmjs.dev/editor/), the StarCraft: Brood War map editor.
|
|
15681
|
+
|
|
15682
|
+
## Developing
|
|
15683
|
+
|
|
15684
|
+
\`\`\`sh
|
|
15685
|
+
npm install
|
|
15686
|
+
npm start # opens the editor with the plugin, and reloads it each time you save
|
|
15687
|
+
\`\`\`
|
|
15688
|
+
|
|
15689
|
+
\`npm test\` runs the tests, \`npm run typecheck\` the type-check, and \`npm run check\` says what the editor would
|
|
15690
|
+
make of the folder. The plugin guide is at [docs.scmjs.dev/plugins](https://docs.scmjs.dev/plugins/).
|
|
15691
|
+
|
|
15692
|
+
## Installing
|
|
15693
|
+
|
|
15694
|
+
Once this is a public GitHub repository, paste \`github:<you>/<repository>\` into Plugins ▸ Manage Plugins….
|
|
15695
|
+
Tag releases (\`v1.0.0\`): they are what the editor's update check offers.
|
|
15696
|
+
`,
|
|
15697
|
+
LICENSE: `MIT License
|
|
15698
|
+
|
|
15699
|
+
Copyright (c) ${o.year} ${o.author || "the authors"}
|
|
15700
|
+
|
|
15701
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15702
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15703
|
+
in the Software without restriction, including without limitation the rights
|
|
15704
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15705
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15706
|
+
furnished to do so, subject to the following conditions:
|
|
15707
|
+
|
|
15708
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15709
|
+
copies or substantial portions of the Software.
|
|
15710
|
+
|
|
15711
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15712
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15713
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
15714
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
15715
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
15716
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
15717
|
+
SOFTWARE.
|
|
15718
|
+
`
|
|
15719
|
+
};
|
|
15720
|
+
}
|
|
15721
|
+
/** `scmjs plugin new [name]`: a folder `plugin-<id>` (or `--dir`) with a plugin to start from. */
|
|
15722
|
+
function newPlugin(o) {
|
|
15723
|
+
const name = (o.name?.trim() || (o.dir ? nameFromFolder(resolve(o.dir)) : "")).slice(0, 64);
|
|
15724
|
+
if (!name) throw new CliError("Name the plugin", "scmjs plugin new \"Base Check\" (or --dir <folder>)", 2);
|
|
15725
|
+
const id = pluginId(name);
|
|
15726
|
+
const dir = resolve(o.dir ?? `plugin-${id}`);
|
|
15727
|
+
if (existsSync(dir) && readdirSync(dir).filter((f) => f !== ".git").length > 0) throw new CliError(`${relative(process.cwd(), dir) || "."} is not empty`, "Give an empty or new folder with --dir.", 2);
|
|
15728
|
+
const files = pluginFiles({
|
|
15729
|
+
name,
|
|
15730
|
+
id,
|
|
15731
|
+
author: gitUser(),
|
|
15732
|
+
year: (/* @__PURE__ */ new Date()).getFullYear()
|
|
15733
|
+
});
|
|
15734
|
+
for (const [path, text] of Object.entries(files)) {
|
|
15735
|
+
mkdirSync(dirname(join(dir, path)), { recursive: true });
|
|
15736
|
+
writeFileSync(join(dir, path), text);
|
|
15737
|
+
}
|
|
15738
|
+
return {
|
|
15739
|
+
dir,
|
|
15740
|
+
files: Object.keys(files)
|
|
15741
|
+
};
|
|
15742
|
+
}
|
|
15743
|
+
var SOURCE_EXT = [
|
|
15744
|
+
".ts",
|
|
15745
|
+
".tsx",
|
|
15746
|
+
".mts",
|
|
15747
|
+
".js",
|
|
15748
|
+
".mjs"
|
|
15749
|
+
];
|
|
15750
|
+
/** The imports of a source file the editor would follow or refuse (type-only imports are erased first). */
|
|
15751
|
+
function sourceImports(code) {
|
|
15752
|
+
const text = code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'`])\/\/.*$/gm, "$1");
|
|
15753
|
+
const out = [];
|
|
15754
|
+
for (const m of text.matchAll(/\b(import|export)\s+(type\s+)?(?:[^'"`;]*?\sfrom\s*)?(['"])([^'"\n]+)\3|\bimport\s*\(\s*(['"])([^'"\n]+)\5\s*\)/g)) {
|
|
15755
|
+
if (m[2]) continue;
|
|
15756
|
+
out.push(m[4] ?? m[6]);
|
|
15757
|
+
}
|
|
15758
|
+
return out;
|
|
15759
|
+
}
|
|
15760
|
+
/** The file a relative import names, as the editor's loader looks for it (`./x`, `./x.js` meaning `x.ts`, `./dir`). */
|
|
15761
|
+
function importedFile(from, spec) {
|
|
15762
|
+
const base = resolve(dirname(from), spec);
|
|
15763
|
+
return [
|
|
15764
|
+
base,
|
|
15765
|
+
...SOURCE_EXT.map((e) => base + e),
|
|
15766
|
+
base.replace(/\.js$/, ".ts"),
|
|
15767
|
+
...SOURCE_EXT.map((e) => join(base, "index" + e))
|
|
15768
|
+
].find((f) => existsSync(f) && statSync(f).isFile()) ?? null;
|
|
15769
|
+
}
|
|
15770
|
+
var newest = (dir, skip) => {
|
|
15771
|
+
let t = 0;
|
|
15772
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
15773
|
+
if (e.name.startsWith(".") || e.name === "node_modules" || skip.has(join(dir, e.name))) continue;
|
|
15774
|
+
const p = join(dir, e.name);
|
|
15775
|
+
if (e.isDirectory()) t = Math.max(t, newest(p, skip));
|
|
15776
|
+
else if (SOURCE_EXT.includes(extname(e.name)) && !e.name.endsWith(".d.ts") && !p.includes(`${sep}tests${sep}`)) t = Math.max(t, statSync(p).mtimeMs);
|
|
15777
|
+
}
|
|
15778
|
+
return t;
|
|
15779
|
+
};
|
|
15780
|
+
/** `scmjs plugin check`: the plugin in `dir` as the editor would load it. */
|
|
15781
|
+
function checkPlugin(dirArg = ".") {
|
|
15782
|
+
const dir = resolve(dirArg);
|
|
15783
|
+
const out = {
|
|
15784
|
+
dir,
|
|
15785
|
+
name: null,
|
|
15786
|
+
id: null,
|
|
15787
|
+
version: null,
|
|
15788
|
+
loads: null,
|
|
15789
|
+
issues: []
|
|
15790
|
+
};
|
|
15791
|
+
const add = (level, message, hint) => out.issues.push({
|
|
15792
|
+
level,
|
|
15793
|
+
message,
|
|
15794
|
+
...hint ? { hint } : {}
|
|
15795
|
+
});
|
|
15796
|
+
const file = join(dir, "plugin.json");
|
|
15797
|
+
if (!existsSync(file)) throw new CliError(`No plugin.json in ${relative(process.cwd(), dir) || "this folder"}`, "Run this in the plugin's folder, or start one with scmjs plugin new.", 2);
|
|
15798
|
+
let raw;
|
|
15799
|
+
try {
|
|
15800
|
+
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
15801
|
+
} catch (e) {
|
|
15802
|
+
add("error", `plugin.json is not valid JSON: ${e.message}`);
|
|
15803
|
+
return out;
|
|
15804
|
+
}
|
|
15805
|
+
let m;
|
|
15806
|
+
try {
|
|
15807
|
+
m = validateManifest(raw, "plugin.json");
|
|
15808
|
+
} catch (e) {
|
|
15809
|
+
add("error", e.message);
|
|
15810
|
+
return out;
|
|
15811
|
+
}
|
|
15812
|
+
out.name = m.name;
|
|
15813
|
+
out.id = m.id ?? pluginId(m.name);
|
|
15814
|
+
out.version = m.version ?? null;
|
|
15815
|
+
if (m.api === void 0) add("warn", "plugin.json names no \"api\"", `Add "api": 1, so an older editor says it is too old instead of failing.`);
|
|
15816
|
+
else if (m.api > 1) add("error", `It needs plugin API ${m.api}; the editor provides 1`);
|
|
15817
|
+
if (!m.id) add("info", `No "id": the editor uses "${out.id}", made from the name`, "An id keeps storage and commands the same if the name changes.");
|
|
15818
|
+
for (const k of [
|
|
15819
|
+
"version",
|
|
15820
|
+
"description",
|
|
15821
|
+
"author",
|
|
15822
|
+
"homepage"
|
|
15823
|
+
]) if (!m[k]) add("info", `No "${k}": shown when someone adds the plugin`);
|
|
15824
|
+
const icon = resolveIcon(m.icon, "file:///plugin/plugin.json");
|
|
15825
|
+
if (!m.icon) add("info", "No \"icon\": the default plugin mark is shown");
|
|
15826
|
+
else if (!icon) add("warn", `The icon "${m.icon}" is not one the editor shows`, "An image beside plugin.json, an https or data: image, or up to four characters.");
|
|
15827
|
+
else if (icon.kind === "image" && icon.url.startsWith("file:///plugin/") && !existsSync(join(dir, decodeURIComponent(icon.url.slice(15))))) add("error", `The icon ${m.icon} is not in the folder`);
|
|
15828
|
+
const entry = m.entry ?? ["plugin.ts", "plugin.js"].find((f) => existsSync(join(dir, f)));
|
|
15829
|
+
const entryFile = entry ? join(dir, entry) : null;
|
|
15830
|
+
if (!entryFile || !existsSync(entryFile)) add("error", entry ? `The entry ${entry} is not there` : "No \"entry\", and neither plugin.ts nor plugin.js is there");
|
|
15831
|
+
if (m.build) {
|
|
15832
|
+
const built = join(dir, m.build);
|
|
15833
|
+
out.loads = m.build;
|
|
15834
|
+
if (!existsSync(built)) add("error", `The build ${m.build} is not there, and the editor loads it instead of ${entry ?? "the source"}`, existsSync(join(dir, "package.json")) ? "Run npm run build (scmjs plugin dev builds it for you)." : "Build it, or take \"build\" out of plugin.json.");
|
|
15835
|
+
else if (newest(dir, /* @__PURE__ */ new Set([built])) > statSync(built).mtimeMs + 1e3) add("warn", `${m.build} is older than the source`, "Run npm run build, or the editor loads what it was.");
|
|
15836
|
+
} else if (entryFile && existsSync(entryFile)) {
|
|
15837
|
+
out.loads = entry;
|
|
15838
|
+
const seen = /* @__PURE__ */ new Set(), todo = [entryFile];
|
|
15839
|
+
while (todo.length) {
|
|
15840
|
+
const f = todo.pop();
|
|
15841
|
+
if (seen.has(f)) continue;
|
|
15842
|
+
seen.add(f);
|
|
15843
|
+
for (const spec of sourceImports(readFileSync(f, "utf8"))) if (/^(?:\.{1,2}\/|\/)/.test(spec)) {
|
|
15844
|
+
const to = importedFile(f, spec);
|
|
15845
|
+
if (to) todo.push(to);
|
|
15846
|
+
else add("error", `${relative(dir, f)} imports ${spec}, which is not there`);
|
|
15847
|
+
} else if (!/^https?:\/\//.test(spec)) add("error", `${relative(dir, f)} imports the package "${spec}"`, "The editor loads source without a module resolver: bundle the plugin and name the bundle as \"build\".");
|
|
15848
|
+
}
|
|
15849
|
+
}
|
|
15850
|
+
if (out.issues.some((i) => i.level === "error")) out.loads = null;
|
|
15851
|
+
return out;
|
|
15852
|
+
}
|
|
15853
|
+
function printPluginCheck(c, all) {
|
|
15854
|
+
const shown = c.issues.filter((i) => all || i.level !== "info");
|
|
15855
|
+
const errors = c.issues.filter((i) => i.level === "error").length, warns = c.issues.filter((i) => i.level === "warn").length;
|
|
15856
|
+
const head = `${import_picocolors.default.bold(c.name ?? basename(c.dir))}${c.version ? import_picocolors.default.dim(` ${c.version}`) : ""} ${errors ? `${mark.error} ${plural(errors, "error")}` : warns ? `${mark.warn} ${plural(warns, "warning")}` : `${mark.ok} loads`}${c.loads && !errors ? import_picocolors.default.dim(` (${c.loads})`) : ""}`;
|
|
15857
|
+
const order = {
|
|
15858
|
+
error: 0,
|
|
15859
|
+
warn: 1,
|
|
15860
|
+
info: 2
|
|
15861
|
+
};
|
|
15862
|
+
return [head, ...shown.sort((a, b) => order[a.level] - order[b.level]).flatMap((i) => [` ${i.level === "error" ? mark.error : i.level === "warn" ? mark.warn : mark.info} ${i.message}`, ...i.hint ? [import_picocolors.default.dim(` ${i.hint}`)] : []])];
|
|
15863
|
+
}
|
|
15864
|
+
var TYPES = {
|
|
15865
|
+
".json": "application/json",
|
|
15866
|
+
".js": "text/javascript",
|
|
15867
|
+
".mjs": "text/javascript",
|
|
15868
|
+
".ts": "text/plain",
|
|
15869
|
+
".tsx": "text/plain",
|
|
15870
|
+
".mts": "text/plain",
|
|
15871
|
+
".svg": "image/svg+xml",
|
|
15872
|
+
".png": "image/png",
|
|
15873
|
+
".jpg": "image/jpeg",
|
|
15874
|
+
".jpeg": "image/jpeg",
|
|
15875
|
+
".gif": "image/gif",
|
|
15876
|
+
".webp": "image/webp",
|
|
15877
|
+
".avif": "image/avif",
|
|
15878
|
+
".ico": "image/x-icon",
|
|
15879
|
+
".css": "text/css",
|
|
15880
|
+
".html": "text/html",
|
|
15881
|
+
".txt": "text/plain",
|
|
15882
|
+
".md": "text/plain"
|
|
15883
|
+
};
|
|
15884
|
+
var HEADERS = {
|
|
15885
|
+
"access-control-allow-origin": "*",
|
|
15886
|
+
"access-control-allow-private-network": "true",
|
|
15887
|
+
"cache-control": "no-store"
|
|
15888
|
+
};
|
|
15889
|
+
/** The editor's address that loads the plugin served at `served` (src/plugins/devLink.ts reads it). */
|
|
15890
|
+
var devEditorUrl = (editor, served) => {
|
|
15891
|
+
const u = new URL(editor);
|
|
15892
|
+
u.searchParams.set("plugin-dev", served);
|
|
15893
|
+
return u.href;
|
|
15894
|
+
};
|
|
15895
|
+
/** `scmjs plugin dev`: serve, build, open the editor, and tell it to reload on each change, until Ctrl+C. */
|
|
15896
|
+
async function devPlugin(o) {
|
|
15897
|
+
const dir = resolve(o.dir ?? ".");
|
|
15898
|
+
checkPlugin(dir);
|
|
15899
|
+
const manifest = JSON.parse(readFileSync(join(dir, "plugin.json"), "utf8"));
|
|
15900
|
+
const pkg = existsSync(join(dir, "package.json")) ? JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) : null;
|
|
15901
|
+
const builds = !!(manifest.build && pkg?.scripts?.build);
|
|
15902
|
+
if (builds && !existsSync(join(dir, "node_modules"))) throw new CliError("The plugin's packages are not installed", "Run npm install in its folder first.", 2);
|
|
15903
|
+
const clients = /* @__PURE__ */ new Set();
|
|
15904
|
+
const send = (event, data) => {
|
|
15905
|
+
for (const c of clients) c.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
15906
|
+
};
|
|
15907
|
+
let connected = false;
|
|
15908
|
+
const server = createServer((req, res) => {
|
|
15909
|
+
if (req.method === "OPTIONS") {
|
|
15910
|
+
res.writeHead(204, {
|
|
15911
|
+
...HEADERS,
|
|
15912
|
+
"access-control-allow-methods": "GET",
|
|
15913
|
+
"access-control-allow-headers": "*"
|
|
15914
|
+
}).end();
|
|
15915
|
+
return;
|
|
15916
|
+
}
|
|
15917
|
+
const path = decodeURIComponent(new URL(req.url ?? "/", "http://127.0.0.1").pathname);
|
|
15918
|
+
if (path === "/__scmjs/events") {
|
|
15919
|
+
res.writeHead(200, {
|
|
15920
|
+
...HEADERS,
|
|
15921
|
+
"content-type": "text/event-stream",
|
|
15922
|
+
connection: "keep-alive"
|
|
15923
|
+
});
|
|
15924
|
+
res.write(`event: hello\ndata: ${JSON.stringify({ name: manifest.name })}\n\n`);
|
|
15925
|
+
clients.add(res);
|
|
15926
|
+
if (!connected) {
|
|
15927
|
+
connected = true;
|
|
15928
|
+
print(`${mark.ok} The editor is connected`);
|
|
15929
|
+
}
|
|
15930
|
+
req.on("close", () => clients.delete(res));
|
|
15931
|
+
return;
|
|
15932
|
+
}
|
|
15933
|
+
const file = resolve(dir, "." + (path.endsWith("/") ? path + "index.html" : path));
|
|
15934
|
+
if (!(file === dir || file.startsWith(dir + sep)) || /(?:^|[\\/])(?:node_modules|\.git)(?:[\\/]|$)/.test(relative(dir, file)) || !existsSync(file) || !statSync(file).isFile()) {
|
|
15935
|
+
res.writeHead(404, HEADERS).end();
|
|
15936
|
+
return;
|
|
15937
|
+
}
|
|
15938
|
+
res.writeHead(200, {
|
|
15939
|
+
...HEADERS,
|
|
15940
|
+
"content-type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream"
|
|
15941
|
+
});
|
|
15942
|
+
res.end(readFileSync(file));
|
|
15943
|
+
});
|
|
15944
|
+
await new Promise((ok, fail) => {
|
|
15945
|
+
server.once("error", (e) => fail(e.code === "EADDRINUSE" ? new CliError(`Port ${o.port} is in use`, `Pick another with --port. The editor keeps one install per address, so stay with the one you choose.`) : e));
|
|
15946
|
+
server.listen(o.port, "127.0.0.1", () => ok());
|
|
15947
|
+
});
|
|
15948
|
+
const served = `http://127.0.0.1:${o.port}/`;
|
|
15949
|
+
const heartbeat = setInterval(() => {
|
|
15950
|
+
for (const c of clients) c.write(": still here\n\n");
|
|
15951
|
+
}, 2e4);
|
|
15952
|
+
const build = () => new Promise((done) => {
|
|
15953
|
+
if (!builds) return done(null);
|
|
15954
|
+
const child = spawn(process.platform === "win32" ? "npm.cmd" : "npm", [
|
|
15955
|
+
"run",
|
|
15956
|
+
"build",
|
|
15957
|
+
"--silent"
|
|
15958
|
+
], {
|
|
15959
|
+
cwd: dir,
|
|
15960
|
+
shell: process.platform === "win32"
|
|
15961
|
+
});
|
|
15962
|
+
let text = "";
|
|
15963
|
+
child.stdout.on("data", (d) => {
|
|
15964
|
+
text += d;
|
|
15965
|
+
});
|
|
15966
|
+
child.stderr.on("data", (d) => {
|
|
15967
|
+
text += d;
|
|
15968
|
+
});
|
|
15969
|
+
child.on("close", (code) => done(code === 0 ? null : text.trim() || `npm run build exited with ${code}`));
|
|
15970
|
+
child.on("error", (e) => done(e.message));
|
|
15971
|
+
});
|
|
15972
|
+
const failed = await build();
|
|
15973
|
+
const first = checkPlugin(dir);
|
|
15974
|
+
print(`${import_picocolors.default.bold(first.name ?? basename(dir))}${first.version ? import_picocolors.default.dim(` ${first.version}`) : ""}`);
|
|
15975
|
+
if (failed) print(`${mark.error} The build failed`, import_picocolors.default.dim(failed.split("\n").map((l) => ` ${l}`).join("\n")));
|
|
15976
|
+
else for (const line of printPluginCheck(first, false).slice(1)) print(line);
|
|
15977
|
+
const url = devEditorUrl(o.editor ?? process.env.SCMJS_EDITOR_URL ?? "https://scmjs.dev/editor/", served);
|
|
15978
|
+
print(`${mark.info} Serving it at ${import_picocolors.default.cyan(served)}${builds ? dot + "built with npm run build" : ""}`);
|
|
15979
|
+
if (o.open && process.env.BROWSER !== "none") {
|
|
15980
|
+
openBrowser(url);
|
|
15981
|
+
print(`${mark.info} Opening the editor. The first time, it asks you to add the plugin.`);
|
|
15982
|
+
}
|
|
15983
|
+
print(import_picocolors.default.dim(` If nothing opens, go to ${url}`), import_picocolors.default.dim(" Watching for changes. Ctrl+C stops."));
|
|
15984
|
+
let timer = null, changed = /* @__PURE__ */ new Set(), running = false;
|
|
15985
|
+
const built = manifest.build?.replace(/^\.\//, "") ?? null;
|
|
15986
|
+
const skip = (f) => {
|
|
15987
|
+
const p = f.replace(/\\/g, "/");
|
|
15988
|
+
return /(^|\/)(?:node_modules|\.git)(\/|$)/.test(p) || !!built && (p === built || p === dirname(built));
|
|
15989
|
+
};
|
|
15990
|
+
const flush = async () => {
|
|
15991
|
+
if (running) {
|
|
15992
|
+
timer = setTimeout(flush, 100);
|
|
15993
|
+
return;
|
|
15994
|
+
}
|
|
15995
|
+
running = true;
|
|
15996
|
+
const files = [...changed];
|
|
15997
|
+
changed = /* @__PURE__ */ new Set();
|
|
15998
|
+
const err = await build();
|
|
15999
|
+
const at = (/* @__PURE__ */ new Date()).toTimeString().slice(0, 8);
|
|
16000
|
+
if (err) {
|
|
16001
|
+
print(`${mark.error} ${import_picocolors.default.dim(at)} The build failed`, import_picocolors.default.dim(err.split("\n").slice(0, 12).map((l) => ` ${l}`).join("\n")));
|
|
16002
|
+
send("failed", { text: err.split("\n").slice(0, 3).join("\n") });
|
|
16003
|
+
} else {
|
|
16004
|
+
send("reload", { files });
|
|
16005
|
+
print(`${mark.ok} ${import_picocolors.default.dim(at)} ${files.slice(0, 3).join(", ")}${files.length > 3 ? ` and ${files.length - 3} more` : ""}${clients.size ? "; reloaded in the editor" : import_picocolors.default.dim("; no editor connected")}`);
|
|
16006
|
+
}
|
|
16007
|
+
running = false;
|
|
16008
|
+
};
|
|
16009
|
+
const watcher = watch(dir, { recursive: true }, (_e, f) => {
|
|
16010
|
+
if (!f || skip(f)) return;
|
|
16011
|
+
changed.add(f.replace(/\\/g, "/"));
|
|
16012
|
+
if (timer) clearTimeout(timer);
|
|
16013
|
+
timer = setTimeout(flush, 150);
|
|
16014
|
+
});
|
|
16015
|
+
await new Promise((done) => {
|
|
16016
|
+
const stop = () => {
|
|
16017
|
+
watcher.close();
|
|
16018
|
+
clearInterval(heartbeat);
|
|
16019
|
+
for (const c of clients) c.end();
|
|
16020
|
+
server.close();
|
|
16021
|
+
print("", import_picocolors.default.dim("Stopped."));
|
|
16022
|
+
done();
|
|
16023
|
+
};
|
|
16024
|
+
process.once("SIGINT", stop);
|
|
16025
|
+
process.once("SIGTERM", stop);
|
|
16026
|
+
});
|
|
16027
|
+
}
|
|
16028
|
+
//#endregion
|
|
15228
16029
|
//#region cli/src/replay.ts
|
|
15229
16030
|
/** `scmjs replay info`: who played, on what, for how long, from the replay's header, commands and its copy of the map. */
|
|
15230
16031
|
var RACES = [
|
|
@@ -15352,8 +16153,8 @@ function printReplayInfo(r) {
|
|
|
15352
16153
|
//#endregion
|
|
15353
16154
|
//#region cli/src/main.ts
|
|
15354
16155
|
/**
|
|
15355
|
-
* scmjs: StarCraft: Brood War maps and
|
|
15356
|
-
* (`map`, `replay`), the editor's own code underneath (docs/cli.md, .claude/internals/cli.md).
|
|
16156
|
+
* scmjs: StarCraft: Brood War maps, replays and bots from the command line. One command, a noun for each kind of
|
|
16157
|
+
* thing (`map`, `replay`, `plugin`, `bot`), the editor's own code underneath (docs/cli.md, .claude/internals/cli.md).
|
|
15357
16158
|
*/
|
|
15358
16159
|
var json = new Option("--json", "print JSON instead of text, for scripts");
|
|
15359
16160
|
/** Runs `fn` on each file in turn; a file that fails is said and the rest go on. JSON: one value, or a list for several. */
|
|
@@ -15442,29 +16243,35 @@ async function botInfo(input) {
|
|
|
15442
16243
|
]
|
|
15443
16244
|
};
|
|
15444
16245
|
}
|
|
15445
|
-
|
|
16246
|
+
for (const stream of [process.stdout, process.stderr]) stream.on("error", (e) => {
|
|
16247
|
+
if (e.code === "EPIPE") process.exit(process.exitCode ?? 0);
|
|
16248
|
+
else throw e;
|
|
16249
|
+
});
|
|
16250
|
+
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({
|
|
15446
16251
|
styleTitle: (s) => import_picocolors.default.bold(s),
|
|
15447
16252
|
styleCommandText: (s) => import_picocolors.default.cyan(s),
|
|
15448
16253
|
styleSubcommandText: (s) => import_picocolors.default.cyan(s),
|
|
15449
16254
|
styleArgumentText: (s) => import_picocolors.default.yellow(s),
|
|
15450
|
-
styleOptionText: (s) => import_picocolors.default.green(s)
|
|
16255
|
+
styleOptionText: (s) => import_picocolors.default.green(s),
|
|
16256
|
+
subcommandTerm: (cmd) => {
|
|
16257
|
+
const subs = cmd.commands.map((c) => c.name()).filter((n) => n !== "help");
|
|
16258
|
+
return subs.length ? `${cmd.name()} ${subs.join("|")}` : new Help$1().subcommandTerm(cmd).replace(" [options]", "");
|
|
16259
|
+
}
|
|
15451
16260
|
}).addHelpText("after", () => `
|
|
15452
16261
|
${import_picocolors.default.bold("Examples:")}
|
|
15453
16262
|
${import_picocolors.default.dim("$")} scmjs info "(2)Destination.scx" what a map or a replay holds
|
|
15454
16263
|
${import_picocolors.default.dim("$")} scmjs map check *.scx Check Map on every map here
|
|
15455
16264
|
${import_picocolors.default.dim("$")} scmjs replay open game.rep watch it in the editor
|
|
16265
|
+
${import_picocolors.default.dim("$")} scmjs plugin new "Base Check" a plugin for the editor to start from
|
|
16266
|
+
${import_picocolors.default.dim("$")} scmjs bot new a BWAPI bot to start from, for scmjs.dev/play
|
|
15456
16267
|
${import_picocolors.default.dim("$")} scmjs about this version, and where things are
|
|
15457
16268
|
|
|
15458
16269
|
${import_picocolors.default.dim("Docs: https://docs.scmjs.dev")}`);
|
|
15459
|
-
program.command("
|
|
15460
|
-
const r = aboutReport();
|
|
15461
|
-
print(...o.json ? [JSON.stringify(r, null, 2)] : printAbout(r));
|
|
15462
|
-
});
|
|
15463
|
-
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) => {
|
|
16270
|
+
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) => {
|
|
15464
16271
|
const kind = expectKind(input, "map", "replay", "bot");
|
|
15465
16272
|
return kind === "map" ? mapInfo(input) : kind === "replay" ? replayInfo(input) : botInfo(input);
|
|
15466
16273
|
}));
|
|
15467
|
-
var map = program.command("map").description("read, check and open maps");
|
|
16274
|
+
var map = program.command("map").description("read, check and open maps").helpGroup("Maps and replays:");
|
|
15468
16275
|
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));
|
|
15469
16276
|
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) => {
|
|
15470
16277
|
const reports = await eachFile(files, !!o.json, async (input) => {
|
|
@@ -15491,15 +16298,61 @@ map.command("open").description("open a map in the scmJS app, or the editor in y
|
|
|
15491
16298
|
expectKind(input, "map");
|
|
15492
16299
|
await openInScmjs(input, o);
|
|
15493
16300
|
});
|
|
15494
|
-
var replay = program.command("replay").description("read and open replays");
|
|
16301
|
+
var replay = program.command("replay").description("read and open replays").helpGroup("Maps and replays:");
|
|
15495
16302
|
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));
|
|
15496
16303
|
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) => {
|
|
15497
16304
|
const input = await readInput(file);
|
|
15498
16305
|
expectKind(input, "replay");
|
|
15499
16306
|
await openInScmjs(input, o);
|
|
15500
16307
|
});
|
|
16308
|
+
var plugin = program.command("plugin").description("start, check and run a plugin for the editor").helpGroup("Plugins and bots:");
|
|
16309
|
+
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) => {
|
|
16310
|
+
const made = newPlugin({
|
|
16311
|
+
name,
|
|
16312
|
+
dir: o.dir
|
|
16313
|
+
});
|
|
16314
|
+
const rel = relative(process.cwd(), made.dir) || ".";
|
|
16315
|
+
print(`${mark.ok} ${import_picocolors.default.bold(name ?? rel)} written in ${import_picocolors.default.cyan(rel)}${dot}${plural(made.files.length, "file")}`, "", import_picocolors.default.dim(" Next:"), ` cd ${rel.includes(" ") ? JSON.stringify(rel) : rel}`, " npm install", ` npm start ${import_picocolors.default.dim("the editor with your plugin, reloaded each time you save")}`, "", import_picocolors.default.dim(" plugin.ts is the plugin; plugin.json describes it. Guide: https://docs.scmjs.dev/plugins/"));
|
|
16316
|
+
});
|
|
16317
|
+
plugin.command("check").description("what the editor would make of the plugin").argument("[folder]", "the plugin's folder", ".").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 the editor would not load it (or on warnings, with --strict): fit for a CI step.`).action((folder, o) => {
|
|
16318
|
+
const c = checkPlugin(folder);
|
|
16319
|
+
if (c.issues.some((i) => i.level === "error" || o.strict && i.level === "warn")) process.exitCode = 1;
|
|
16320
|
+
print(...o.json ? [JSON.stringify(c, null, 2)] : printPluginCheck(c, !!o.all));
|
|
16321
|
+
});
|
|
16322
|
+
plugin.command("dev").description("the editor with your plugin, reloaded on every save").argument("[folder]", "the plugin's folder", ".").option("--port <port>", "the port to serve it on; the editor remembers the plugin by it", "5310").option("--no-open", "do not open the editor; print its address instead").addOption(new Option("--editor <url>", "the editor's address (SCMJS_EDITOR_URL), such as a local one").hideHelp()).addHelpText("after", `\nBuilds it first when plugin.json names a "build" and package.json has a build script.`).action(async (folder, o) => {
|
|
16323
|
+
const port = Number(o.port);
|
|
16324
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new CliError(`--port ${o.port} is not a port`, null, 2);
|
|
16325
|
+
await devPlugin({
|
|
16326
|
+
dir: folder,
|
|
16327
|
+
port,
|
|
16328
|
+
open: o.open,
|
|
16329
|
+
editor: o.editor
|
|
16330
|
+
});
|
|
16331
|
+
});
|
|
16332
|
+
var bot = program.command("bot").description("build a BWAPI bot for scmjs.dev/play (Docker)").helpGroup("Plugins and bots:").addHelpText("after", () => `
|
|
16333
|
+
Run these in your bot's folder. The toolchain is a Docker image, downloaded the first time.
|
|
16334
|
+
A built bot is added on the play page: Options → Community bots → Add a bot from a file…
|
|
16335
|
+
|
|
16336
|
+
${import_picocolors.default.dim(`Guide: ${BOT_GUIDE}`)}`);
|
|
16337
|
+
bot.command("new").description("a bot to start from, in this folder").action(async () => {
|
|
16338
|
+
await runBot(["new"]);
|
|
16339
|
+
});
|
|
16340
|
+
bot.command("init").description("scmbot.json from the bot's Visual Studio project").argument("[project]", "the .vcxproj, when the folder has several").action(async (project) => {
|
|
16341
|
+
await runBot(project ? ["init", project] : ["init"]);
|
|
16342
|
+
});
|
|
16343
|
+
bot.command("build").description("out/<key>.scmbot, to add on the play page").action(async () => {
|
|
16344
|
+
await runBot(["build"]);
|
|
16345
|
+
});
|
|
16346
|
+
bot.command("update").description("the newest toolchain, the one the play page expects").action(async () => {
|
|
16347
|
+
await updateBot();
|
|
16348
|
+
});
|
|
16349
|
+
program.command("about").description("this version, any newer one, and links").helpGroup("This tool:").addOption(json).action(async (o) => {
|
|
16350
|
+
const r = await aboutReport();
|
|
16351
|
+
print(...o.json ? [JSON.stringify(r, null, 2)] : printAbout(r));
|
|
16352
|
+
});
|
|
15501
16353
|
try {
|
|
15502
|
-
|
|
16354
|
+
if (process.argv.length <= 2) program.outputHelp();
|
|
16355
|
+
else await program.parseAsync();
|
|
15503
16356
|
} catch (e) {
|
|
15504
16357
|
if (e instanceof CliError) {
|
|
15505
16358
|
fail(e);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scm-js/cli",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "StarCraft: Brood War maps and
|
|
3
|
+
"version": "0.8.10",
|
|
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",
|
|
7
7
|
"brood war",
|
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
"scm",
|
|
11
11
|
"scx",
|
|
12
12
|
"rep",
|
|
13
|
-
"cli"
|
|
13
|
+
"cli",
|
|
14
|
+
"bwapi",
|
|
15
|
+
"bot"
|
|
14
16
|
],
|
|
15
17
|
"license": "MIT",
|
|
18
|
+
"author": "Jeany",
|
|
16
19
|
"type": "module",
|
|
17
20
|
"bin": {
|
|
18
21
|
"scmjs": "dist/scmjs.mjs"
|