@s2script/sdk 0.7.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/chat.d.ts +7 -2
- package/dist/cli.js +846 -144
- 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)) {
|
|
@@ -3075,8 +3366,15 @@ import { join as join7 } from "node:path";
|
|
|
3075
3366
|
function credentialsPath() {
|
|
3076
3367
|
return join7(homedir(), ".s2script", "credentials.json");
|
|
3077
3368
|
}
|
|
3369
|
+
function normalizeRegistryUrl(url) {
|
|
3370
|
+
const trimmed = url.replace(/\/$/, "");
|
|
3371
|
+
if (trimmed === "https://s2script.com") return "https://www.s2script.com";
|
|
3372
|
+
return trimmed;
|
|
3373
|
+
}
|
|
3078
3374
|
function defaultRegistryUrl() {
|
|
3079
|
-
|
|
3375
|
+
const fromEnv = process.env.S2SCRIPT_REGISTRY_URL;
|
|
3376
|
+
if (fromEnv) return normalizeRegistryUrl(fromEnv);
|
|
3377
|
+
return "https://www.s2script.com";
|
|
3080
3378
|
}
|
|
3081
3379
|
function loadCredentials() {
|
|
3082
3380
|
const fromEnv = process.env.S2SCRIPT_TOKEN || process.env.S2SCRIPT_DEPLOY_TOKEN;
|
|
@@ -3089,7 +3387,7 @@ function loadCredentials() {
|
|
|
3089
3387
|
const j = JSON.parse(readFileSync6(p, "utf8"));
|
|
3090
3388
|
if (!j.token) return null;
|
|
3091
3389
|
return {
|
|
3092
|
-
registryUrl: (j.registryUrl || defaultRegistryUrl())
|
|
3390
|
+
registryUrl: normalizeRegistryUrl(j.registryUrl || defaultRegistryUrl()),
|
|
3093
3391
|
token: j.token
|
|
3094
3392
|
};
|
|
3095
3393
|
} catch {
|
|
@@ -3102,7 +3400,7 @@ function saveCredentials(creds) {
|
|
|
3102
3400
|
writeFileSync3(
|
|
3103
3401
|
credentialsPath(),
|
|
3104
3402
|
JSON.stringify(
|
|
3105
|
-
{ registryUrl: creds.registryUrl
|
|
3403
|
+
{ registryUrl: normalizeRegistryUrl(creds.registryUrl), token: creds.token },
|
|
3106
3404
|
null,
|
|
3107
3405
|
2
|
|
3108
3406
|
) + "\n",
|
|
@@ -3112,13 +3410,64 @@ function saveCredentials(creds) {
|
|
|
3112
3410
|
|
|
3113
3411
|
// src/registry/client.ts
|
|
3114
3412
|
var RegistryError = class extends Error {
|
|
3413
|
+
// Fields are declared/assigned explicitly rather than as constructor parameter
|
|
3414
|
+
// properties: the test runner loads src/*.ts under node's strip-only type
|
|
3415
|
+
// stripping, which rejects `constructor(public status: number)`.
|
|
3115
3416
|
constructor(message, status, body) {
|
|
3116
3417
|
super(message);
|
|
3418
|
+
this.name = "RegistryError";
|
|
3117
3419
|
this.status = status;
|
|
3118
3420
|
this.body = body;
|
|
3119
|
-
this.name = "RegistryError";
|
|
3120
3421
|
}
|
|
3121
3422
|
};
|
|
3423
|
+
var TIMEOUTS_MS = {
|
|
3424
|
+
deploy: 3e5,
|
|
3425
|
+
// up to ~40 MiB on a slow uplink
|
|
3426
|
+
resolve: 3e4,
|
|
3427
|
+
meta: 3e4,
|
|
3428
|
+
plan: 3e4,
|
|
3429
|
+
"types download": 12e4,
|
|
3430
|
+
"s2sp download": 12e4
|
|
3431
|
+
};
|
|
3432
|
+
function rootCause(err2) {
|
|
3433
|
+
let cur = err2;
|
|
3434
|
+
for (let i2 = 0; i2 < 5 && cur instanceof Error && cur.cause; i2++) cur = cur.cause;
|
|
3435
|
+
return cur instanceof Error ? cur.message : String(cur);
|
|
3436
|
+
}
|
|
3437
|
+
function hintFor(status, raw) {
|
|
3438
|
+
if (status === 401) return "run `s2s login` (or set S2SCRIPT_TOKEN)";
|
|
3439
|
+
if (status === 403 && /cross-site/i.test(raw)) {
|
|
3440
|
+
return "the request was rejected before it reached the registry API \u2014 this s2s CLI and the server are incompatible; run: npm i -g @s2script/sdk@latest";
|
|
3441
|
+
}
|
|
3442
|
+
if (status === 403) return "the token may lack permission for this package, or belong to another account \u2014 check `s2s login`";
|
|
3443
|
+
if (status === 413) return "the plugin or its types exceed the registry size limits \u2014 trim the archive and retry";
|
|
3444
|
+
if (status === 415) return "the server rejected this CLI's request format; run: npm i -g @s2script/sdk@latest";
|
|
3445
|
+
if (status === 429) return "rate limited \u2014 wait a minute and retry";
|
|
3446
|
+
if (status >= 500) return "server-side failure \u2014 retry in a moment; if it persists, report it with the message above";
|
|
3447
|
+
return "";
|
|
3448
|
+
}
|
|
3449
|
+
async function errorFromResponse(res, op) {
|
|
3450
|
+
const raw = await res.text().catch(() => "");
|
|
3451
|
+
let body;
|
|
3452
|
+
let detail;
|
|
3453
|
+
try {
|
|
3454
|
+
body = JSON.parse(raw);
|
|
3455
|
+
const b = body;
|
|
3456
|
+
if (b && typeof b.error === "string") detail = b.error;
|
|
3457
|
+
else if (b && typeof b.message === "string") detail = b.message;
|
|
3458
|
+
} catch {
|
|
3459
|
+
}
|
|
3460
|
+
if (!detail) {
|
|
3461
|
+
if (/^\s*</.test(raw)) detail = "server returned an HTML error page";
|
|
3462
|
+
else if (raw.trim()) detail = raw.trim().slice(0, 200);
|
|
3463
|
+
else detail = res.statusText || "no response body";
|
|
3464
|
+
}
|
|
3465
|
+
let msg = `${op} failed (HTTP ${res.status}): ${detail}`;
|
|
3466
|
+
const hint = hintFor(res.status, raw);
|
|
3467
|
+
if (hint) msg += `
|
|
3468
|
+
hint: ${hint}`;
|
|
3469
|
+
return new RegistryError(msg, res.status, body ?? raw);
|
|
3470
|
+
}
|
|
3122
3471
|
var RegistryClient = class {
|
|
3123
3472
|
constructor(opts) {
|
|
3124
3473
|
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
@@ -3130,67 +3479,94 @@ var RegistryClient = class {
|
|
|
3130
3479
|
if (this.token) h2.Authorization = `Bearer ${this.token}`;
|
|
3131
3480
|
return h2;
|
|
3132
3481
|
}
|
|
3482
|
+
async request(op, url, init) {
|
|
3483
|
+
const timeoutMs = TIMEOUTS_MS[op];
|
|
3484
|
+
const host = new URL(String(url)).host;
|
|
3485
|
+
let res;
|
|
3486
|
+
try {
|
|
3487
|
+
res = await this.fetchFn(url, {
|
|
3488
|
+
method: init?.method ?? "GET",
|
|
3489
|
+
// Cast: a Uint8Array is a valid BodyInit at runtime, but the ambient
|
|
3490
|
+
// fetch typings here only accept ArrayBuffer-backed views.
|
|
3491
|
+
body: init?.body,
|
|
3492
|
+
headers: this.headers(init?.contentType ? { "content-type": init.contentType } : void 0),
|
|
3493
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3494
|
+
});
|
|
3495
|
+
} catch (e) {
|
|
3496
|
+
const name = e instanceof Error ? e.name : "";
|
|
3497
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
3498
|
+
throw new RegistryError(
|
|
3499
|
+
`${op}: timed out after ${timeoutMs / 1e3}s talking to ${host}
|
|
3500
|
+
hint: check your connection or set S2SCRIPT_REGISTRY_URL if you meant a different registry`,
|
|
3501
|
+
0
|
|
3502
|
+
);
|
|
3503
|
+
}
|
|
3504
|
+
throw new RegistryError(
|
|
3505
|
+
`${op}: network error talking to ${host} \u2014 ${rootCause(e)}
|
|
3506
|
+
hint: check your connection, proxy, and that ${host} is reachable`,
|
|
3507
|
+
0,
|
|
3508
|
+
e
|
|
3509
|
+
);
|
|
3510
|
+
}
|
|
3511
|
+
if (!res.ok) throw await errorFromResponse(res, op);
|
|
3512
|
+
return res;
|
|
3513
|
+
}
|
|
3514
|
+
async json(res, op) {
|
|
3515
|
+
try {
|
|
3516
|
+
return await res.json();
|
|
3517
|
+
} catch {
|
|
3518
|
+
throw new RegistryError(`${op}: server returned invalid JSON (HTTP ${res.status})`, res.status);
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3133
3521
|
async deploy(opts) {
|
|
3134
|
-
const
|
|
3135
|
-
|
|
3136
|
-
|
|
3522
|
+
const entries = {
|
|
3523
|
+
"manifest.json": new TextEncoder().encode(JSON.stringify(opts.manifest)),
|
|
3524
|
+
"plugin.s2sp": new Uint8Array(opts.s2sp)
|
|
3525
|
+
};
|
|
3137
3526
|
if (opts.types && opts.types.length) {
|
|
3138
|
-
|
|
3527
|
+
entries["types.tgz"] = new Uint8Array(opts.types);
|
|
3139
3528
|
}
|
|
3140
|
-
const
|
|
3529
|
+
const body = zipSync(entries, { level: 0 });
|
|
3530
|
+
const res = await this.request("deploy", `${this.baseUrl}/api/v1/deploy`, {
|
|
3141
3531
|
method: "POST",
|
|
3142
|
-
|
|
3143
|
-
|
|
3532
|
+
body,
|
|
3533
|
+
contentType: "application/octet-stream"
|
|
3144
3534
|
});
|
|
3145
|
-
|
|
3146
|
-
if (!res.ok) {
|
|
3147
|
-
throw new RegistryError(
|
|
3148
|
-
body.error || `deploy failed (${res.status})`,
|
|
3149
|
-
res.status,
|
|
3150
|
-
body
|
|
3151
|
-
);
|
|
3152
|
-
}
|
|
3153
|
-
return body;
|
|
3535
|
+
return this.json(res, "deploy");
|
|
3154
3536
|
}
|
|
3155
3537
|
async resolve(name, range = "*") {
|
|
3156
3538
|
const u4 = new URL(`${this.baseUrl}/api/v1/resolve`);
|
|
3157
3539
|
u4.searchParams.set("name", name);
|
|
3158
3540
|
u4.searchParams.set("range", range);
|
|
3159
|
-
const res = await this.
|
|
3160
|
-
|
|
3161
|
-
if (!res.ok) {
|
|
3162
|
-
throw new RegistryError(
|
|
3163
|
-
body.message || `resolve failed (${res.status})`,
|
|
3164
|
-
res.status,
|
|
3165
|
-
body
|
|
3166
|
-
);
|
|
3167
|
-
}
|
|
3168
|
-
return body;
|
|
3541
|
+
const res = await this.request("resolve", u4);
|
|
3542
|
+
return this.json(res, "resolve");
|
|
3169
3543
|
}
|
|
3170
3544
|
async downloadTypes(name, version) {
|
|
3171
3545
|
const u4 = new URL(`${this.baseUrl}/api/v1/download/types`);
|
|
3172
3546
|
u4.searchParams.set("name", name);
|
|
3173
3547
|
u4.searchParams.set("version", version);
|
|
3174
|
-
const res = await this.
|
|
3175
|
-
if (!res.ok) {
|
|
3176
|
-
const body = await res.json().catch(() => ({}));
|
|
3177
|
-
throw new RegistryError(
|
|
3178
|
-
body.message || `types download failed (${res.status})`,
|
|
3179
|
-
res.status,
|
|
3180
|
-
body
|
|
3181
|
-
);
|
|
3182
|
-
}
|
|
3548
|
+
const res = await this.request("types download", u4);
|
|
3183
3549
|
return Buffer.from(await res.arrayBuffer());
|
|
3184
3550
|
}
|
|
3185
3551
|
async meta(name) {
|
|
3186
3552
|
const u4 = new URL(`${this.baseUrl}/api/v1/meta`);
|
|
3187
3553
|
u4.searchParams.set("name", name);
|
|
3188
|
-
const res = await this.
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
}
|
|
3193
|
-
|
|
3554
|
+
const res = await this.request("meta", u4);
|
|
3555
|
+
return this.json(res, "meta");
|
|
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());
|
|
3194
3570
|
}
|
|
3195
3571
|
};
|
|
3196
3572
|
|
|
@@ -3424,30 +3800,217 @@ async function run3(argv2) {
|
|
|
3424
3800
|
}
|
|
3425
3801
|
}
|
|
3426
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
|
+
|
|
3427
3990
|
// src/create/create.ts
|
|
3428
3991
|
import {
|
|
3429
|
-
mkdirSync as
|
|
3430
|
-
writeFileSync as
|
|
3431
|
-
existsSync as
|
|
3992
|
+
mkdirSync as mkdirSync5,
|
|
3993
|
+
writeFileSync as writeFileSync7,
|
|
3994
|
+
existsSync as existsSync11,
|
|
3432
3995
|
readdirSync as readdirSync2,
|
|
3433
|
-
readFileSync as
|
|
3996
|
+
readFileSync as readFileSync11,
|
|
3434
3997
|
symlinkSync
|
|
3435
3998
|
} from "node:fs";
|
|
3436
|
-
import { dirname as dirname2, join as
|
|
3999
|
+
import { dirname as dirname2, join as join11, resolve as resolve12, basename as basename3 } from "node:path";
|
|
3437
4000
|
import { spawnSync } from "node:child_process";
|
|
3438
4001
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3439
4002
|
function readCliVersion() {
|
|
3440
4003
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
3441
4004
|
const candidates = [
|
|
3442
|
-
|
|
4005
|
+
join11(here, "..", "package.json"),
|
|
3443
4006
|
// dist/cli.js → packages/sdk/package.json
|
|
3444
|
-
|
|
4007
|
+
join11(here, "..", "..", "package.json")
|
|
3445
4008
|
// src/create → packages/sdk/package.json
|
|
3446
4009
|
];
|
|
3447
4010
|
for (const p of candidates) {
|
|
3448
|
-
if (!
|
|
4011
|
+
if (!existsSync11(p)) continue;
|
|
3449
4012
|
try {
|
|
3450
|
-
const pkg = JSON.parse(
|
|
4013
|
+
const pkg = JSON.parse(readFileSync11(p, "utf8"));
|
|
3451
4014
|
if (pkg.name === "@s2script/sdk" && pkg.version) return pkg.version;
|
|
3452
4015
|
} catch {
|
|
3453
4016
|
}
|
|
@@ -3457,18 +4020,18 @@ function readCliVersion() {
|
|
|
3457
4020
|
function findLocalPackagesDir() {
|
|
3458
4021
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
3459
4022
|
const candidates = [
|
|
3460
|
-
|
|
4023
|
+
join11(here, "..", ".."),
|
|
3461
4024
|
// dist/cli.js → packages
|
|
3462
|
-
|
|
4025
|
+
join11(here, "..", "..", "..")
|
|
3463
4026
|
// src/create → packages
|
|
3464
4027
|
];
|
|
3465
4028
|
for (const c2 of candidates) {
|
|
3466
|
-
if (isPackagesDir(c2)) return
|
|
4029
|
+
if (isPackagesDir(c2)) return resolve12(c2);
|
|
3467
4030
|
}
|
|
3468
4031
|
return void 0;
|
|
3469
4032
|
}
|
|
3470
4033
|
function defaultNameFromPath(dir) {
|
|
3471
|
-
const base =
|
|
4034
|
+
const base = basename3(resolve12(dir));
|
|
3472
4035
|
const slug = base.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "plugin";
|
|
3473
4036
|
return `@plugin/${slug}`;
|
|
3474
4037
|
}
|
|
@@ -3496,18 +4059,18 @@ function versionSpecFrom(status, stdout2) {
|
|
|
3496
4059
|
const v = (stdout2 ?? "").trim();
|
|
3497
4060
|
return status === 0 && /^\d+\.\d+\.\d+/.test(v) ? `^${v}` : "latest";
|
|
3498
4061
|
}
|
|
3499
|
-
function registryDevDeps(game, sdkVersion,
|
|
4062
|
+
function registryDevDeps(game, sdkVersion, resolve13 = resolvePublishedVersion) {
|
|
3500
4063
|
const deps = {};
|
|
3501
4064
|
for (const n3 of createPackageNames(game)) {
|
|
3502
|
-
deps[`@s2script/${n3}`] = n3 === "sdk" ? `^${sdkVersion}` :
|
|
4065
|
+
deps[`@s2script/${n3}`] = n3 === "sdk" ? `^${sdkVersion}` : resolve13(`@s2script/${n3}`);
|
|
3503
4066
|
}
|
|
3504
4067
|
return deps;
|
|
3505
4068
|
}
|
|
3506
4069
|
function fileDevDeps(packagesDir, game) {
|
|
3507
4070
|
const deps = {};
|
|
3508
4071
|
for (const n3 of createPackageNames(game)) {
|
|
3509
|
-
const abs =
|
|
3510
|
-
if (!
|
|
4072
|
+
const abs = join11(packagesDir, n3);
|
|
4073
|
+
if (!existsSync11(join11(abs, "package.json"))) return void 0;
|
|
3511
4074
|
deps[`@s2script/${n3}`] = `file:${abs}`;
|
|
3512
4075
|
}
|
|
3513
4076
|
return deps;
|
|
@@ -3552,7 +4115,12 @@ function tsconfigJson() {
|
|
|
3552
4115
|
return JSON.stringify(
|
|
3553
4116
|
{
|
|
3554
4117
|
compilerOptions: sharedCompilerOptionsJson,
|
|
3555
|
-
|
|
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"]
|
|
3556
4124
|
},
|
|
3557
4125
|
null,
|
|
3558
4126
|
2
|
|
@@ -3587,10 +4155,10 @@ dist/
|
|
|
3587
4155
|
`;
|
|
3588
4156
|
}
|
|
3589
4157
|
function linkLocalPackagesForNoInstall(targetPath, localPackagesDir) {
|
|
3590
|
-
const nm =
|
|
3591
|
-
|
|
3592
|
-
const dest =
|
|
3593
|
-
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);
|
|
3594
4162
|
}
|
|
3595
4163
|
function runInstall(dir, pm) {
|
|
3596
4164
|
if (pm === "none") return;
|
|
@@ -3603,7 +4171,7 @@ function runInstall(dir, pm) {
|
|
|
3603
4171
|
if (r2.status !== 0) throw new Error(`${pm} install failed (exit ${r2.status})`);
|
|
3604
4172
|
}
|
|
3605
4173
|
async function createPlugin(opts = {}) {
|
|
3606
|
-
const targetPath =
|
|
4174
|
+
const targetPath = resolve12(opts.path ?? ".");
|
|
3607
4175
|
const interactive = isInteractive({ yes: opts.yes });
|
|
3608
4176
|
let game = assertGame(opts.game);
|
|
3609
4177
|
let name = opts.name;
|
|
@@ -3612,7 +4180,7 @@ async function createPlugin(opts = {}) {
|
|
|
3612
4180
|
if (template !== "minimal") {
|
|
3613
4181
|
throw new Error(`unknown template ${JSON.stringify(template)} (v1 supports: minimal)`);
|
|
3614
4182
|
}
|
|
3615
|
-
if (
|
|
4183
|
+
if (existsSync11(targetPath)) {
|
|
3616
4184
|
const kids = readdirSync2(targetPath);
|
|
3617
4185
|
const meaningful = kids.filter((k) => k !== ".git");
|
|
3618
4186
|
if (meaningful.length) {
|
|
@@ -3665,15 +4233,15 @@ async function createPlugin(opts = {}) {
|
|
|
3665
4233
|
install = install ?? (opts.noInstall ? "none" : "npm");
|
|
3666
4234
|
const version = readCliVersion();
|
|
3667
4235
|
const localPackagesDir = findLocalPackagesDir();
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
4236
|
+
mkdirSync5(join11(targetPath, "src"), { recursive: true });
|
|
4237
|
+
writeFileSync7(
|
|
4238
|
+
join11(targetPath, "package.json"),
|
|
3671
4239
|
packageJsonContent(name, game, version, localPackagesDir)
|
|
3672
4240
|
);
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
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());
|
|
3677
4245
|
let installed = false;
|
|
3678
4246
|
if (install !== "none") {
|
|
3679
4247
|
const pm = install;
|
|
@@ -3696,7 +4264,7 @@ async function createPlugin(opts = {}) {
|
|
|
3696
4264
|
}
|
|
3697
4265
|
|
|
3698
4266
|
// src/commands/create.ts
|
|
3699
|
-
async function
|
|
4267
|
+
async function run5(argv2) {
|
|
3700
4268
|
const yes = hasFlag(argv2, "--yes") || hasFlag(argv2, "-y");
|
|
3701
4269
|
const interactive = isInteractive({ yes });
|
|
3702
4270
|
const pathArg = argv2.find((a2) => !a2.startsWith("-"));
|
|
@@ -3724,26 +4292,158 @@ async function run4(argv2) {
|
|
|
3724
4292
|
}
|
|
3725
4293
|
}
|
|
3726
4294
|
|
|
4295
|
+
// src/registry/login.ts
|
|
4296
|
+
import { hostname } from "node:os";
|
|
4297
|
+
|
|
4298
|
+
// src/registry/device-flow.ts
|
|
4299
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
4300
|
+
function rootCause2(err2) {
|
|
4301
|
+
let cur = err2;
|
|
4302
|
+
for (let i2 = 0; i2 < 5 && cur instanceof Error && cur.cause; i2++) cur = cur.cause;
|
|
4303
|
+
return cur instanceof Error ? cur.message : String(cur);
|
|
4304
|
+
}
|
|
4305
|
+
async function describeError(res) {
|
|
4306
|
+
const raw = await res.text().catch(() => "");
|
|
4307
|
+
try {
|
|
4308
|
+
const body = JSON.parse(raw);
|
|
4309
|
+
if (body && typeof body.error === "string") return body.error;
|
|
4310
|
+
if (body && typeof body.message === "string") return body.message;
|
|
4311
|
+
} catch {
|
|
4312
|
+
}
|
|
4313
|
+
if (/^\s*</.test(raw)) return "server returned an HTML error page";
|
|
4314
|
+
if (raw.trim()) return raw.trim().slice(0, 200);
|
|
4315
|
+
return res.statusText || "no response body";
|
|
4316
|
+
}
|
|
4317
|
+
async function startDeviceAuth(registryUrl, opts) {
|
|
4318
|
+
const fetchFn = opts?.fetchFn ?? fetch;
|
|
4319
|
+
let res;
|
|
4320
|
+
try {
|
|
4321
|
+
res = await fetchFn(`${registryUrl}/api/v1/auth/device/start`, {
|
|
4322
|
+
method: "POST",
|
|
4323
|
+
headers: { "content-type": "application/json" },
|
|
4324
|
+
body: JSON.stringify({ client: opts?.client }),
|
|
4325
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
4326
|
+
});
|
|
4327
|
+
} catch (e) {
|
|
4328
|
+
throw new Error(`could not reach ${registryUrl} \u2014 ${rootCause2(e)}`);
|
|
4329
|
+
}
|
|
4330
|
+
if (res.status === 404 || res.status === 405) return null;
|
|
4331
|
+
if (!res.ok) {
|
|
4332
|
+
throw new Error(`login start failed (HTTP ${res.status}): ${await describeError(res)}`);
|
|
4333
|
+
}
|
|
4334
|
+
const body = await res.json().catch(() => null);
|
|
4335
|
+
if (!body || typeof body.deviceCode !== "string" || typeof body.userCode !== "string") {
|
|
4336
|
+
throw new Error(`login start returned an unexpected response from ${registryUrl}`);
|
|
4337
|
+
}
|
|
4338
|
+
return body;
|
|
4339
|
+
}
|
|
4340
|
+
async function pollForToken(registryUrl, start, opts) {
|
|
4341
|
+
const fetchFn = opts?.fetchFn ?? fetch;
|
|
4342
|
+
const sleep = opts?.sleep ?? ((ms) => new Promise((r2) => setTimeout(r2, ms)));
|
|
4343
|
+
let interval = Math.max(1, start.interval);
|
|
4344
|
+
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
4345
|
+
while (Date.now() < deadline) {
|
|
4346
|
+
await sleep(interval * 1e3);
|
|
4347
|
+
let res;
|
|
4348
|
+
try {
|
|
4349
|
+
res = await fetchFn(`${registryUrl}/api/v1/auth/device/poll`, {
|
|
4350
|
+
method: "POST",
|
|
4351
|
+
headers: { "content-type": "application/json" },
|
|
4352
|
+
body: JSON.stringify({ deviceCode: start.deviceCode }),
|
|
4353
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
4354
|
+
});
|
|
4355
|
+
} catch (e) {
|
|
4356
|
+
throw new Error(`could not reach ${registryUrl} \u2014 ${rootCause2(e)}`);
|
|
4357
|
+
}
|
|
4358
|
+
if (!res.ok) {
|
|
4359
|
+
throw new Error(`login poll failed (HTTP ${res.status}): ${await describeError(res)}`);
|
|
4360
|
+
}
|
|
4361
|
+
const body = await res.json().catch(() => null);
|
|
4362
|
+
if (!body) throw new Error("login poll returned a non-JSON response");
|
|
4363
|
+
switch (body.status) {
|
|
4364
|
+
case "pending":
|
|
4365
|
+
break;
|
|
4366
|
+
case "slow_down":
|
|
4367
|
+
interval += 5;
|
|
4368
|
+
break;
|
|
4369
|
+
case "approved":
|
|
4370
|
+
if (!body.token) throw new Error("server approved the login but sent no token");
|
|
4371
|
+
return { token: body.token, tokenName: body.tokenName };
|
|
4372
|
+
case "denied":
|
|
4373
|
+
throw new Error("login request was denied in the browser");
|
|
4374
|
+
case "expired":
|
|
4375
|
+
throw new Error("login request expired \u2014 run `s2s login` again");
|
|
4376
|
+
default:
|
|
4377
|
+
throw new Error(`unexpected login status: ${body.status}`);
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
throw new Error("login request expired \u2014 run `s2s login` again");
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
// src/registry/open-browser.ts
|
|
4384
|
+
import { spawn } from "node:child_process";
|
|
4385
|
+
function tryOpenBrowser(url) {
|
|
4386
|
+
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
4387
|
+
return false;
|
|
4388
|
+
}
|
|
4389
|
+
let cmd;
|
|
4390
|
+
let args;
|
|
4391
|
+
if (process.platform === "darwin") {
|
|
4392
|
+
cmd = "open";
|
|
4393
|
+
args = [url];
|
|
4394
|
+
} else if (process.platform === "win32") {
|
|
4395
|
+
cmd = "cmd";
|
|
4396
|
+
args = ["/c", "start", '""', url];
|
|
4397
|
+
} else {
|
|
4398
|
+
cmd = "xdg-open";
|
|
4399
|
+
args = [url];
|
|
4400
|
+
}
|
|
4401
|
+
try {
|
|
4402
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
4403
|
+
child.on("error", () => {
|
|
4404
|
+
});
|
|
4405
|
+
child.unref();
|
|
4406
|
+
return true;
|
|
4407
|
+
} catch {
|
|
4408
|
+
return false;
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
|
|
3727
4412
|
// src/registry/login.ts
|
|
3728
4413
|
async function loginInteractive(opts) {
|
|
3729
|
-
const registryUrl = (opts?.registryUrl || defaultRegistryUrl())
|
|
4414
|
+
const registryUrl = normalizeRegistryUrl(opts?.registryUrl || defaultRegistryUrl());
|
|
3730
4415
|
let token = opts?.token || process.env.S2SCRIPT_TOKEN || process.env.S2SCRIPT_DEPLOY_TOKEN;
|
|
3731
4416
|
if (!token) {
|
|
3732
4417
|
if (!isInteractive({ ci: opts?.ci })) {
|
|
3733
|
-
throw new Error(
|
|
3734
|
-
"no deploy token: set S2SCRIPT_TOKEN or run `s2s login` interactively"
|
|
3735
|
-
);
|
|
4418
|
+
throw new Error("no deploy token: set S2SCRIPT_TOKEN or run `s2s login` interactively");
|
|
3736
4419
|
}
|
|
3737
4420
|
intro2("s2script login");
|
|
3738
4421
|
log2.info(`Registry: ${registryUrl}`);
|
|
3739
|
-
|
|
3740
|
-
|
|
4422
|
+
const start = await startDeviceAuth(registryUrl, { client: hostname() });
|
|
4423
|
+
if (start) {
|
|
4424
|
+
log2.info(`Confirmation code: ${start.userCode}`);
|
|
4425
|
+
log2.info(`Approve at: ${start.verificationUriComplete}`);
|
|
4426
|
+
if (!opts?.noBrowser && tryOpenBrowser(start.verificationUriComplete)) {
|
|
4427
|
+
log2.info("Opened your browser \u2014 approve the request there.");
|
|
4428
|
+
} else {
|
|
4429
|
+
log2.info("Open that link in any browser (it can be on another machine).");
|
|
4430
|
+
}
|
|
4431
|
+
const approved = await task(
|
|
4432
|
+
"Waiting for approval in the browser\u2026",
|
|
4433
|
+
() => pollForToken(registryUrl, start),
|
|
4434
|
+
{ interactive: true, done: (r2) => `Approved (token "${r2.tokenName ?? "cli"}")` }
|
|
4435
|
+
);
|
|
4436
|
+
token = approved.token;
|
|
4437
|
+
} else {
|
|
4438
|
+
log2.info(
|
|
4439
|
+
`Sign in (or create an account), then mint a deploy token at:
|
|
3741
4440
|
${registryUrl}/account/tokens`
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
4441
|
+
);
|
|
4442
|
+
token = (await password2({
|
|
4443
|
+
message: "Paste your deploy token",
|
|
4444
|
+
validate: (v) => (v ?? "").trim().startsWith("s2s_") ? void 0 : 'token must start with "s2s_"'
|
|
4445
|
+
})).trim();
|
|
4446
|
+
}
|
|
3747
4447
|
}
|
|
3748
4448
|
if (!token || !token.startsWith("s2s_")) {
|
|
3749
4449
|
throw new Error('token must start with "s2s_"');
|
|
@@ -3754,13 +4454,14 @@ async function loginInteractive(opts) {
|
|
|
3754
4454
|
}
|
|
3755
4455
|
|
|
3756
4456
|
// src/commands/login.ts
|
|
3757
|
-
async function
|
|
4457
|
+
async function run6(argv2) {
|
|
3758
4458
|
const interactive = isInteractive({ ci: hasFlag(argv2, "--ci") });
|
|
3759
4459
|
try {
|
|
3760
4460
|
const creds = await loginInteractive({
|
|
3761
4461
|
token: parseFlag(argv2, "--token"),
|
|
3762
4462
|
registryUrl: parseFlag(argv2, "--registry") || defaultRegistryUrl(),
|
|
3763
|
-
ci: hasFlag(argv2, "--ci")
|
|
4463
|
+
ci: hasFlag(argv2, "--ci"),
|
|
4464
|
+
noBrowser: hasFlag(argv2, "--no-browser")
|
|
3764
4465
|
});
|
|
3765
4466
|
const msg = `logged in \u2192 ${creds.registryUrl} (credentials saved)`;
|
|
3766
4467
|
if (interactive) outro2(msg);
|
|
@@ -3772,8 +4473,8 @@ async function run5(argv2) {
|
|
|
3772
4473
|
}
|
|
3773
4474
|
|
|
3774
4475
|
// src/config/gen.ts
|
|
3775
|
-
import { readFileSync as
|
|
3776
|
-
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";
|
|
3777
4478
|
|
|
3778
4479
|
// src/config/config-path.ts
|
|
3779
4480
|
function configFileName(id) {
|
|
@@ -3819,13 +4520,13 @@ function genConfigFromManifest(manifest, outDir) {
|
|
|
3819
4520
|
}
|
|
3820
4521
|
const id = typeof manifest.id === "string" ? manifest.id : "";
|
|
3821
4522
|
if (!id) throw new Error("config gen: manifest declares config but has no id");
|
|
3822
|
-
|
|
3823
|
-
const path =
|
|
3824
|
-
|
|
4523
|
+
mkdirSync6(outDir, { recursive: true });
|
|
4524
|
+
const path = join12(outDir, configFileName(id));
|
|
4525
|
+
writeFileSync8(path, generateDefaultJsonc(config));
|
|
3825
4526
|
return path;
|
|
3826
4527
|
}
|
|
3827
4528
|
function genConfigForS2sp(s2spPath, outDir) {
|
|
3828
|
-
const entry = unzipSync(
|
|
4529
|
+
const entry = unzipSync(readFileSync12(s2spPath))["manifest.json"];
|
|
3829
4530
|
if (!entry) throw new Error(`config gen: ${s2spPath} has no manifest.json`);
|
|
3830
4531
|
const manifest = JSON.parse(Buffer.from(entry).toString("utf8"));
|
|
3831
4532
|
return genConfigFromManifest(manifest, outDir);
|
|
@@ -3842,7 +4543,7 @@ function runConfigGen(s2sps, outDir) {
|
|
|
3842
4543
|
}
|
|
3843
4544
|
|
|
3844
4545
|
// src/commands/config.ts
|
|
3845
|
-
async function
|
|
4546
|
+
async function run7(argv2) {
|
|
3846
4547
|
const rest = argv2[0] === "gen" ? argv2.slice(1) : argv2;
|
|
3847
4548
|
const interactive = isInteractive();
|
|
3848
4549
|
const outDir = parseFlag(rest, "--out") ?? process.cwd();
|
|
@@ -3872,12 +4573,12 @@ async function run6(argv2) {
|
|
|
3872
4573
|
}
|
|
3873
4574
|
|
|
3874
4575
|
// src/commands/codegen.ts
|
|
3875
|
-
import { dirname as dirname3, join as
|
|
4576
|
+
import { dirname as dirname3, join as join16 } from "node:path";
|
|
3876
4577
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3877
4578
|
|
|
3878
4579
|
// src/schemagen/gen.ts
|
|
3879
|
-
import { readFileSync as
|
|
3880
|
-
import { join as
|
|
4580
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
|
|
4581
|
+
import { join as join13 } from "node:path";
|
|
3881
4582
|
|
|
3882
4583
|
// src/schemagen/model.ts
|
|
3883
4584
|
var READ = {
|
|
@@ -4069,21 +4770,21 @@ function emitJs(model) {
|
|
|
4069
4770
|
for (const c2 of model.classes) {
|
|
4070
4771
|
out.push(` A[${S(c2.className)}] = {`);
|
|
4071
4772
|
for (const f of flattenedFields(model, c2.className)) {
|
|
4072
|
-
const
|
|
4773
|
+
const resolve13 = `off(${S(f.declaringClass)},${S(f.rawName)})`;
|
|
4073
4774
|
let entry;
|
|
4074
4775
|
if (f.accessorKind === "str") {
|
|
4075
|
-
entry = `get: function () { return this.ref.readString(${
|
|
4776
|
+
entry = `get: function () { return this.ref.readString(${resolve13}, ${f.strLen}); }`;
|
|
4076
4777
|
} else if (f.accessorKind === "u64" || f.accessorKind === "i64") {
|
|
4077
|
-
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(); }`;
|
|
4078
4779
|
} else if (VEC_INFO[f.accessorKind]) {
|
|
4079
4780
|
const vi = VEC_INFO[f.accessorKind];
|
|
4080
|
-
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]); }`;
|
|
4081
4782
|
} else {
|
|
4082
|
-
entry = `get: function () { return this.ref.${READ[f.accessorKind]}(${
|
|
4783
|
+
entry = `get: function () { return this.ref.${READ[f.accessorKind]}(${resolve13}); }`;
|
|
4083
4784
|
}
|
|
4084
4785
|
if (f.writable) {
|
|
4085
4786
|
const wm = WRITE[f.accessorKind];
|
|
4086
|
-
entry += `, set: function (v) { var o = ${
|
|
4787
|
+
entry += `, set: function (v) { var o = ${resolve13}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(o); }`;
|
|
4087
4788
|
}
|
|
4088
4789
|
out.push(` ${S(f.propName)}: { ${entry} },`);
|
|
4089
4790
|
}
|
|
@@ -4109,8 +4810,8 @@ var LIST_PATH = "games/cs2/codegen-classes.json";
|
|
|
4109
4810
|
var JS_OUT = "games/cs2/js/schema.generated.js";
|
|
4110
4811
|
var DTS_OUT = "packages/cs2/schema.generated.d.ts";
|
|
4111
4812
|
function runGenSchema(repoRoot2, opts) {
|
|
4112
|
-
const catalog = JSON.parse(
|
|
4113
|
-
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"));
|
|
4114
4815
|
const model = buildModel(catalog, list);
|
|
4115
4816
|
const dts = emitDts(model);
|
|
4116
4817
|
const js = emitJs(model);
|
|
@@ -4119,16 +4820,16 @@ function runGenSchema(repoRoot2, opts) {
|
|
|
4119
4820
|
const drift = [];
|
|
4120
4821
|
const files = [[JS_OUT, js], [DTS_OUT, dts]];
|
|
4121
4822
|
for (const [rel, content] of files) {
|
|
4122
|
-
const abs =
|
|
4823
|
+
const abs = join13(repoRoot2, rel);
|
|
4123
4824
|
if (opts.check) {
|
|
4124
4825
|
let cur = "";
|
|
4125
4826
|
try {
|
|
4126
|
-
cur =
|
|
4827
|
+
cur = readFileSync13(abs, "utf8");
|
|
4127
4828
|
} catch {
|
|
4128
4829
|
}
|
|
4129
4830
|
if (cur !== content) drift.push(rel);
|
|
4130
4831
|
} else {
|
|
4131
|
-
|
|
4832
|
+
writeFileSync9(abs, content);
|
|
4132
4833
|
}
|
|
4133
4834
|
}
|
|
4134
4835
|
if (model.collisions.length) console.error(`gen-schema: ${model.collisions.length} name collision(s) \u2192 raw fallback:
|
|
@@ -4138,8 +4839,8 @@ function runGenSchema(repoRoot2, opts) {
|
|
|
4138
4839
|
}
|
|
4139
4840
|
|
|
4140
4841
|
// src/eventgen/gen.ts
|
|
4141
|
-
import { readFileSync as
|
|
4142
|
-
import { join as
|
|
4842
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "node:fs";
|
|
4843
|
+
import { join as join14 } from "node:path";
|
|
4143
4844
|
|
|
4144
4845
|
// src/eventgen/model.ts
|
|
4145
4846
|
var TYPE_TO_GETTER = {
|
|
@@ -4224,27 +4925,27 @@ function emitEventDts(model) {
|
|
|
4224
4925
|
var CATALOG_PATH2 = "games/cs2/gamedata/event-catalog.json";
|
|
4225
4926
|
var DTS_OUT2 = "packages/cs2/events.generated.d.ts";
|
|
4226
4927
|
function runGenEvents(repoRoot2, opts) {
|
|
4227
|
-
const catalog = JSON.parse(
|
|
4928
|
+
const catalog = JSON.parse(readFileSync14(join14(repoRoot2, CATALOG_PATH2), "utf8"));
|
|
4228
4929
|
const model = buildEventModel(catalog);
|
|
4229
4930
|
const dts = emitEventDts(model);
|
|
4230
4931
|
const drift = [];
|
|
4231
|
-
const abs =
|
|
4932
|
+
const abs = join14(repoRoot2, DTS_OUT2);
|
|
4232
4933
|
if (opts.check) {
|
|
4233
4934
|
let cur = "";
|
|
4234
4935
|
try {
|
|
4235
|
-
cur =
|
|
4936
|
+
cur = readFileSync14(abs, "utf8");
|
|
4236
4937
|
} catch {
|
|
4237
4938
|
}
|
|
4238
4939
|
if (cur !== dts) drift.push(DTS_OUT2);
|
|
4239
4940
|
} else {
|
|
4240
|
-
|
|
4941
|
+
writeFileSync10(abs, dts);
|
|
4241
4942
|
}
|
|
4242
4943
|
return { events: model.length, drift };
|
|
4243
4944
|
}
|
|
4244
4945
|
|
|
4245
4946
|
// src/navgen/gen.ts
|
|
4246
|
-
import { readFileSync as
|
|
4247
|
-
import { join as
|
|
4947
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11 } from "node:fs";
|
|
4948
|
+
import { join as join15 } from "node:path";
|
|
4248
4949
|
|
|
4249
4950
|
// src/navgen/model.ts
|
|
4250
4951
|
var SUPPORTED_NAV_KINDS = /* @__PURE__ */ new Set([
|
|
@@ -4319,22 +5020,22 @@ function emitNavJs(model) {
|
|
|
4319
5020
|
out.push(` function ${w.wrapper}(root, path) { this.root = root; this.path = path; }`);
|
|
4320
5021
|
out.push(` Object.defineProperties(${w.wrapper}.prototype, {`);
|
|
4321
5022
|
for (const f of w.fields) {
|
|
4322
|
-
const
|
|
5023
|
+
const resolve13 = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
|
|
4323
5024
|
let getter;
|
|
4324
5025
|
const kind = f.accessorKind;
|
|
4325
5026
|
if (kind === "u64") {
|
|
4326
|
-
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(); }`;
|
|
4327
5028
|
} else if (kind === "i64") {
|
|
4328
|
-
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(); }`;
|
|
4329
5030
|
} else if (VEC_INFO[kind]) {
|
|
4330
5031
|
const vi = VEC_INFO[kind];
|
|
4331
|
-
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]); }`;
|
|
4332
5033
|
} else if (kind === "handle") {
|
|
4333
|
-
getter = `get: function () { return this.root.readHandleVia(this.path, ${
|
|
5034
|
+
getter = `get: function () { return this.root.readHandleVia(this.path, ${resolve13}); }`;
|
|
4334
5035
|
} else {
|
|
4335
5036
|
const method = READ_VIA[kind];
|
|
4336
5037
|
if (!method) continue;
|
|
4337
|
-
getter = `get: function () { return this.root.${method}(this.path, ${
|
|
5038
|
+
getter = `get: function () { return this.root.${method}(this.path, ${resolve13}); }`;
|
|
4338
5039
|
}
|
|
4339
5040
|
out.push(` ${S2(f.propName)}: { ${getter} },`);
|
|
4340
5041
|
}
|
|
@@ -4406,8 +5107,8 @@ var CATALOG_PATH3 = "games/cs2/gamedata/schema-catalog.json";
|
|
|
4406
5107
|
var JS_OUT2 = "games/cs2/js/nav.generated.js";
|
|
4407
5108
|
var DTS_OUT3 = "packages/cs2/nav.generated.d.ts";
|
|
4408
5109
|
function runGenNav(repoRoot2, opts) {
|
|
4409
|
-
const config = JSON.parse(
|
|
4410
|
-
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"));
|
|
4411
5112
|
const model = buildNavModel(config, catalog);
|
|
4412
5113
|
const js = emitNavJs(model);
|
|
4413
5114
|
const dts = emitNavDts(model);
|
|
@@ -4415,16 +5116,16 @@ function runGenNav(repoRoot2, opts) {
|
|
|
4415
5116
|
const drift = [];
|
|
4416
5117
|
const files = [[JS_OUT2, js], [DTS_OUT3, dts]];
|
|
4417
5118
|
for (const [rel, content] of files) {
|
|
4418
|
-
const abs =
|
|
5119
|
+
const abs = join15(repoRoot2, rel);
|
|
4419
5120
|
if (opts.check) {
|
|
4420
5121
|
let cur = "";
|
|
4421
5122
|
try {
|
|
4422
|
-
cur =
|
|
5123
|
+
cur = readFileSync15(abs, "utf8");
|
|
4423
5124
|
} catch {
|
|
4424
5125
|
}
|
|
4425
5126
|
if (cur !== content) drift.push(rel);
|
|
4426
5127
|
} else {
|
|
4427
|
-
|
|
5128
|
+
writeFileSync11(abs, content);
|
|
4428
5129
|
}
|
|
4429
5130
|
}
|
|
4430
5131
|
return { wrappers: model.wrappers.length, fields, drift };
|
|
@@ -4432,9 +5133,9 @@ function runGenNav(repoRoot2, opts) {
|
|
|
4432
5133
|
|
|
4433
5134
|
// src/commands/codegen.ts
|
|
4434
5135
|
function repoRoot() {
|
|
4435
|
-
return
|
|
5136
|
+
return join16(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "..");
|
|
4436
5137
|
}
|
|
4437
|
-
async function
|
|
5138
|
+
async function run8(kind, argv2) {
|
|
4438
5139
|
const root = repoRoot();
|
|
4439
5140
|
const check = argv2[0] === "--check";
|
|
4440
5141
|
if (kind === "schema") {
|
|
@@ -4478,15 +5179,16 @@ async function run7(kind, argv2) {
|
|
|
4478
5179
|
|
|
4479
5180
|
// src/commands/index.ts
|
|
4480
5181
|
var COMMANDS = [
|
|
4481
|
-
{ name: "create", summary: "Scaffold a new plugin", run:
|
|
5182
|
+
{ name: "create", summary: "Scaffold a new plugin", run: run5 },
|
|
4482
5183
|
{ name: "build", summary: "Build a plugin to a .s2sp", run },
|
|
4483
5184
|
{ name: "deploy", summary: "Publish a plugin to the registry", run: run2 },
|
|
4484
5185
|
{ name: "add", summary: "Add a registry package's types", run: run3 },
|
|
4485
|
-
{ name: "
|
|
4486
|
-
{ name: "
|
|
4487
|
-
{ name: "
|
|
4488
|
-
{ name: "gen-
|
|
4489
|
-
{ 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) }
|
|
4490
5192
|
];
|
|
4491
5193
|
function find(name) {
|
|
4492
5194
|
if (name === "publish") return COMMANDS.find((c2) => c2.name === "deploy");
|
|
@@ -4494,7 +5196,7 @@ function find(name) {
|
|
|
4494
5196
|
}
|
|
4495
5197
|
|
|
4496
5198
|
// src/commands/menu.ts
|
|
4497
|
-
async function
|
|
5199
|
+
async function run9() {
|
|
4498
5200
|
intro2("s2script");
|
|
4499
5201
|
const name = await select2({
|
|
4500
5202
|
message: "What would you like to do?",
|
|
@@ -4509,12 +5211,12 @@ var argv = process.argv.slice(2);
|
|
|
4509
5211
|
var command = argv[0];
|
|
4510
5212
|
function usage() {
|
|
4511
5213
|
console.error(
|
|
4512
|
-
"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)"
|
|
4513
5215
|
);
|
|
4514
5216
|
}
|
|
4515
5217
|
if (!command) {
|
|
4516
5218
|
if (isInteractive()) {
|
|
4517
|
-
await
|
|
5219
|
+
await run9();
|
|
4518
5220
|
} else {
|
|
4519
5221
|
usage();
|
|
4520
5222
|
process.exit(1);
|