@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.
Files changed (3) hide show
  1. package/dist/cli.js +588 -88
  2. package/package.json +4 -1
  3. 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 = [entry, join3(packagesDir, "sdk", "globals.d.ts"), ...localDts];
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
- "types download": 12e4
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 mkdirSync4,
3499
- writeFileSync as writeFileSync6,
3500
- existsSync as existsSync9,
3992
+ mkdirSync as mkdirSync5,
3993
+ writeFileSync as writeFileSync7,
3994
+ existsSync as existsSync11,
3501
3995
  readdirSync as readdirSync2,
3502
- readFileSync as readFileSync10,
3996
+ readFileSync as readFileSync11,
3503
3997
  symlinkSync
3504
3998
  } from "node:fs";
3505
- import { dirname as dirname2, join as join10, resolve as resolve10, basename } from "node:path";
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
- join10(here, "..", "package.json"),
4005
+ join11(here, "..", "package.json"),
3512
4006
  // dist/cli.js → packages/sdk/package.json
3513
- join10(here, "..", "..", "package.json")
4007
+ join11(here, "..", "..", "package.json")
3514
4008
  // src/create → packages/sdk/package.json
3515
4009
  ];
3516
4010
  for (const p of candidates) {
3517
- if (!existsSync9(p)) continue;
4011
+ if (!existsSync11(p)) continue;
3518
4012
  try {
3519
- const pkg = JSON.parse(readFileSync10(p, "utf8"));
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
- join10(here, "..", ".."),
4023
+ join11(here, "..", ".."),
3530
4024
  // dist/cli.js → packages
3531
- join10(here, "..", "..", "..")
4025
+ join11(here, "..", "..", "..")
3532
4026
  // src/create → packages
3533
4027
  ];
3534
4028
  for (const c2 of candidates) {
3535
- if (isPackagesDir(c2)) return resolve10(c2);
4029
+ if (isPackagesDir(c2)) return resolve12(c2);
3536
4030
  }
3537
4031
  return void 0;
3538
4032
  }
3539
4033
  function defaultNameFromPath(dir) {
3540
- const base = basename(resolve10(dir));
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, resolve11 = resolvePublishedVersion) {
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}` : resolve11(`@s2script/${n3}`);
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 = join10(packagesDir, n3);
3579
- if (!existsSync9(join10(abs, "package.json"))) return void 0;
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
- include: ["src", "node_modules/@s2script/sdk/globals.d.ts"]
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 = join10(targetPath, "node_modules");
3660
- mkdirSync4(nm, { recursive: true });
3661
- const dest = join10(nm, "@s2script");
3662
- if (!existsSync9(dest)) symlinkSync(localPackagesDir, dest);
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 = resolve10(opts.path ?? ".");
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 (existsSync9(targetPath)) {
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
- mkdirSync4(join10(targetPath, "src"), { recursive: true });
3738
- writeFileSync6(
3739
- join10(targetPath, "package.json"),
4236
+ mkdirSync5(join11(targetPath, "src"), { recursive: true });
4237
+ writeFileSync7(
4238
+ join11(targetPath, "package.json"),
3740
4239
  packageJsonContent(name, game, version, localPackagesDir)
3741
4240
  );
3742
- writeFileSync6(join10(targetPath, "tsconfig.json"), tsconfigJson());
3743
- writeFileSync6(join10(targetPath, "eslint.config.mjs"), eslintConfig());
3744
- writeFileSync6(join10(targetPath, "src", "plugin.ts"), pluginSource(game));
3745
- writeFileSync6(join10(targetPath, ".gitignore"), gitignore());
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 run4(argv2) {
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 run5(argv2) {
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 readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "node:fs";
3978
- import { join as join11 } from "node:path";
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
- mkdirSync5(outDir, { recursive: true });
4025
- const path = join11(outDir, configFileName(id));
4026
- writeFileSync7(path, generateDefaultJsonc(config));
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(readFileSync11(s2spPath))["manifest.json"];
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 run6(argv2) {
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 join15 } from "node:path";
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 readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
4082
- import { join as join12 } from "node:path";
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 resolve11 = `off(${S(f.declaringClass)},${S(f.rawName)})`;
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(${resolve11}, ${f.strLen}); }`;
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]}(${resolve11}); return v === null ? null : v.toString(); }`;
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(${resolve11}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
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]}(${resolve11}); }`;
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 = ${resolve11}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(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(readFileSync12(join12(repoRoot2, CATALOG_PATH), "utf8"));
4315
- const list = JSON.parse(readFileSync12(join12(repoRoot2, LIST_PATH), "utf8"));
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 = join12(repoRoot2, rel);
4823
+ const abs = join13(repoRoot2, rel);
4325
4824
  if (opts.check) {
4326
4825
  let cur = "";
4327
4826
  try {
4328
- cur = readFileSync12(abs, "utf8");
4827
+ cur = readFileSync13(abs, "utf8");
4329
4828
  } catch {
4330
4829
  }
4331
4830
  if (cur !== content) drift.push(rel);
4332
4831
  } else {
4333
- writeFileSync8(abs, content);
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 readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
4344
- import { join as join13 } from "node:path";
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(readFileSync13(join13(repoRoot2, CATALOG_PATH2), "utf8"));
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 = join13(repoRoot2, DTS_OUT2);
4932
+ const abs = join14(repoRoot2, DTS_OUT2);
4434
4933
  if (opts.check) {
4435
4934
  let cur = "";
4436
4935
  try {
4437
- cur = readFileSync13(abs, "utf8");
4936
+ cur = readFileSync14(abs, "utf8");
4438
4937
  } catch {
4439
4938
  }
4440
4939
  if (cur !== dts) drift.push(DTS_OUT2);
4441
4940
  } else {
4442
- writeFileSync9(abs, dts);
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 readFileSync14, writeFileSync as writeFileSync10 } from "node:fs";
4449
- import { join as join14 } from "node:path";
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 resolve11 = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
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, ${resolve11}); return v === null ? null : v.toString(); }`;
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, ${resolve11}); return v === null ? null : v.toString(); }`;
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, ${resolve11}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
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, ${resolve11}); }`;
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, ${resolve11}); }`;
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(readFileSync14(join14(repoRoot2, TARGETS_PATH), "utf8"));
4612
- const catalog = JSON.parse(readFileSync14(join14(repoRoot2, CATALOG_PATH3), "utf8"));
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 = join14(repoRoot2, rel);
5119
+ const abs = join15(repoRoot2, rel);
4621
5120
  if (opts.check) {
4622
5121
  let cur = "";
4623
5122
  try {
4624
- cur = readFileSync14(abs, "utf8");
5123
+ cur = readFileSync15(abs, "utf8");
4625
5124
  } catch {
4626
5125
  }
4627
5126
  if (cur !== content) drift.push(rel);
4628
5127
  } else {
4629
- writeFileSync10(abs, content);
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 join15(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "..");
5136
+ return join16(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "..");
4638
5137
  }
4639
- async function run7(kind, argv2) {
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: run4 },
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: "login", summary: "Save a registry deploy token", run: run5 },
4688
- { name: "config", summary: "Emit a plugin's default config file(s)", run: run6 },
4689
- { name: "gen-schema", summary: "Regenerate schema accessors", run: (a2) => run7("schema", a2) },
4690
- { name: "gen-events", summary: "Regenerate the event catalog", run: (a2) => run7("events", a2) },
4691
- { name: "gen-nav", summary: "Regenerate nav accessors", run: (a2) => run7("nav", a2) }
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 run8() {
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 run8();
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.8.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
+ };