@s2script/sdk 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +588 -88
- package/package.json +4 -1
- package/unsafe.d.ts +26 -0
package/dist/cli.js
CHANGED
|
@@ -1074,8 +1074,8 @@ function unzipSync(data, opts) {
|
|
|
1074
1074
|
}
|
|
1075
1075
|
|
|
1076
1076
|
// src/build.ts
|
|
1077
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync } from "node:fs";
|
|
1078
|
-
import { resolve as resolve5, join as join6 } from "node:path";
|
|
1077
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync, rmSync as rmSync2 } from "node:fs";
|
|
1078
|
+
import { resolve as resolve5, join as join6, sep, basename } from "node:path";
|
|
1079
1079
|
|
|
1080
1080
|
// src/typecheck/typecheck.ts
|
|
1081
1081
|
import ts from "typescript";
|
|
@@ -1168,6 +1168,10 @@ function localDeclarationFiles(pluginDir) {
|
|
|
1168
1168
|
if (!existsSync3(srcDir)) return [];
|
|
1169
1169
|
return readdirSync(srcDir).filter((f) => f.endsWith(".d.ts")).map((f) => join3(srcDir, f));
|
|
1170
1170
|
}
|
|
1171
|
+
function generatedDeclarationFiles(pluginDir) {
|
|
1172
|
+
const gd = join3(pluginDir, ".s2script", "gamedata.d.ts");
|
|
1173
|
+
return existsSync3(gd) ? [gd] : [];
|
|
1174
|
+
}
|
|
1171
1175
|
function declaredModules(dtsFiles) {
|
|
1172
1176
|
const out = /* @__PURE__ */ new Set();
|
|
1173
1177
|
for (const f of dtsFiles) {
|
|
@@ -1217,7 +1221,12 @@ function typecheckPlugin(pluginDir, opts) {
|
|
|
1217
1221
|
...contractPaths
|
|
1218
1222
|
}
|
|
1219
1223
|
};
|
|
1220
|
-
const rootNames = [
|
|
1224
|
+
const rootNames = [
|
|
1225
|
+
entry,
|
|
1226
|
+
join3(packagesDir, "sdk", "globals.d.ts"),
|
|
1227
|
+
...localDts,
|
|
1228
|
+
...generatedDeclarationFiles(absDir)
|
|
1229
|
+
];
|
|
1221
1230
|
const tmp = mkdtempSync(join3(tmpdir(), "s2tc-"));
|
|
1222
1231
|
try {
|
|
1223
1232
|
if (deps.length) {
|
|
@@ -1521,6 +1530,253 @@ async function lintPlugin(pluginDir, program) {
|
|
|
1521
1530
|
return { ok: errorCount === 0, output, errorCount };
|
|
1522
1531
|
}
|
|
1523
1532
|
|
|
1533
|
+
// src/gamedata/types.ts
|
|
1534
|
+
var PLATFORM = "linuxsteamrt64";
|
|
1535
|
+
var ARG_KINDS = ["bool", "int", "float", "string", "vector", "entity"];
|
|
1536
|
+
var RET_KINDS = ["void", "bool", "int", "float", "entity"];
|
|
1537
|
+
var INT_CLASS_ARGS = /* @__PURE__ */ new Set(["bool", "int", "string", "vector", "entity"]);
|
|
1538
|
+
var MAX_INT_ARGS = 5;
|
|
1539
|
+
var MAX_FLOAT_ARGS = 8;
|
|
1540
|
+
|
|
1541
|
+
// src/gamedata/validate.ts
|
|
1542
|
+
var ALLOWED_SECTIONS = /* @__PURE__ */ new Set(["signatures", "calls"]);
|
|
1543
|
+
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
1544
|
+
var RESERVED_CALL_NAMES = /* @__PURE__ */ new Set(["constructor", "prototype", "__proto__"]);
|
|
1545
|
+
var RESOLVE_KINDS = ["direct", "ctor-body-xref", "lea-disp"];
|
|
1546
|
+
var KNOWN_PERMISSIONS = ["engine:calls"];
|
|
1547
|
+
function validatePluginGamedata(gd, opts) {
|
|
1548
|
+
const errs = [];
|
|
1549
|
+
if (gd == null || typeof gd !== "object" || Array.isArray(gd)) return ["gamedata must be an object"];
|
|
1550
|
+
const g = gd;
|
|
1551
|
+
for (const key of Object.keys(g)) {
|
|
1552
|
+
if (!ALLOWED_SECTIONS.has(key)) {
|
|
1553
|
+
errs.push(`gamedata section '${key}' is not supported in v1 (allowed: signatures, calls)`);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
const sigs = g.signatures ?? {};
|
|
1557
|
+
if (typeof sigs !== "object" || Array.isArray(sigs)) errs.push("signatures must be an object");
|
|
1558
|
+
for (const [name, platforms] of Object.entries(sigs)) {
|
|
1559
|
+
const p = platforms?.[PLATFORM];
|
|
1560
|
+
if (!p) {
|
|
1561
|
+
errs.push(`signature '${name}': missing platform '${PLATFORM}'`);
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
for (const f of ["module", "pattern", "resolve"]) {
|
|
1565
|
+
if (typeof p[f] !== "string" || !p[f].length) {
|
|
1566
|
+
errs.push(`signature '${name}': '${f}' must be a non-empty string`);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
if (typeof p.resolve === "string" && p.resolve.length && !RESOLVE_KINDS.includes(p.resolve)) {
|
|
1570
|
+
errs.push(
|
|
1571
|
+
`signature '${name}': unknown resolve step ${JSON.stringify(p.resolve)} (allowed: ${RESOLVE_KINDS.join(", ")})`
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
for (const perm of opts.permissions) {
|
|
1576
|
+
if (!KNOWN_PERMISSIONS.includes(perm)) {
|
|
1577
|
+
errs.push(`unknown permission ${JSON.stringify(perm)} (allowed: ${KNOWN_PERMISSIONS.join(", ")})`);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
const calls = g.calls ?? {};
|
|
1581
|
+
const callNames = Object.keys(calls);
|
|
1582
|
+
if (callNames.length && !opts.permissions.includes("engine:calls")) {
|
|
1583
|
+
errs.push('gamedata declares a `calls` section but the manifest does not declare permission "engine:calls"');
|
|
1584
|
+
}
|
|
1585
|
+
for (const [name, rawDecl] of Object.entries(calls)) {
|
|
1586
|
+
const where = `call '${name}'`;
|
|
1587
|
+
if (!IDENTIFIER.test(name)) {
|
|
1588
|
+
errs.push(
|
|
1589
|
+
`${where}: call name must be a plain identifier matching ${IDENTIFIER.source} \u2014 it is emitted verbatim as a TypeScript interface member in the generated .s2script/gamedata.d.ts`
|
|
1590
|
+
);
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
if (RESERVED_CALL_NAMES.has(name)) {
|
|
1594
|
+
errs.push(`${where}: call name '${name}' is reserved`);
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
if (rawDecl == null || typeof rawDecl !== "object") {
|
|
1598
|
+
errs.push(`${where}: must be an object`);
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
const decl = rawDecl;
|
|
1602
|
+
const recv = decl.receiver;
|
|
1603
|
+
if (!recv || recv.kind !== "entity") {
|
|
1604
|
+
errs.push(`${where}: receiver.kind must be "entity" in v1`);
|
|
1605
|
+
} else if (recv.via !== void 0) {
|
|
1606
|
+
const via = recv.via;
|
|
1607
|
+
if (typeof via?.class !== "string" || typeof via?.field !== "string") {
|
|
1608
|
+
errs.push(`${where}: receiver.via requires both 'class' and 'field' strings`);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
const target = decl.target;
|
|
1612
|
+
if (!target) {
|
|
1613
|
+
errs.push(`${where}: missing 'target'`);
|
|
1614
|
+
} else if (target.kind === "signature") {
|
|
1615
|
+
if (typeof target.name !== "string") errs.push(`${where}: target.name must be a string`);
|
|
1616
|
+
else if (!(target.name in sigs)) errs.push(`${where}: target.name '${target.name}' has no entry in 'signatures'`);
|
|
1617
|
+
} else if (target.kind === "vtable") {
|
|
1618
|
+
if (typeof target.class !== "string") errs.push(`${where}: vtable target requires 'class'`);
|
|
1619
|
+
const plat = target[PLATFORM];
|
|
1620
|
+
if (!plat) {
|
|
1621
|
+
errs.push(`${where}: vtable target missing platform '${PLATFORM}'`);
|
|
1622
|
+
} else {
|
|
1623
|
+
if (!Number.isInteger(plat.index) || plat.index < 0) {
|
|
1624
|
+
errs.push(`${where}: vtable index must be a non-negative integer`);
|
|
1625
|
+
}
|
|
1626
|
+
const prologue = plat.validate?.prologue;
|
|
1627
|
+
if (typeof prologue !== "string" || !prologue.length) {
|
|
1628
|
+
errs.push(`${where}: a vtable target REQUIRES validate.prologue (a bare borrowed index is never trusted)`);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
} else {
|
|
1632
|
+
errs.push(`${where}: target.kind must be "signature" or "vtable"`);
|
|
1633
|
+
}
|
|
1634
|
+
const args = decl.args;
|
|
1635
|
+
if (!Array.isArray(args)) {
|
|
1636
|
+
errs.push(`${where}: 'args' must be an array`);
|
|
1637
|
+
} else {
|
|
1638
|
+
let ints = 0, floats = 0;
|
|
1639
|
+
for (const a2 of args) {
|
|
1640
|
+
if (typeof a2 !== "string" || !ARG_KINDS.includes(a2)) {
|
|
1641
|
+
errs.push(`${where}: unknown arg kind ${JSON.stringify(a2)} (allowed: ${ARG_KINDS.join(", ")})`);
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1644
|
+
if (INT_CLASS_ARGS.has(a2)) ints++;
|
|
1645
|
+
else floats++;
|
|
1646
|
+
}
|
|
1647
|
+
if (ints > MAX_INT_ARGS) {
|
|
1648
|
+
errs.push(`${where}: ${ints} integer-class args exceeds the max of ${MAX_INT_ARGS} (\`this\` occupies rdi)`);
|
|
1649
|
+
}
|
|
1650
|
+
if (floats > MAX_FLOAT_ARGS) {
|
|
1651
|
+
errs.push(`${where}: ${floats} float args exceeds the max of ${MAX_FLOAT_ARGS}`);
|
|
1652
|
+
}
|
|
1653
|
+
if (decl.argNames !== void 0) {
|
|
1654
|
+
const names = decl.argNames;
|
|
1655
|
+
if (!Array.isArray(names)) {
|
|
1656
|
+
errs.push(`${where}: 'argNames' must be an array of parameter names`);
|
|
1657
|
+
} else if (names.length !== args.length) {
|
|
1658
|
+
errs.push(
|
|
1659
|
+
`${where}: 'argNames' has ${names.length} entr${names.length === 1 ? "y" : "ies"} but 'args' has ${args.length} \u2014 they are positionally matched`
|
|
1660
|
+
);
|
|
1661
|
+
} else {
|
|
1662
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1663
|
+
for (const nm of names) {
|
|
1664
|
+
if (typeof nm !== "string" || !IDENTIFIER.test(nm)) {
|
|
1665
|
+
errs.push(
|
|
1666
|
+
`${where}: argName ${JSON.stringify(nm)} must be a plain identifier matching ${IDENTIFIER.source} \u2014 it is emitted as a parameter name in the generated .d.ts`
|
|
1667
|
+
);
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
if (nm === "self") {
|
|
1671
|
+
errs.push(`${where}: argName 'self' is reserved (it names the receiver parameter)`);
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
if (seen.has(nm)) errs.push(`${where}: duplicate argName ${JSON.stringify(nm)}`);
|
|
1675
|
+
seen.add(nm);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
if (typeof decl.returns !== "string" || !RET_KINDS.includes(decl.returns)) {
|
|
1681
|
+
errs.push(`${where}: 'returns' must be one of ${RET_KINDS.join(", ")}`);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return errs;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// src/gamedata/gen-types.ts
|
|
1688
|
+
var VECTOR_TS = "{ readonly x: number; readonly y: number; readonly z: number }";
|
|
1689
|
+
var ARG_TS = {
|
|
1690
|
+
bool: "boolean",
|
|
1691
|
+
int: "number",
|
|
1692
|
+
float: "number",
|
|
1693
|
+
string: "string",
|
|
1694
|
+
vector: VECTOR_TS,
|
|
1695
|
+
entity: "EntityRef | null"
|
|
1696
|
+
};
|
|
1697
|
+
var RET_TS = {
|
|
1698
|
+
void: "void",
|
|
1699
|
+
bool: "boolean | null",
|
|
1700
|
+
int: "number | null",
|
|
1701
|
+
float: "number | null",
|
|
1702
|
+
entity: "EntityRef | null"
|
|
1703
|
+
};
|
|
1704
|
+
function generateGamedataTypes(gd) {
|
|
1705
|
+
const calls = gd.calls ?? {};
|
|
1706
|
+
const lines = Object.keys(calls).sort().map((name) => {
|
|
1707
|
+
const decl = calls[name];
|
|
1708
|
+
const names = decl.argNames;
|
|
1709
|
+
const params = [
|
|
1710
|
+
"self: EntityRef",
|
|
1711
|
+
...decl.args.map((a2, i2) => `${names?.[i2] ?? `a${i2}`}: ${ARG_TS[a2]}`)
|
|
1712
|
+
];
|
|
1713
|
+
return ` ${name}: (${params.join(", ")}) => ${RET_TS[decl.returns]};`;
|
|
1714
|
+
});
|
|
1715
|
+
const body = lines.length ? `
|
|
1716
|
+
${lines.join("\n")}
|
|
1717
|
+
` : "\n ";
|
|
1718
|
+
return [
|
|
1719
|
+
"// GENERATED by `s2s build` from the plugin's gamedata \u2014 DO NOT EDIT.",
|
|
1720
|
+
"// The EntityRef import is REQUIRED: a `declare module` augmentation resolves type names in its",
|
|
1721
|
+
"// own file scope, so an un-imported EntityRef degrades to an error skipLibCheck can swallow.",
|
|
1722
|
+
'import type { EntityRef } from "@s2script/sdk/entity";',
|
|
1723
|
+
"",
|
|
1724
|
+
'declare module "@s2script/sdk/unsafe" {',
|
|
1725
|
+
` interface EngineCalls {${body}}`,
|
|
1726
|
+
"}",
|
|
1727
|
+
""
|
|
1728
|
+
].join("\n");
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/gamedata/jsonc.ts
|
|
1732
|
+
function stripJsonComments(src) {
|
|
1733
|
+
let out = "";
|
|
1734
|
+
let i2 = 0;
|
|
1735
|
+
const n3 = src.length;
|
|
1736
|
+
while (i2 < n3) {
|
|
1737
|
+
const c2 = src[i2];
|
|
1738
|
+
if (c2 === '"') {
|
|
1739
|
+
out += c2;
|
|
1740
|
+
i2++;
|
|
1741
|
+
while (i2 < n3) {
|
|
1742
|
+
const s = src[i2];
|
|
1743
|
+
if (s === "\\") {
|
|
1744
|
+
out += src.slice(i2, i2 + 2);
|
|
1745
|
+
i2 += 2;
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
out += s;
|
|
1749
|
+
i2++;
|
|
1750
|
+
if (s === '"') break;
|
|
1751
|
+
}
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
if (c2 === "/" && src[i2 + 1] === "/") {
|
|
1755
|
+
while (i2 < n3 && src[i2] !== "\n") {
|
|
1756
|
+
out += " ";
|
|
1757
|
+
i2++;
|
|
1758
|
+
}
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
if (c2 === "/" && src[i2 + 1] === "*") {
|
|
1762
|
+
out += " ";
|
|
1763
|
+
i2 += 2;
|
|
1764
|
+
while (i2 < n3 && !(src[i2] === "*" && src[i2 + 1] === "/")) {
|
|
1765
|
+
out += src[i2] === "\n" ? "\n" : " ";
|
|
1766
|
+
i2++;
|
|
1767
|
+
}
|
|
1768
|
+
if (i2 < n3) {
|
|
1769
|
+
out += " ";
|
|
1770
|
+
i2 += 2;
|
|
1771
|
+
}
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
out += c2;
|
|
1775
|
+
i2++;
|
|
1776
|
+
}
|
|
1777
|
+
return out;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1524
1780
|
// src/build.ts
|
|
1525
1781
|
async function buildPlugin(dir, packagesDir) {
|
|
1526
1782
|
const absDir = resolve5(dir);
|
|
@@ -1533,6 +1789,39 @@ async function buildPlugin(dir, packagesDir) {
|
|
|
1533
1789
|
if (cfgErrs.length) throw new Error(`invalid s2script.config:
|
|
1534
1790
|
${cfgErrs.join("\n ")}`);
|
|
1535
1791
|
}
|
|
1792
|
+
const permissions = Array.isArray(s2.permissions) ? s2.permissions : [];
|
|
1793
|
+
let gamedata;
|
|
1794
|
+
if (typeof s2.gamedata === "string") {
|
|
1795
|
+
const gdPath = resolve5(absDir, s2.gamedata);
|
|
1796
|
+
const dirPrefix = absDir.endsWith(sep) ? absDir : absDir + sep;
|
|
1797
|
+
if (gdPath !== absDir && !gdPath.startsWith(dirPrefix)) {
|
|
1798
|
+
throw new Error(
|
|
1799
|
+
`s2script.gamedata escapes the plugin directory: ${JSON.stringify(s2.gamedata)} resolves to ${gdPath}`
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
const unscoped = pkg.name.includes("/") ? pkg.name.slice(pkg.name.lastIndexOf("/") + 1) : pkg.name;
|
|
1803
|
+
const actualBase = basename(gdPath);
|
|
1804
|
+
const allowedBases = [`${unscoped}.gamedata.jsonc`, `${unscoped}.gamedata.json`];
|
|
1805
|
+
if (!allowedBases.includes(actualBase)) {
|
|
1806
|
+
throw new Error(
|
|
1807
|
+
`s2script.gamedata must be named '${unscoped}.gamedata.jsonc' (after the plugin name without its scope, like the framework's own core.gamedata.jsonc), but is '${actualBase}'`
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
const raw = stripJsonComments(readFileSync4(gdPath, "utf8"));
|
|
1811
|
+
try {
|
|
1812
|
+
gamedata = JSON.parse(raw);
|
|
1813
|
+
} catch (e) {
|
|
1814
|
+
throw new Error(`invalid gamedata ${gdPath}: ${e.message}`);
|
|
1815
|
+
}
|
|
1816
|
+
const gdErrs = validatePluginGamedata(gamedata, { permissions });
|
|
1817
|
+
if (gdErrs.length) throw new Error(`invalid gamedata:
|
|
1818
|
+
${gdErrs.join("\n ")}`);
|
|
1819
|
+
const genDir = join6(absDir, ".s2script");
|
|
1820
|
+
mkdirSync(genDir, { recursive: true });
|
|
1821
|
+
writeFileSync2(join6(genDir, "gamedata.d.ts"), generateGamedataTypes(gamedata));
|
|
1822
|
+
} else {
|
|
1823
|
+
rmSync2(join6(absDir, ".s2script", "gamedata.d.ts"), { force: true });
|
|
1824
|
+
}
|
|
1536
1825
|
const tc = typecheckPlugin(absDir, packagesDir !== void 0 ? { packagesDir } : void 0);
|
|
1537
1826
|
if (!tc.ok) {
|
|
1538
1827
|
throw new Error(`typecheck failed (${tc.diagnostics.length} error(s)):
|
|
@@ -1642,6 +1931,7 @@ ${lint.output}`);
|
|
|
1642
1931
|
manifest.publishes = derivedPublishes;
|
|
1643
1932
|
}
|
|
1644
1933
|
if (config !== void 0) manifest.config = config;
|
|
1934
|
+
if (permissions.length > 0) manifest.permissions = permissions;
|
|
1645
1935
|
const compiledAgainst = {};
|
|
1646
1936
|
for (const dep of [
|
|
1647
1937
|
...Object.keys(pluginDependencies),
|
|
@@ -1655,6 +1945,7 @@ ${lint.output}`);
|
|
|
1655
1945
|
"manifest.json": Buffer.from(JSON.stringify(manifest, null, 2)),
|
|
1656
1946
|
"plugin.js": Buffer.from(pluginJs)
|
|
1657
1947
|
};
|
|
1948
|
+
if (gamedata) zipFiles["gamedata.json"] = Buffer.from(JSON.stringify(gamedata, null, 2));
|
|
1658
1949
|
if (gate.typesPath !== null && Object.keys(derivedPublishes).length > 0) {
|
|
1659
1950
|
const contract = readFileSync4(gate.typesPath);
|
|
1660
1951
|
for (const iface of Object.keys(derivedPublishes)) {
|
|
@@ -3134,7 +3425,9 @@ var TIMEOUTS_MS = {
|
|
|
3134
3425
|
// up to ~40 MiB on a slow uplink
|
|
3135
3426
|
resolve: 3e4,
|
|
3136
3427
|
meta: 3e4,
|
|
3137
|
-
|
|
3428
|
+
plan: 3e4,
|
|
3429
|
+
"types download": 12e4,
|
|
3430
|
+
"s2sp download": 12e4
|
|
3138
3431
|
};
|
|
3139
3432
|
function rootCause(err2) {
|
|
3140
3433
|
let cur = err2;
|
|
@@ -3261,6 +3554,20 @@ var RegistryClient = class {
|
|
|
3261
3554
|
const res = await this.request("meta", u4);
|
|
3262
3555
|
return this.json(res, "meta");
|
|
3263
3556
|
}
|
|
3557
|
+
async plan(name, range = "*") {
|
|
3558
|
+
const u4 = new URL(`${this.baseUrl}/api/v1/plan`);
|
|
3559
|
+
u4.searchParams.set("name", name);
|
|
3560
|
+
u4.searchParams.set("range", range);
|
|
3561
|
+
const res = await this.request("plan", u4);
|
|
3562
|
+
return this.json(res, "plan");
|
|
3563
|
+
}
|
|
3564
|
+
async downloadS2sp(name, version) {
|
|
3565
|
+
const u4 = new URL(`${this.baseUrl}/api/v1/download/s2sp`);
|
|
3566
|
+
u4.searchParams.set("name", name);
|
|
3567
|
+
u4.searchParams.set("version", version);
|
|
3568
|
+
const res = await this.request("s2sp download", u4);
|
|
3569
|
+
return Buffer.from(await res.arrayBuffer());
|
|
3570
|
+
}
|
|
3264
3571
|
};
|
|
3265
3572
|
|
|
3266
3573
|
// src/registry/deploy.ts
|
|
@@ -3493,30 +3800,217 @@ async function run3(argv2) {
|
|
|
3493
3800
|
}
|
|
3494
3801
|
}
|
|
3495
3802
|
|
|
3803
|
+
// src/commands/install.ts
|
|
3804
|
+
import { resolve as resolve11 } from "node:path";
|
|
3805
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
3806
|
+
|
|
3807
|
+
// src/registry/install.ts
|
|
3808
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3809
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync6, renameSync, existsSync as existsSync9 } from "node:fs";
|
|
3810
|
+
import { basename as basename2, join as join10, resolve as resolve10, sep as sep2 } from "node:path";
|
|
3811
|
+
function parseManifest(input) {
|
|
3812
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
3813
|
+
throw new Error("manifest must be a JSON object");
|
|
3814
|
+
}
|
|
3815
|
+
const plugins = input.plugins;
|
|
3816
|
+
if (!plugins || typeof plugins !== "object" || Array.isArray(plugins)) {
|
|
3817
|
+
throw new Error('manifest must have a "plugins" object (name -> version range)');
|
|
3818
|
+
}
|
|
3819
|
+
const out = {};
|
|
3820
|
+
for (const [name, range] of Object.entries(plugins)) {
|
|
3821
|
+
if (typeof range !== "string") throw new Error(`range for ${name} must be a string`);
|
|
3822
|
+
out[name] = range;
|
|
3823
|
+
}
|
|
3824
|
+
return { plugins: out };
|
|
3825
|
+
}
|
|
3826
|
+
function parseSpec2(spec) {
|
|
3827
|
+
const at = spec.lastIndexOf("@");
|
|
3828
|
+
if (at > 0) return { name: spec.slice(0, at), range: spec.slice(at + 1) || "*" };
|
|
3829
|
+
return { name: spec, range: "*" };
|
|
3830
|
+
}
|
|
3831
|
+
function mergeSpecs(manifest, args) {
|
|
3832
|
+
const merged = { ...manifest.plugins };
|
|
3833
|
+
for (const a2 of args) {
|
|
3834
|
+
const { name, range } = parseSpec2(a2);
|
|
3835
|
+
merged[name] = range;
|
|
3836
|
+
}
|
|
3837
|
+
return merged;
|
|
3838
|
+
}
|
|
3839
|
+
function sha256Hex(bytes) {
|
|
3840
|
+
return createHash2("sha256").update(bytes).digest("hex");
|
|
3841
|
+
}
|
|
3842
|
+
var SAFE_FILENAME_RE = /^[A-Za-z0-9._-]+$/;
|
|
3843
|
+
function safeFilename(e) {
|
|
3844
|
+
const raw = e.filename;
|
|
3845
|
+
const base = basename2(raw);
|
|
3846
|
+
if (!base || base === "." || base === ".." || base !== raw || base.includes("/") || base.includes("\\") || base.includes("..") || !SAFE_FILENAME_RE.test(base)) {
|
|
3847
|
+
throw new Error(`refusing ${e.name}@${e.version}: unsafe filename "${raw}"`);
|
|
3848
|
+
}
|
|
3849
|
+
return base;
|
|
3850
|
+
}
|
|
3851
|
+
async function resolveMerged(client, specs) {
|
|
3852
|
+
const chosen = /* @__PURE__ */ new Map();
|
|
3853
|
+
const warnings = [];
|
|
3854
|
+
const errors = [];
|
|
3855
|
+
for (const [name, range] of Object.entries(specs)) {
|
|
3856
|
+
const plan = await client.plan(name, range);
|
|
3857
|
+
if (plan.errors.length) {
|
|
3858
|
+
errors.push(...plan.errors);
|
|
3859
|
+
continue;
|
|
3860
|
+
}
|
|
3861
|
+
warnings.push(...plan.warnings);
|
|
3862
|
+
for (const entry of plan.install) {
|
|
3863
|
+
const prior = chosen.get(entry.name);
|
|
3864
|
+
if (prior && prior.version !== entry.version) {
|
|
3865
|
+
errors.push(
|
|
3866
|
+
`version conflict for ${entry.name}: ${prior.version} vs ${entry.version} across requested plugins`
|
|
3867
|
+
);
|
|
3868
|
+
continue;
|
|
3869
|
+
}
|
|
3870
|
+
chosen.set(entry.name, entry);
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
return {
|
|
3874
|
+
install: [...chosen.values()].sort((a2, b) => a2.name.localeCompare(b.name)),
|
|
3875
|
+
warnings: [...new Set(warnings)],
|
|
3876
|
+
errors: [...new Set(errors)]
|
|
3877
|
+
};
|
|
3878
|
+
}
|
|
3879
|
+
async function installPlan(opts) {
|
|
3880
|
+
const log3 = opts.log ?? (() => {
|
|
3881
|
+
});
|
|
3882
|
+
const { install, warnings, errors } = await resolveMerged(opts.client, opts.specs);
|
|
3883
|
+
if (errors.length) throw new Error(`cannot resolve plugins:
|
|
3884
|
+
- ${errors.join("\n - ")}`);
|
|
3885
|
+
if (opts.reviewedOnly) {
|
|
3886
|
+
const unreviewed = install.filter((e) => e.reviewState !== "reviewed");
|
|
3887
|
+
if (unreviewed.length) {
|
|
3888
|
+
throw new Error(
|
|
3889
|
+
`--reviewed-only: refusing unreviewed plugins: ${unreviewed.map((e) => `${e.name}@${e.version}`).join(", ")}`
|
|
3890
|
+
);
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
const dirResolved = resolve10(opts.dir);
|
|
3894
|
+
const targets = install.map((e) => {
|
|
3895
|
+
if (!e.sha256) {
|
|
3896
|
+
throw new Error(`refusing ${e.name}@${e.version}: registry did not provide a sha256 to verify against`);
|
|
3897
|
+
}
|
|
3898
|
+
const filename = safeFilename(e);
|
|
3899
|
+
const dest = resolve10(join10(opts.dir, filename));
|
|
3900
|
+
if (dest !== dirResolved && !dest.startsWith(dirResolved + sep2)) {
|
|
3901
|
+
throw new Error(`refusing ${e.name}@${e.version}: destination "${dest}" escapes plugins dir "${dirResolved}"`);
|
|
3902
|
+
}
|
|
3903
|
+
return { entry: e, filename, dest };
|
|
3904
|
+
});
|
|
3905
|
+
for (const w of warnings) log3(`warning: ${w}`);
|
|
3906
|
+
if (opts.dryRun) {
|
|
3907
|
+
for (const e of install) log3(`would install ${e.name}@${e.version} -> ${e.filename}`);
|
|
3908
|
+
return { written: [], skipped: [], warnings };
|
|
3909
|
+
}
|
|
3910
|
+
mkdirSync4(opts.dir, { recursive: true });
|
|
3911
|
+
const written = [];
|
|
3912
|
+
const skipped = [];
|
|
3913
|
+
for (const { entry: e, filename, dest } of targets) {
|
|
3914
|
+
if (existsSync9(dest)) {
|
|
3915
|
+
const have = sha256Hex(readFileSync10(dest));
|
|
3916
|
+
if (have === e.sha256) {
|
|
3917
|
+
skipped.push(filename);
|
|
3918
|
+
log3(`up to date ${filename}`);
|
|
3919
|
+
continue;
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
const bytes = await opts.client.downloadS2sp(e.name, e.version);
|
|
3923
|
+
const got = sha256Hex(bytes);
|
|
3924
|
+
if (got !== e.sha256) {
|
|
3925
|
+
throw new Error(`sha256 mismatch for ${e.name}@${e.version}: expected ${e.sha256}, got ${got}`);
|
|
3926
|
+
}
|
|
3927
|
+
const tmp = `${dest}.${process.pid}.tmp`;
|
|
3928
|
+
writeFileSync6(tmp, bytes);
|
|
3929
|
+
renameSync(tmp, dest);
|
|
3930
|
+
written.push(filename);
|
|
3931
|
+
log3(`installed ${e.name}@${e.version} -> ${filename}`);
|
|
3932
|
+
}
|
|
3933
|
+
return { written, skipped, warnings };
|
|
3934
|
+
}
|
|
3935
|
+
function loadManifestFile(path) {
|
|
3936
|
+
if (!existsSync9(path)) return { plugins: {} };
|
|
3937
|
+
return parseManifest(JSON.parse(readFileSync10(path, "utf8")));
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
// src/commands/install.ts
|
|
3941
|
+
var AUTODETECT = "addons/s2script/plugins";
|
|
3942
|
+
async function run4(argv2) {
|
|
3943
|
+
const args = positionals(argv2, ["--dir", "--file", "--registry"]);
|
|
3944
|
+
const file = parseFlag(argv2, "--file") ?? "s2script-plugins.json";
|
|
3945
|
+
const registry = parseFlag(argv2, "--registry") || loadCredentials()?.registryUrl || defaultRegistryUrl();
|
|
3946
|
+
const dryRun = hasFlag(argv2, "--dry-run");
|
|
3947
|
+
const reviewedOnly = hasFlag(argv2, "--reviewed-only");
|
|
3948
|
+
let dir = parseFlag(argv2, "--dir");
|
|
3949
|
+
if (!dir) {
|
|
3950
|
+
if (existsSync10(resolve11(AUTODETECT))) dir = resolve11(AUTODETECT);
|
|
3951
|
+
else {
|
|
3952
|
+
console.error(
|
|
3953
|
+
`Usage: s2s install [name[@range]...] --dir <plugins dir>
|
|
3954
|
+
No --dir given and ./${AUTODETECT} not found. Point --dir at your addons/s2script/plugins directory.`
|
|
3955
|
+
);
|
|
3956
|
+
process.exit(1);
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
const client = new RegistryClient({ baseUrl: registry });
|
|
3960
|
+
try {
|
|
3961
|
+
const manifest = loadManifestFile(resolve11(file));
|
|
3962
|
+
const specs = mergeSpecs(manifest, args);
|
|
3963
|
+
if (Object.keys(specs).length === 0) {
|
|
3964
|
+
console.error(
|
|
3965
|
+
`Nothing to install. Add plugins to ${file} or pass names: s2s install rtv@^1.0.0`
|
|
3966
|
+
);
|
|
3967
|
+
process.exit(1);
|
|
3968
|
+
}
|
|
3969
|
+
const res = await installPlan({
|
|
3970
|
+
client,
|
|
3971
|
+
specs,
|
|
3972
|
+
dir,
|
|
3973
|
+
dryRun,
|
|
3974
|
+
reviewedOnly,
|
|
3975
|
+
log: (m) => console.log(m)
|
|
3976
|
+
});
|
|
3977
|
+
if (dryRun) {
|
|
3978
|
+
console.log("dry run \u2014 nothing written");
|
|
3979
|
+
} else {
|
|
3980
|
+
console.log(
|
|
3981
|
+
`done: ${res.written.length} installed, ${res.skipped.length} up to date, in ${dir}`
|
|
3982
|
+
);
|
|
3983
|
+
}
|
|
3984
|
+
} catch (e) {
|
|
3985
|
+
console.error(String(e instanceof Error ? e.message : e));
|
|
3986
|
+
process.exit(1);
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3496
3990
|
// src/create/create.ts
|
|
3497
3991
|
import {
|
|
3498
|
-
mkdirSync as
|
|
3499
|
-
writeFileSync as
|
|
3500
|
-
existsSync as
|
|
3992
|
+
mkdirSync as mkdirSync5,
|
|
3993
|
+
writeFileSync as writeFileSync7,
|
|
3994
|
+
existsSync as existsSync11,
|
|
3501
3995
|
readdirSync as readdirSync2,
|
|
3502
|
-
readFileSync as
|
|
3996
|
+
readFileSync as readFileSync11,
|
|
3503
3997
|
symlinkSync
|
|
3504
3998
|
} from "node:fs";
|
|
3505
|
-
import { dirname as dirname2, join as
|
|
3999
|
+
import { dirname as dirname2, join as join11, resolve as resolve12, basename as basename3 } from "node:path";
|
|
3506
4000
|
import { spawnSync } from "node:child_process";
|
|
3507
4001
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3508
4002
|
function readCliVersion() {
|
|
3509
4003
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
3510
4004
|
const candidates = [
|
|
3511
|
-
|
|
4005
|
+
join11(here, "..", "package.json"),
|
|
3512
4006
|
// dist/cli.js → packages/sdk/package.json
|
|
3513
|
-
|
|
4007
|
+
join11(here, "..", "..", "package.json")
|
|
3514
4008
|
// src/create → packages/sdk/package.json
|
|
3515
4009
|
];
|
|
3516
4010
|
for (const p of candidates) {
|
|
3517
|
-
if (!
|
|
4011
|
+
if (!existsSync11(p)) continue;
|
|
3518
4012
|
try {
|
|
3519
|
-
const pkg = JSON.parse(
|
|
4013
|
+
const pkg = JSON.parse(readFileSync11(p, "utf8"));
|
|
3520
4014
|
if (pkg.name === "@s2script/sdk" && pkg.version) return pkg.version;
|
|
3521
4015
|
} catch {
|
|
3522
4016
|
}
|
|
@@ -3526,18 +4020,18 @@ function readCliVersion() {
|
|
|
3526
4020
|
function findLocalPackagesDir() {
|
|
3527
4021
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
3528
4022
|
const candidates = [
|
|
3529
|
-
|
|
4023
|
+
join11(here, "..", ".."),
|
|
3530
4024
|
// dist/cli.js → packages
|
|
3531
|
-
|
|
4025
|
+
join11(here, "..", "..", "..")
|
|
3532
4026
|
// src/create → packages
|
|
3533
4027
|
];
|
|
3534
4028
|
for (const c2 of candidates) {
|
|
3535
|
-
if (isPackagesDir(c2)) return
|
|
4029
|
+
if (isPackagesDir(c2)) return resolve12(c2);
|
|
3536
4030
|
}
|
|
3537
4031
|
return void 0;
|
|
3538
4032
|
}
|
|
3539
4033
|
function defaultNameFromPath(dir) {
|
|
3540
|
-
const base =
|
|
4034
|
+
const base = basename3(resolve12(dir));
|
|
3541
4035
|
const slug = base.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "plugin";
|
|
3542
4036
|
return `@plugin/${slug}`;
|
|
3543
4037
|
}
|
|
@@ -3565,18 +4059,18 @@ function versionSpecFrom(status, stdout2) {
|
|
|
3565
4059
|
const v = (stdout2 ?? "").trim();
|
|
3566
4060
|
return status === 0 && /^\d+\.\d+\.\d+/.test(v) ? `^${v}` : "latest";
|
|
3567
4061
|
}
|
|
3568
|
-
function registryDevDeps(game, sdkVersion,
|
|
4062
|
+
function registryDevDeps(game, sdkVersion, resolve13 = resolvePublishedVersion) {
|
|
3569
4063
|
const deps = {};
|
|
3570
4064
|
for (const n3 of createPackageNames(game)) {
|
|
3571
|
-
deps[`@s2script/${n3}`] = n3 === "sdk" ? `^${sdkVersion}` :
|
|
4065
|
+
deps[`@s2script/${n3}`] = n3 === "sdk" ? `^${sdkVersion}` : resolve13(`@s2script/${n3}`);
|
|
3572
4066
|
}
|
|
3573
4067
|
return deps;
|
|
3574
4068
|
}
|
|
3575
4069
|
function fileDevDeps(packagesDir, game) {
|
|
3576
4070
|
const deps = {};
|
|
3577
4071
|
for (const n3 of createPackageNames(game)) {
|
|
3578
|
-
const abs =
|
|
3579
|
-
if (!
|
|
4072
|
+
const abs = join11(packagesDir, n3);
|
|
4073
|
+
if (!existsSync11(join11(abs, "package.json"))) return void 0;
|
|
3580
4074
|
deps[`@s2script/${n3}`] = `file:${abs}`;
|
|
3581
4075
|
}
|
|
3582
4076
|
return deps;
|
|
@@ -3621,7 +4115,12 @@ function tsconfigJson() {
|
|
|
3621
4115
|
return JSON.stringify(
|
|
3622
4116
|
{
|
|
3623
4117
|
compilerOptions: sharedCompilerOptionsJson,
|
|
3624
|
-
|
|
4118
|
+
// `.s2script/gamedata.d.ts` is generated by `s2s build` from a plugin's own gamedata and
|
|
4119
|
+
// augments EngineCalls. It must be in `include` or the EDITOR sees `keyof EngineCalls` as
|
|
4120
|
+
// `never` — every Engine.call() shows as an error while `s2s build` passes, because the build
|
|
4121
|
+
// adds the file to its own program (typecheck.ts) but tsconfig never did. Harmless when the
|
|
4122
|
+
// plugin ships no gamedata: the path simply matches nothing.
|
|
4123
|
+
include: ["src", ".s2script/gamedata.d.ts", "node_modules/@s2script/sdk/globals.d.ts"]
|
|
3625
4124
|
},
|
|
3626
4125
|
null,
|
|
3627
4126
|
2
|
|
@@ -3656,10 +4155,10 @@ dist/
|
|
|
3656
4155
|
`;
|
|
3657
4156
|
}
|
|
3658
4157
|
function linkLocalPackagesForNoInstall(targetPath, localPackagesDir) {
|
|
3659
|
-
const nm =
|
|
3660
|
-
|
|
3661
|
-
const dest =
|
|
3662
|
-
if (!
|
|
4158
|
+
const nm = join11(targetPath, "node_modules");
|
|
4159
|
+
mkdirSync5(nm, { recursive: true });
|
|
4160
|
+
const dest = join11(nm, "@s2script");
|
|
4161
|
+
if (!existsSync11(dest)) symlinkSync(localPackagesDir, dest);
|
|
3663
4162
|
}
|
|
3664
4163
|
function runInstall(dir, pm) {
|
|
3665
4164
|
if (pm === "none") return;
|
|
@@ -3672,7 +4171,7 @@ function runInstall(dir, pm) {
|
|
|
3672
4171
|
if (r2.status !== 0) throw new Error(`${pm} install failed (exit ${r2.status})`);
|
|
3673
4172
|
}
|
|
3674
4173
|
async function createPlugin(opts = {}) {
|
|
3675
|
-
const targetPath =
|
|
4174
|
+
const targetPath = resolve12(opts.path ?? ".");
|
|
3676
4175
|
const interactive = isInteractive({ yes: opts.yes });
|
|
3677
4176
|
let game = assertGame(opts.game);
|
|
3678
4177
|
let name = opts.name;
|
|
@@ -3681,7 +4180,7 @@ async function createPlugin(opts = {}) {
|
|
|
3681
4180
|
if (template !== "minimal") {
|
|
3682
4181
|
throw new Error(`unknown template ${JSON.stringify(template)} (v1 supports: minimal)`);
|
|
3683
4182
|
}
|
|
3684
|
-
if (
|
|
4183
|
+
if (existsSync11(targetPath)) {
|
|
3685
4184
|
const kids = readdirSync2(targetPath);
|
|
3686
4185
|
const meaningful = kids.filter((k) => k !== ".git");
|
|
3687
4186
|
if (meaningful.length) {
|
|
@@ -3734,15 +4233,15 @@ async function createPlugin(opts = {}) {
|
|
|
3734
4233
|
install = install ?? (opts.noInstall ? "none" : "npm");
|
|
3735
4234
|
const version = readCliVersion();
|
|
3736
4235
|
const localPackagesDir = findLocalPackagesDir();
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
4236
|
+
mkdirSync5(join11(targetPath, "src"), { recursive: true });
|
|
4237
|
+
writeFileSync7(
|
|
4238
|
+
join11(targetPath, "package.json"),
|
|
3740
4239
|
packageJsonContent(name, game, version, localPackagesDir)
|
|
3741
4240
|
);
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
4241
|
+
writeFileSync7(join11(targetPath, "tsconfig.json"), tsconfigJson());
|
|
4242
|
+
writeFileSync7(join11(targetPath, "eslint.config.mjs"), eslintConfig());
|
|
4243
|
+
writeFileSync7(join11(targetPath, "src", "plugin.ts"), pluginSource(game));
|
|
4244
|
+
writeFileSync7(join11(targetPath, ".gitignore"), gitignore());
|
|
3746
4245
|
let installed = false;
|
|
3747
4246
|
if (install !== "none") {
|
|
3748
4247
|
const pm = install;
|
|
@@ -3765,7 +4264,7 @@ async function createPlugin(opts = {}) {
|
|
|
3765
4264
|
}
|
|
3766
4265
|
|
|
3767
4266
|
// src/commands/create.ts
|
|
3768
|
-
async function
|
|
4267
|
+
async function run5(argv2) {
|
|
3769
4268
|
const yes = hasFlag(argv2, "--yes") || hasFlag(argv2, "-y");
|
|
3770
4269
|
const interactive = isInteractive({ yes });
|
|
3771
4270
|
const pathArg = argv2.find((a2) => !a2.startsWith("-"));
|
|
@@ -3955,7 +4454,7 @@ async function loginInteractive(opts) {
|
|
|
3955
4454
|
}
|
|
3956
4455
|
|
|
3957
4456
|
// src/commands/login.ts
|
|
3958
|
-
async function
|
|
4457
|
+
async function run6(argv2) {
|
|
3959
4458
|
const interactive = isInteractive({ ci: hasFlag(argv2, "--ci") });
|
|
3960
4459
|
try {
|
|
3961
4460
|
const creds = await loginInteractive({
|
|
@@ -3974,8 +4473,8 @@ async function run5(argv2) {
|
|
|
3974
4473
|
}
|
|
3975
4474
|
|
|
3976
4475
|
// src/config/gen.ts
|
|
3977
|
-
import { readFileSync as
|
|
3978
|
-
import { join as
|
|
4476
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6 } from "node:fs";
|
|
4477
|
+
import { join as join12 } from "node:path";
|
|
3979
4478
|
|
|
3980
4479
|
// src/config/config-path.ts
|
|
3981
4480
|
function configFileName(id) {
|
|
@@ -4021,13 +4520,13 @@ function genConfigFromManifest(manifest, outDir) {
|
|
|
4021
4520
|
}
|
|
4022
4521
|
const id = typeof manifest.id === "string" ? manifest.id : "";
|
|
4023
4522
|
if (!id) throw new Error("config gen: manifest declares config but has no id");
|
|
4024
|
-
|
|
4025
|
-
const path =
|
|
4026
|
-
|
|
4523
|
+
mkdirSync6(outDir, { recursive: true });
|
|
4524
|
+
const path = join12(outDir, configFileName(id));
|
|
4525
|
+
writeFileSync8(path, generateDefaultJsonc(config));
|
|
4027
4526
|
return path;
|
|
4028
4527
|
}
|
|
4029
4528
|
function genConfigForS2sp(s2spPath, outDir) {
|
|
4030
|
-
const entry = unzipSync(
|
|
4529
|
+
const entry = unzipSync(readFileSync12(s2spPath))["manifest.json"];
|
|
4031
4530
|
if (!entry) throw new Error(`config gen: ${s2spPath} has no manifest.json`);
|
|
4032
4531
|
const manifest = JSON.parse(Buffer.from(entry).toString("utf8"));
|
|
4033
4532
|
return genConfigFromManifest(manifest, outDir);
|
|
@@ -4044,7 +4543,7 @@ function runConfigGen(s2sps, outDir) {
|
|
|
4044
4543
|
}
|
|
4045
4544
|
|
|
4046
4545
|
// src/commands/config.ts
|
|
4047
|
-
async function
|
|
4546
|
+
async function run7(argv2) {
|
|
4048
4547
|
const rest = argv2[0] === "gen" ? argv2.slice(1) : argv2;
|
|
4049
4548
|
const interactive = isInteractive();
|
|
4050
4549
|
const outDir = parseFlag(rest, "--out") ?? process.cwd();
|
|
@@ -4074,12 +4573,12 @@ async function run6(argv2) {
|
|
|
4074
4573
|
}
|
|
4075
4574
|
|
|
4076
4575
|
// src/commands/codegen.ts
|
|
4077
|
-
import { dirname as dirname3, join as
|
|
4576
|
+
import { dirname as dirname3, join as join16 } from "node:path";
|
|
4078
4577
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
4079
4578
|
|
|
4080
4579
|
// src/schemagen/gen.ts
|
|
4081
|
-
import { readFileSync as
|
|
4082
|
-
import { join as
|
|
4580
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
|
|
4581
|
+
import { join as join13 } from "node:path";
|
|
4083
4582
|
|
|
4084
4583
|
// src/schemagen/model.ts
|
|
4085
4584
|
var READ = {
|
|
@@ -4271,21 +4770,21 @@ function emitJs(model) {
|
|
|
4271
4770
|
for (const c2 of model.classes) {
|
|
4272
4771
|
out.push(` A[${S(c2.className)}] = {`);
|
|
4273
4772
|
for (const f of flattenedFields(model, c2.className)) {
|
|
4274
|
-
const
|
|
4773
|
+
const resolve13 = `off(${S(f.declaringClass)},${S(f.rawName)})`;
|
|
4275
4774
|
let entry;
|
|
4276
4775
|
if (f.accessorKind === "str") {
|
|
4277
|
-
entry = `get: function () { return this.ref.readString(${
|
|
4776
|
+
entry = `get: function () { return this.ref.readString(${resolve13}, ${f.strLen}); }`;
|
|
4278
4777
|
} else if (f.accessorKind === "u64" || f.accessorKind === "i64") {
|
|
4279
|
-
entry = `get: function () { var v = this.ref.${READ[f.accessorKind]}(${
|
|
4778
|
+
entry = `get: function () { var v = this.ref.${READ[f.accessorKind]}(${resolve13}); return v === null ? null : v.toString(); }`;
|
|
4280
4779
|
} else if (VEC_INFO[f.accessorKind]) {
|
|
4281
4780
|
const vi = VEC_INFO[f.accessorKind];
|
|
4282
|
-
entry = `get: function () { var a = this.ref.readFloats(${
|
|
4781
|
+
entry = `get: function () { var a = this.ref.readFloats(${resolve13}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
|
|
4283
4782
|
} else {
|
|
4284
|
-
entry = `get: function () { return this.ref.${READ[f.accessorKind]}(${
|
|
4783
|
+
entry = `get: function () { return this.ref.${READ[f.accessorKind]}(${resolve13}); }`;
|
|
4285
4784
|
}
|
|
4286
4785
|
if (f.writable) {
|
|
4287
4786
|
const wm = WRITE[f.accessorKind];
|
|
4288
|
-
entry += `, set: function (v) { var o = ${
|
|
4787
|
+
entry += `, set: function (v) { var o = ${resolve13}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(o); }`;
|
|
4289
4788
|
}
|
|
4290
4789
|
out.push(` ${S(f.propName)}: { ${entry} },`);
|
|
4291
4790
|
}
|
|
@@ -4311,8 +4810,8 @@ var LIST_PATH = "games/cs2/codegen-classes.json";
|
|
|
4311
4810
|
var JS_OUT = "games/cs2/js/schema.generated.js";
|
|
4312
4811
|
var DTS_OUT = "packages/cs2/schema.generated.d.ts";
|
|
4313
4812
|
function runGenSchema(repoRoot2, opts) {
|
|
4314
|
-
const catalog = JSON.parse(
|
|
4315
|
-
const list = JSON.parse(
|
|
4813
|
+
const catalog = JSON.parse(readFileSync13(join13(repoRoot2, CATALOG_PATH), "utf8"));
|
|
4814
|
+
const list = JSON.parse(readFileSync13(join13(repoRoot2, LIST_PATH), "utf8"));
|
|
4316
4815
|
const model = buildModel(catalog, list);
|
|
4317
4816
|
const dts = emitDts(model);
|
|
4318
4817
|
const js = emitJs(model);
|
|
@@ -4321,16 +4820,16 @@ function runGenSchema(repoRoot2, opts) {
|
|
|
4321
4820
|
const drift = [];
|
|
4322
4821
|
const files = [[JS_OUT, js], [DTS_OUT, dts]];
|
|
4323
4822
|
for (const [rel, content] of files) {
|
|
4324
|
-
const abs =
|
|
4823
|
+
const abs = join13(repoRoot2, rel);
|
|
4325
4824
|
if (opts.check) {
|
|
4326
4825
|
let cur = "";
|
|
4327
4826
|
try {
|
|
4328
|
-
cur =
|
|
4827
|
+
cur = readFileSync13(abs, "utf8");
|
|
4329
4828
|
} catch {
|
|
4330
4829
|
}
|
|
4331
4830
|
if (cur !== content) drift.push(rel);
|
|
4332
4831
|
} else {
|
|
4333
|
-
|
|
4832
|
+
writeFileSync9(abs, content);
|
|
4334
4833
|
}
|
|
4335
4834
|
}
|
|
4336
4835
|
if (model.collisions.length) console.error(`gen-schema: ${model.collisions.length} name collision(s) \u2192 raw fallback:
|
|
@@ -4340,8 +4839,8 @@ function runGenSchema(repoRoot2, opts) {
|
|
|
4340
4839
|
}
|
|
4341
4840
|
|
|
4342
4841
|
// src/eventgen/gen.ts
|
|
4343
|
-
import { readFileSync as
|
|
4344
|
-
import { join as
|
|
4842
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "node:fs";
|
|
4843
|
+
import { join as join14 } from "node:path";
|
|
4345
4844
|
|
|
4346
4845
|
// src/eventgen/model.ts
|
|
4347
4846
|
var TYPE_TO_GETTER = {
|
|
@@ -4426,27 +4925,27 @@ function emitEventDts(model) {
|
|
|
4426
4925
|
var CATALOG_PATH2 = "games/cs2/gamedata/event-catalog.json";
|
|
4427
4926
|
var DTS_OUT2 = "packages/cs2/events.generated.d.ts";
|
|
4428
4927
|
function runGenEvents(repoRoot2, opts) {
|
|
4429
|
-
const catalog = JSON.parse(
|
|
4928
|
+
const catalog = JSON.parse(readFileSync14(join14(repoRoot2, CATALOG_PATH2), "utf8"));
|
|
4430
4929
|
const model = buildEventModel(catalog);
|
|
4431
4930
|
const dts = emitEventDts(model);
|
|
4432
4931
|
const drift = [];
|
|
4433
|
-
const abs =
|
|
4932
|
+
const abs = join14(repoRoot2, DTS_OUT2);
|
|
4434
4933
|
if (opts.check) {
|
|
4435
4934
|
let cur = "";
|
|
4436
4935
|
try {
|
|
4437
|
-
cur =
|
|
4936
|
+
cur = readFileSync14(abs, "utf8");
|
|
4438
4937
|
} catch {
|
|
4439
4938
|
}
|
|
4440
4939
|
if (cur !== dts) drift.push(DTS_OUT2);
|
|
4441
4940
|
} else {
|
|
4442
|
-
|
|
4941
|
+
writeFileSync10(abs, dts);
|
|
4443
4942
|
}
|
|
4444
4943
|
return { events: model.length, drift };
|
|
4445
4944
|
}
|
|
4446
4945
|
|
|
4447
4946
|
// src/navgen/gen.ts
|
|
4448
|
-
import { readFileSync as
|
|
4449
|
-
import { join as
|
|
4947
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11 } from "node:fs";
|
|
4948
|
+
import { join as join15 } from "node:path";
|
|
4450
4949
|
|
|
4451
4950
|
// src/navgen/model.ts
|
|
4452
4951
|
var SUPPORTED_NAV_KINDS = /* @__PURE__ */ new Set([
|
|
@@ -4521,22 +5020,22 @@ function emitNavJs(model) {
|
|
|
4521
5020
|
out.push(` function ${w.wrapper}(root, path) { this.root = root; this.path = path; }`);
|
|
4522
5021
|
out.push(` Object.defineProperties(${w.wrapper}.prototype, {`);
|
|
4523
5022
|
for (const f of w.fields) {
|
|
4524
|
-
const
|
|
5023
|
+
const resolve13 = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
|
|
4525
5024
|
let getter;
|
|
4526
5025
|
const kind = f.accessorKind;
|
|
4527
5026
|
if (kind === "u64") {
|
|
4528
|
-
getter = `get: function () { var v = this.root.readUInt64Via(this.path, ${
|
|
5027
|
+
getter = `get: function () { var v = this.root.readUInt64Via(this.path, ${resolve13}); return v === null ? null : v.toString(); }`;
|
|
4529
5028
|
} else if (kind === "i64") {
|
|
4530
|
-
getter = `get: function () { var v = this.root.readInt64Via(this.path, ${
|
|
5029
|
+
getter = `get: function () { var v = this.root.readInt64Via(this.path, ${resolve13}); return v === null ? null : v.toString(); }`;
|
|
4531
5030
|
} else if (VEC_INFO[kind]) {
|
|
4532
5031
|
const vi = VEC_INFO[kind];
|
|
4533
|
-
getter = `get: function () { var a = this.root.readFloatsChain(this.path, ${
|
|
5032
|
+
getter = `get: function () { var a = this.root.readFloatsChain(this.path, ${resolve13}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
|
|
4534
5033
|
} else if (kind === "handle") {
|
|
4535
|
-
getter = `get: function () { return this.root.readHandleVia(this.path, ${
|
|
5034
|
+
getter = `get: function () { return this.root.readHandleVia(this.path, ${resolve13}); }`;
|
|
4536
5035
|
} else {
|
|
4537
5036
|
const method = READ_VIA[kind];
|
|
4538
5037
|
if (!method) continue;
|
|
4539
|
-
getter = `get: function () { return this.root.${method}(this.path, ${
|
|
5038
|
+
getter = `get: function () { return this.root.${method}(this.path, ${resolve13}); }`;
|
|
4540
5039
|
}
|
|
4541
5040
|
out.push(` ${S2(f.propName)}: { ${getter} },`);
|
|
4542
5041
|
}
|
|
@@ -4608,8 +5107,8 @@ var CATALOG_PATH3 = "games/cs2/gamedata/schema-catalog.json";
|
|
|
4608
5107
|
var JS_OUT2 = "games/cs2/js/nav.generated.js";
|
|
4609
5108
|
var DTS_OUT3 = "packages/cs2/nav.generated.d.ts";
|
|
4610
5109
|
function runGenNav(repoRoot2, opts) {
|
|
4611
|
-
const config = JSON.parse(
|
|
4612
|
-
const catalog = JSON.parse(
|
|
5110
|
+
const config = JSON.parse(readFileSync15(join15(repoRoot2, TARGETS_PATH), "utf8"));
|
|
5111
|
+
const catalog = JSON.parse(readFileSync15(join15(repoRoot2, CATALOG_PATH3), "utf8"));
|
|
4613
5112
|
const model = buildNavModel(config, catalog);
|
|
4614
5113
|
const js = emitNavJs(model);
|
|
4615
5114
|
const dts = emitNavDts(model);
|
|
@@ -4617,16 +5116,16 @@ function runGenNav(repoRoot2, opts) {
|
|
|
4617
5116
|
const drift = [];
|
|
4618
5117
|
const files = [[JS_OUT2, js], [DTS_OUT3, dts]];
|
|
4619
5118
|
for (const [rel, content] of files) {
|
|
4620
|
-
const abs =
|
|
5119
|
+
const abs = join15(repoRoot2, rel);
|
|
4621
5120
|
if (opts.check) {
|
|
4622
5121
|
let cur = "";
|
|
4623
5122
|
try {
|
|
4624
|
-
cur =
|
|
5123
|
+
cur = readFileSync15(abs, "utf8");
|
|
4625
5124
|
} catch {
|
|
4626
5125
|
}
|
|
4627
5126
|
if (cur !== content) drift.push(rel);
|
|
4628
5127
|
} else {
|
|
4629
|
-
|
|
5128
|
+
writeFileSync11(abs, content);
|
|
4630
5129
|
}
|
|
4631
5130
|
}
|
|
4632
5131
|
return { wrappers: model.wrappers.length, fields, drift };
|
|
@@ -4634,9 +5133,9 @@ function runGenNav(repoRoot2, opts) {
|
|
|
4634
5133
|
|
|
4635
5134
|
// src/commands/codegen.ts
|
|
4636
5135
|
function repoRoot() {
|
|
4637
|
-
return
|
|
5136
|
+
return join16(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "..");
|
|
4638
5137
|
}
|
|
4639
|
-
async function
|
|
5138
|
+
async function run8(kind, argv2) {
|
|
4640
5139
|
const root = repoRoot();
|
|
4641
5140
|
const check = argv2[0] === "--check";
|
|
4642
5141
|
if (kind === "schema") {
|
|
@@ -4680,15 +5179,16 @@ async function run7(kind, argv2) {
|
|
|
4680
5179
|
|
|
4681
5180
|
// src/commands/index.ts
|
|
4682
5181
|
var COMMANDS = [
|
|
4683
|
-
{ name: "create", summary: "Scaffold a new plugin", run:
|
|
5182
|
+
{ name: "create", summary: "Scaffold a new plugin", run: run5 },
|
|
4684
5183
|
{ name: "build", summary: "Build a plugin to a .s2sp", run },
|
|
4685
5184
|
{ name: "deploy", summary: "Publish a plugin to the registry", run: run2 },
|
|
4686
5185
|
{ name: "add", summary: "Add a registry package's types", run: run3 },
|
|
4687
|
-
{ name: "
|
|
4688
|
-
{ name: "
|
|
4689
|
-
{ name: "
|
|
4690
|
-
{ name: "gen-
|
|
4691
|
-
{ name: "gen-
|
|
5186
|
+
{ name: "install", summary: "Download plugins + their deps into a server", run: run4 },
|
|
5187
|
+
{ name: "login", summary: "Save a registry deploy token", run: run6 },
|
|
5188
|
+
{ name: "config", summary: "Emit a plugin's default config file(s)", run: run7 },
|
|
5189
|
+
{ name: "gen-schema", summary: "Regenerate schema accessors", run: (a2) => run8("schema", a2) },
|
|
5190
|
+
{ name: "gen-events", summary: "Regenerate the event catalog", run: (a2) => run8("events", a2) },
|
|
5191
|
+
{ name: "gen-nav", summary: "Regenerate nav accessors", run: (a2) => run8("nav", a2) }
|
|
4692
5192
|
];
|
|
4693
5193
|
function find(name) {
|
|
4694
5194
|
if (name === "publish") return COMMANDS.find((c2) => c2.name === "deploy");
|
|
@@ -4696,7 +5196,7 @@ function find(name) {
|
|
|
4696
5196
|
}
|
|
4697
5197
|
|
|
4698
5198
|
// src/commands/menu.ts
|
|
4699
|
-
async function
|
|
5199
|
+
async function run9() {
|
|
4700
5200
|
intro2("s2script");
|
|
4701
5201
|
const name = await select2({
|
|
4702
5202
|
message: "What would you like to do?",
|
|
@@ -4711,12 +5211,12 @@ var argv = process.argv.slice(2);
|
|
|
4711
5211
|
var command = argv[0];
|
|
4712
5212
|
function usage() {
|
|
4713
5213
|
console.error(
|
|
4714
|
-
"Usage:\n s2s create [path] [--game cs2|none] [--name <pkg>] [--template minimal]\n [--install npm|pnpm|yarn|bun|none] [--no-install] [-y]\n s2s build <dir> [--packages-dir <path>]\n s2s login [--token s2s_\u2026] [--registry <url>]\n s2s deploy [dir] [--ci] [--registry <url>] [--packages-dir <path>]\n s2s add <pkg>[@range] [--dir <plugin>] [--registry <url>]\n s2s config gen <plugin.s2sp...> --out <dir>\n s2s gen-schema [--check]\n s2s gen-events [--check]\n s2s gen-nav [--check]\n\nEnv: S2SCRIPT_REGISTRY_URL S2SCRIPT_TOKEN (CI deploy)"
|
|
5214
|
+
"Usage:\n s2s create [path] [--game cs2|none] [--name <pkg>] [--template minimal]\n [--install npm|pnpm|yarn|bun|none] [--no-install] [-y]\n s2s build <dir> [--packages-dir <path>]\n s2s login [--token s2s_\u2026] [--registry <url>]\n s2s deploy [dir] [--ci] [--registry <url>] [--packages-dir <path>]\n s2s add <pkg>[@range] [--dir <plugin>] [--registry <url>]\n s2s install [name[@range]...] [--file <path>] [--dir <plugins dir>] [--registry <url>]\n s2s config gen <plugin.s2sp...> --out <dir>\n s2s gen-schema [--check]\n s2s gen-events [--check]\n s2s gen-nav [--check]\n\nEnv: S2SCRIPT_REGISTRY_URL S2SCRIPT_TOKEN (CI deploy)"
|
|
4715
5215
|
);
|
|
4716
5216
|
}
|
|
4717
5217
|
if (!command) {
|
|
4718
5218
|
if (isInteractive()) {
|
|
4719
|
-
await
|
|
5219
|
+
await run9();
|
|
4720
5220
|
} else {
|
|
4721
5221
|
usage();
|
|
4722
5222
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@s2script/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"license": "MIT OR Apache-2.0",
|
|
5
5
|
"description": "s2script SDK — the builtin capability types (imported as @s2script/sdk/entity, …) plus the `s2s` CLI for building Source 2 / CS2 plugins.",
|
|
6
6
|
"type": "module",
|
|
@@ -111,6 +111,9 @@
|
|
|
111
111
|
"./transmit": {
|
|
112
112
|
"types": "./transmit.d.ts"
|
|
113
113
|
},
|
|
114
|
+
"./unsafe": {
|
|
115
|
+
"types": "./unsafe.d.ts"
|
|
116
|
+
},
|
|
114
117
|
"./usercmd": {
|
|
115
118
|
"types": "./usercmd.d.ts"
|
|
116
119
|
},
|
package/unsafe.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @s2script/sdk/unsafe — plugin-declared engine calls. NO runtime code: the engine injects the
|
|
3
|
+
* implementation at load (`__s2pkg_unsafe`).
|
|
4
|
+
*
|
|
5
|
+
* UNSAFE by design. A declared call reaches a real engine function. The framework validates every
|
|
6
|
+
* descriptor at load and degrades a failure to `null`, but it cannot prove your signature or vtable
|
|
7
|
+
* index names the function you meant. Requires manifest `permissions: ["engine:calls"]` AND an
|
|
8
|
+
* operator allow-list entry (see docs/superpowers/specs/2026-07-24-plugin-gamedata-design.md §6).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The calls this plugin declares. The base interface is EMPTY — `s2s build` generates
|
|
13
|
+
* `.s2script/gamedata.d.ts`, which augments this from your gamedata's `calls` section.
|
|
14
|
+
*/
|
|
15
|
+
export interface EngineCalls {}
|
|
16
|
+
|
|
17
|
+
export declare const Engine: {
|
|
18
|
+
/**
|
|
19
|
+
* The declared call, or `null` when its descriptor failed a load-time gate (signature miss,
|
|
20
|
+
* validator rejection, slot outside `.text`, missing platform entry, or the plugin is not
|
|
21
|
+
* operator-allow-listed). Guard once at load; the returned function is a plain callable.
|
|
22
|
+
*/
|
|
23
|
+
call<K extends keyof EngineCalls>(name: K): EngineCalls[K] | null;
|
|
24
|
+
/** Why a descriptor is unavailable; `"available"` when it resolved. For diagnostics/operator reports. */
|
|
25
|
+
status(name: string): string;
|
|
26
|
+
};
|