@s2script/sdk 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -446,8 +446,8 @@ async function lintPlugin(pluginDir, program) {
446
446
  const results = await eslint.lintFiles(targets);
447
447
  const errorCount = results.reduce((n, r) => n + r.errorCount, 0);
448
448
  const formatter = await eslint.loadFormatter("stylish");
449
- const output2 = String(await formatter.format(results));
450
- return { ok: errorCount === 0, output: output2, errorCount };
449
+ const output3 = String(await formatter.format(results));
450
+ return { ok: errorCount === 0, output: output3, errorCount };
451
451
  }
452
452
 
453
453
  // src/build.ts
@@ -788,21 +788,21 @@ function emitJs(model) {
788
788
  for (const c of model.classes) {
789
789
  out.push(` A[${S(c.className)}] = {`);
790
790
  for (const f of flattenedFields(model, c.className)) {
791
- const resolve8 = `off(${S(f.declaringClass)},${S(f.rawName)})`;
791
+ const resolve10 = `off(${S(f.declaringClass)},${S(f.rawName)})`;
792
792
  let entry;
793
793
  if (f.accessorKind === "str") {
794
- entry = `get: function () { return this.ref.readString(${resolve8}, ${f.strLen}); }`;
794
+ entry = `get: function () { return this.ref.readString(${resolve10}, ${f.strLen}); }`;
795
795
  } else if (f.accessorKind === "u64" || f.accessorKind === "i64") {
796
- entry = `get: function () { var v = this.ref.${READ[f.accessorKind]}(${resolve8}); return v === null ? null : v.toString(); }`;
796
+ entry = `get: function () { var v = this.ref.${READ[f.accessorKind]}(${resolve10}); return v === null ? null : v.toString(); }`;
797
797
  } else if (VEC_INFO[f.accessorKind]) {
798
798
  const vi = VEC_INFO[f.accessorKind];
799
- entry = `get: function () { var a = this.ref.readFloats(${resolve8}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
799
+ entry = `get: function () { var a = this.ref.readFloats(${resolve10}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
800
800
  } else {
801
- entry = `get: function () { return this.ref.${READ[f.accessorKind]}(${resolve8}); }`;
801
+ entry = `get: function () { return this.ref.${READ[f.accessorKind]}(${resolve10}); }`;
802
802
  }
803
803
  if (f.writable) {
804
804
  const wm = WRITE[f.accessorKind];
805
- entry += `, set: function (v) { var o = ${resolve8}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(o); }`;
805
+ entry += `, set: function (v) { var o = ${resolve10}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(o); }`;
806
806
  }
807
807
  out.push(` ${S(f.propName)}: { ${entry} },`);
808
808
  }
@@ -1038,22 +1038,22 @@ function emitNavJs(model) {
1038
1038
  out.push(` function ${w.wrapper}(root, path) { this.root = root; this.path = path; }`);
1039
1039
  out.push(` Object.defineProperties(${w.wrapper}.prototype, {`);
1040
1040
  for (const f of w.fields) {
1041
- const resolve8 = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
1041
+ const resolve10 = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
1042
1042
  let getter;
1043
1043
  const kind = f.accessorKind;
1044
1044
  if (kind === "u64") {
1045
- getter = `get: function () { var v = this.root.readUInt64Via(this.path, ${resolve8}); return v === null ? null : v.toString(); }`;
1045
+ getter = `get: function () { var v = this.root.readUInt64Via(this.path, ${resolve10}); return v === null ? null : v.toString(); }`;
1046
1046
  } else if (kind === "i64") {
1047
- getter = `get: function () { var v = this.root.readInt64Via(this.path, ${resolve8}); return v === null ? null : v.toString(); }`;
1047
+ getter = `get: function () { var v = this.root.readInt64Via(this.path, ${resolve10}); return v === null ? null : v.toString(); }`;
1048
1048
  } else if (VEC_INFO[kind]) {
1049
1049
  const vi = VEC_INFO[kind];
1050
- getter = `get: function () { var a = this.root.readFloatsChain(this.path, ${resolve8}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
1050
+ getter = `get: function () { var a = this.root.readFloatsChain(this.path, ${resolve10}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
1051
1051
  } else if (kind === "handle") {
1052
- getter = `get: function () { return this.root.readHandleVia(this.path, ${resolve8}); }`;
1052
+ getter = `get: function () { return this.root.readHandleVia(this.path, ${resolve10}); }`;
1053
1053
  } else {
1054
1054
  const method = READ_VIA[kind];
1055
1055
  if (!method) continue;
1056
- getter = `get: function () { return this.root.${method}(this.path, ${resolve8}); }`;
1056
+ getter = `get: function () { return this.root.${method}(this.path, ${resolve10}); }`;
1057
1057
  }
1058
1058
  out.push(` ${S2(f.propName)}: { ${getter} },`);
1059
1059
  }
@@ -1244,10 +1244,10 @@ function versionSpecFrom(status, stdout) {
1244
1244
  const v = (stdout ?? "").trim();
1245
1245
  return status === 0 && /^\d+\.\d+\.\d+/.test(v) ? `^${v}` : "latest";
1246
1246
  }
1247
- function registryDevDeps(game, sdkVersion, resolve8 = resolvePublishedVersion) {
1247
+ function registryDevDeps(game, sdkVersion, resolve10 = resolvePublishedVersion) {
1248
1248
  const deps = {};
1249
1249
  for (const n of createPackageNames(game)) {
1250
- deps[`@s2script/${n}`] = n === "sdk" ? `^${sdkVersion}` : resolve8(`@s2script/${n}`);
1250
+ deps[`@s2script/${n}`] = n === "sdk" ? `^${sdkVersion}` : resolve10(`@s2script/${n}`);
1251
1251
  }
1252
1252
  return deps;
1253
1253
  }
@@ -1451,7 +1451,7 @@ function generateDefaultJsonc(config) {
1451
1451
  return "{\n" + genEntries(config, 1) + "}\n";
1452
1452
  }
1453
1453
  function genEntries(entries, indent) {
1454
- const pad = " ".repeat(indent);
1454
+ const pad2 = " ".repeat(indent);
1455
1455
  const keys = Object.keys(entries).filter((k) => !k.includes(".")).sort();
1456
1456
  let out = "";
1457
1457
  keys.forEach((key, i) => {
@@ -1461,15 +1461,15 @@ function genEntries(entries, indent) {
1461
1461
  const obj = raw;
1462
1462
  if (isDecl(obj)) {
1463
1463
  const desc = typeof obj.description === "string" ? obj.description : "";
1464
- out += `${pad}// ${String(obj.type)}${desc ? ` \u2014 ${desc}` : ""}
1464
+ out += `${pad2}// ${String(obj.type)}${desc ? ` \u2014 ${desc}` : ""}
1465
1465
  `;
1466
- out += `${pad}${JSON.stringify(key)}: ${JSON.stringify(obj.default)}${comma}
1466
+ out += `${pad2}${JSON.stringify(key)}: ${JSON.stringify(obj.default)}${comma}
1467
1467
  `;
1468
1468
  } else {
1469
- out += `${pad}${JSON.stringify(key)}: {
1469
+ out += `${pad2}${JSON.stringify(key)}: {
1470
1470
  `;
1471
1471
  out += genEntries(obj, indent + 1);
1472
- out += `${pad}}${comma}
1472
+ out += `${pad2}}${comma}
1473
1473
  `;
1474
1474
  }
1475
1475
  });
@@ -1505,13 +1505,386 @@ function runConfigGen(s2sps, outDir) {
1505
1505
  return { written, skipped };
1506
1506
  }
1507
1507
 
1508
+ // src/registry/login.ts
1509
+ import { createInterface as createInterface2 } from "node:readline/promises";
1510
+ import { stdin as input2, stdout as output2 } from "node:process";
1511
+
1512
+ // src/registry/credentials.ts
1513
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "node:fs";
1514
+ import { homedir } from "node:os";
1515
+ import { join as join12 } from "node:path";
1516
+ function credentialsPath() {
1517
+ return join12(homedir(), ".s2script", "credentials.json");
1518
+ }
1519
+ function defaultRegistryUrl() {
1520
+ return process.env.S2SCRIPT_REGISTRY_URL?.replace(/\/$/, "") || "https://s2script.com";
1521
+ }
1522
+ function loadCredentials() {
1523
+ const fromEnv = process.env.S2SCRIPT_TOKEN || process.env.S2SCRIPT_DEPLOY_TOKEN;
1524
+ if (fromEnv) {
1525
+ return { registryUrl: defaultRegistryUrl(), token: fromEnv };
1526
+ }
1527
+ const p = credentialsPath();
1528
+ if (!existsSync7(p)) return null;
1529
+ try {
1530
+ const j = JSON.parse(readFileSync9(p, "utf8"));
1531
+ if (!j.token) return null;
1532
+ return {
1533
+ registryUrl: (j.registryUrl || defaultRegistryUrl()).replace(/\/$/, ""),
1534
+ token: j.token
1535
+ };
1536
+ } catch {
1537
+ return null;
1538
+ }
1539
+ }
1540
+ function saveCredentials(creds) {
1541
+ const dir = join12(homedir(), ".s2script");
1542
+ mkdirSync4(dir, { recursive: true });
1543
+ writeFileSync7(
1544
+ credentialsPath(),
1545
+ JSON.stringify(
1546
+ { registryUrl: creds.registryUrl.replace(/\/$/, ""), token: creds.token },
1547
+ null,
1548
+ 2
1549
+ ) + "\n",
1550
+ { mode: 384 }
1551
+ );
1552
+ }
1553
+
1554
+ // src/registry/login.ts
1555
+ async function loginInteractive(opts) {
1556
+ const registryUrl = (opts?.registryUrl || defaultRegistryUrl()).replace(/\/$/, "");
1557
+ let token = opts?.token || process.env.S2SCRIPT_TOKEN || process.env.S2SCRIPT_DEPLOY_TOKEN;
1558
+ if (!token) {
1559
+ if (opts?.ci || !input2.isTTY) {
1560
+ throw new Error(
1561
+ "no deploy token: set S2SCRIPT_TOKEN or run `s2script login` interactively"
1562
+ );
1563
+ }
1564
+ const rl = createInterface2({ input: input2, output: output2 });
1565
+ try {
1566
+ console.log(`Registry: ${registryUrl}`);
1567
+ console.log("Create a deploy token at /account/tokens on the registry site, then paste it:");
1568
+ token = (await rl.question("token: ")).trim();
1569
+ } finally {
1570
+ rl.close();
1571
+ }
1572
+ }
1573
+ if (!token || !token.startsWith("s2s_")) {
1574
+ throw new Error('token must start with "s2s_"');
1575
+ }
1576
+ const creds = { registryUrl, token };
1577
+ saveCredentials(creds);
1578
+ return creds;
1579
+ }
1580
+
1581
+ // src/registry/deploy.ts
1582
+ import { readFileSync as readFileSync11 } from "node:fs";
1583
+ import { resolve as resolve7 } from "node:path";
1584
+ import AdmZip3 from "adm-zip";
1585
+
1586
+ // src/types-pack.ts
1587
+ import { readFileSync as readFileSync10 } from "node:fs";
1588
+ import { gzipSync } from "node:zlib";
1589
+ function pad(buf, size) {
1590
+ if (buf.length > size) throw new Error(`tar field overflow (${buf.length}>${size})`);
1591
+ const out = Buffer.alloc(size, 0);
1592
+ buf.copy(out);
1593
+ return out;
1594
+ }
1595
+ function tarHeader(name, size, type) {
1596
+ const header = Buffer.alloc(512, 0);
1597
+ pad(Buffer.from(name, "utf8"), 100).copy(header, 0);
1598
+ Buffer.from("0000644\0", "utf8").copy(header, 100);
1599
+ Buffer.from("0000000\0", "utf8").copy(header, 108);
1600
+ Buffer.from("0000000\0", "utf8").copy(header, 116);
1601
+ const sizeOct = size.toString(8).padStart(11, "0") + "\0";
1602
+ Buffer.from(sizeOct, "utf8").copy(header, 124);
1603
+ const mtime = Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0") + "\0";
1604
+ Buffer.from(mtime, "utf8").copy(header, 136);
1605
+ Buffer.from(" ", "utf8").copy(header, 148);
1606
+ header[156] = type.charCodeAt(0);
1607
+ Buffer.from("ustar\0", "utf8").copy(header, 257);
1608
+ Buffer.from("00", "utf8").copy(header, 263);
1609
+ let sum = 0;
1610
+ for (let i = 0; i < 512; i++) sum += header[i];
1611
+ const chk = sum.toString(8).padStart(6, "0") + "\0 ";
1612
+ Buffer.from(chk, "utf8").copy(header, 148);
1613
+ return header;
1614
+ }
1615
+ function tarFile(name, content) {
1616
+ const header = tarHeader(name, content.length, "0");
1617
+ const padLen = (512 - content.length % 512) % 512;
1618
+ return Buffer.concat([header, content, Buffer.alloc(padLen, 0)]);
1619
+ }
1620
+ function packTypesTarball(input3) {
1621
+ const dts = readFileSync10(input3.typesPath);
1622
+ const pkgJson = Buffer.from(
1623
+ JSON.stringify(
1624
+ {
1625
+ name: input3.name,
1626
+ version: input3.version,
1627
+ types: "api.d.ts",
1628
+ s2script: {
1629
+ kind: "interface",
1630
+ ...input3.publishes ? { publishes: input3.publishes } : {}
1631
+ }
1632
+ },
1633
+ null,
1634
+ 2
1635
+ ) + "\n",
1636
+ "utf8"
1637
+ );
1638
+ const tar = Buffer.concat([
1639
+ tarFile("package/package.json", pkgJson),
1640
+ tarFile("package/api.d.ts", dts),
1641
+ Buffer.alloc(1024, 0)
1642
+ // two zero blocks = EOF
1643
+ ]);
1644
+ return gzipSync(tar);
1645
+ }
1646
+
1647
+ // src/registry/client.ts
1648
+ var RegistryError = class extends Error {
1649
+ constructor(message, status, body) {
1650
+ super(message);
1651
+ this.status = status;
1652
+ this.body = body;
1653
+ this.name = "RegistryError";
1654
+ }
1655
+ };
1656
+ var RegistryClient = class {
1657
+ constructor(opts) {
1658
+ this.baseUrl = opts.baseUrl.replace(/\/$/, "");
1659
+ this.token = opts.token;
1660
+ this.fetchFn = opts.fetch ?? fetch;
1661
+ }
1662
+ headers(extra) {
1663
+ const h = { ...extra ?? {} };
1664
+ if (this.token) h.Authorization = `Bearer ${this.token}`;
1665
+ return h;
1666
+ }
1667
+ async deploy(opts) {
1668
+ const fd = new FormData();
1669
+ fd.set("manifest", JSON.stringify(opts.manifest));
1670
+ fd.set("s2sp", new Blob([new Uint8Array(opts.s2sp)]), "plugin.s2sp");
1671
+ if (opts.types && opts.types.length) {
1672
+ fd.set("types", new Blob([new Uint8Array(opts.types)]), "types.tgz");
1673
+ }
1674
+ const res = await this.fetchFn(`${this.baseUrl}/api/v1/deploy`, {
1675
+ method: "POST",
1676
+ headers: this.headers(),
1677
+ body: fd
1678
+ });
1679
+ const body = await res.json().catch(() => ({}));
1680
+ if (!res.ok) {
1681
+ throw new RegistryError(
1682
+ body.error || `deploy failed (${res.status})`,
1683
+ res.status,
1684
+ body
1685
+ );
1686
+ }
1687
+ return body;
1688
+ }
1689
+ async resolve(name, range = "*") {
1690
+ const u = new URL(`${this.baseUrl}/api/v1/resolve`);
1691
+ u.searchParams.set("name", name);
1692
+ u.searchParams.set("range", range);
1693
+ const res = await this.fetchFn(u, { headers: this.headers() });
1694
+ const body = await res.json().catch(() => ({}));
1695
+ if (!res.ok) {
1696
+ throw new RegistryError(
1697
+ body.message || `resolve failed (${res.status})`,
1698
+ res.status,
1699
+ body
1700
+ );
1701
+ }
1702
+ return body;
1703
+ }
1704
+ async downloadTypes(name, version) {
1705
+ const u = new URL(`${this.baseUrl}/api/v1/download/types`);
1706
+ u.searchParams.set("name", name);
1707
+ u.searchParams.set("version", version);
1708
+ const res = await this.fetchFn(u, { headers: this.headers() });
1709
+ if (!res.ok) {
1710
+ const body = await res.json().catch(() => ({}));
1711
+ throw new RegistryError(
1712
+ body.message || `types download failed (${res.status})`,
1713
+ res.status,
1714
+ body
1715
+ );
1716
+ }
1717
+ return Buffer.from(await res.arrayBuffer());
1718
+ }
1719
+ async meta(name) {
1720
+ const u = new URL(`${this.baseUrl}/api/v1/meta`);
1721
+ u.searchParams.set("name", name);
1722
+ const res = await this.fetchFn(u, { headers: this.headers() });
1723
+ const body = await res.json().catch(() => ({}));
1724
+ if (!res.ok) {
1725
+ throw new RegistryError(`meta failed (${res.status})`, res.status, body);
1726
+ }
1727
+ return body;
1728
+ }
1729
+ };
1730
+
1731
+ // src/registry/deploy.ts
1732
+ async function deployPlugin(opts) {
1733
+ const absDir = resolve7(opts.dir);
1734
+ const pkg = JSON.parse(readFileSync11(resolve7(absDir, "package.json"), "utf8"));
1735
+ const gate = assertPublishesTypes(pkg, absDir);
1736
+ if (!gate.ok) throw new Error(`publish gate failed: ${gate.error}`);
1737
+ const creds = loadCredentials();
1738
+ if (!creds?.token) {
1739
+ throw new Error(
1740
+ opts.ci ? "S2SCRIPT_TOKEN required in CI" : "not logged in \u2014 run `s2s login` or set S2SCRIPT_TOKEN"
1741
+ );
1742
+ }
1743
+ const outPath = await buildPlugin(absDir, opts.packagesDir);
1744
+ const s2sp = readFileSync11(outPath);
1745
+ const zip = new AdmZip3(s2sp);
1746
+ const manifestEntry = zip.getEntry("manifest.json");
1747
+ if (!manifestEntry) {
1748
+ throw new Error(`built archive missing manifest.json: ${outPath}`);
1749
+ }
1750
+ const manifest = JSON.parse(manifestEntry.getData().toString("utf8"));
1751
+ let types = null;
1752
+ if (gate.typesPath) {
1753
+ types = packTypesTarball({
1754
+ name: typeof manifest.id === "string" ? manifest.id : pkg.name,
1755
+ version: typeof manifest.version === "string" ? manifest.version : pkg.version,
1756
+ typesPath: gate.typesPath,
1757
+ publishes: hasPublishes(manifest.publishes) ? manifest.publishes : void 0
1758
+ });
1759
+ }
1760
+ const client = new RegistryClient({
1761
+ baseUrl: opts.registryUrl || creds.registryUrl || defaultRegistryUrl(),
1762
+ token: creds.token
1763
+ });
1764
+ return client.deploy({ manifest, s2sp, types });
1765
+ }
1766
+
1767
+ // src/registry/add.ts
1768
+ import {
1769
+ mkdirSync as mkdirSync5,
1770
+ writeFileSync as writeFileSync9,
1771
+ readFileSync as readFileSync13,
1772
+ existsSync as existsSync9
1773
+ } from "node:fs";
1774
+ import { join as join14, resolve as resolve8 } from "node:path";
1775
+ import { gunzipSync } from "node:zlib";
1776
+
1777
+ // src/registry/npmrc.ts
1778
+ import { existsSync as existsSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
1779
+ import { join as join13 } from "node:path";
1780
+ function npmCompatRegistryUrl(registryUrl) {
1781
+ return `${registryUrl.replace(/\/$/, "")}/npm/`;
1782
+ }
1783
+ function scopeFromPackageName(name) {
1784
+ if (!name.startsWith("@")) return null;
1785
+ const slash = name.indexOf("/");
1786
+ if (slash < 2) return null;
1787
+ return name.slice(1, slash);
1788
+ }
1789
+ function ensureScopeNpmrc(projectDir, packageName, registryUrl) {
1790
+ const scope = scopeFromPackageName(packageName);
1791
+ if (!scope || scope === "s2script") return null;
1792
+ const compat = npmCompatRegistryUrl(registryUrl);
1793
+ const key = `@${scope}:registry`;
1794
+ const line = `${key}=${compat}`;
1795
+ const npmrcPath = join13(projectDir, ".npmrc");
1796
+ let existing = existsSync8(npmrcPath) ? readFileSync12(npmrcPath, "utf8") : "";
1797
+ const lines = existing.split(/\r?\n/);
1798
+ const idx = lines.findIndex((l) => l.trimStart().startsWith(`${key}=`) || l.trimStart().startsWith(`${key} =`));
1799
+ if (idx >= 0) {
1800
+ lines[idx] = line;
1801
+ } else {
1802
+ if (existing && !existing.endsWith("\n")) lines.push("");
1803
+ lines.push(line);
1804
+ }
1805
+ const out = lines.filter((l, i, a) => !(l === "" && a[i - 1] === "")).join("\n");
1806
+ writeFileSync8(npmrcPath, out.endsWith("\n") ? out : out + "\n");
1807
+ return line;
1808
+ }
1809
+
1810
+ // src/registry/add.ts
1811
+ function parseSpec(spec) {
1812
+ if (spec.startsWith("@")) {
1813
+ const at2 = spec.indexOf("@", 1);
1814
+ if (at2 < 0) return { name: spec, range: "*" };
1815
+ return { name: spec.slice(0, at2), range: spec.slice(at2 + 1) || "*" };
1816
+ }
1817
+ const at = spec.lastIndexOf("@");
1818
+ if (at <= 0) return { name: spec, range: "*" };
1819
+ return { name: spec.slice(0, at), range: spec.slice(at + 1) || "*" };
1820
+ }
1821
+ function extractTypesTarball(tgz, outDir) {
1822
+ const tar = gunzipSync(tgz);
1823
+ mkdirSync5(outDir, { recursive: true });
1824
+ let offset = 0;
1825
+ let wroteContract = false;
1826
+ while (offset + 512 <= tar.length) {
1827
+ const header = tar.subarray(offset, offset + 512);
1828
+ offset += 512;
1829
+ if (header.every((b) => b === 0)) break;
1830
+ const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
1831
+ const sizeOct = header.subarray(124, 135).toString("utf8").replace(/\0.*$/, "").trim();
1832
+ const size = parseInt(sizeOct, 8) || 0;
1833
+ const content = tar.subarray(offset, offset + size);
1834
+ offset += size + (512 - size % 512) % 512;
1835
+ if (!name || name.endsWith("/")) continue;
1836
+ const base = name.replace(/^package\//, "");
1837
+ if (base === "package.json") {
1838
+ writeFileSync9(join14(outDir, "package.json"), content);
1839
+ } else if (base === "api.d.ts" || base === "index.d.ts") {
1840
+ writeFileSync9(join14(outDir, "index.d.ts"), content);
1841
+ wroteContract = true;
1842
+ }
1843
+ }
1844
+ if (!wroteContract && !existsSync9(join14(outDir, "index.d.ts"))) {
1845
+ throw new Error("types tarball missing package/api.d.ts (or index.d.ts)");
1846
+ }
1847
+ }
1848
+ async function addPackage(opts) {
1849
+ const absDir = resolve8(opts.pluginDir);
1850
+ const { name, range } = parseSpec(opts.spec);
1851
+ const creds = loadCredentials();
1852
+ const client = new RegistryClient({
1853
+ baseUrl: opts.registryUrl || creds?.registryUrl || defaultRegistryUrl(),
1854
+ token: creds?.token
1855
+ });
1856
+ const resolved = await client.resolve(name, range);
1857
+ if (!resolved.hasTypes) {
1858
+ throw new Error(`${name}@${resolved.version} has no types artifact (runtime-only package?)`);
1859
+ }
1860
+ const tgz = await client.downloadTypes(name, resolved.version);
1861
+ const typesDir = join14(absDir, ".s2script", "types", ...name.split("/"));
1862
+ extractTypesTarball(tgz, typesDir);
1863
+ const pkgPath = join14(absDir, "package.json");
1864
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf8"));
1865
+ pkg.s2script = pkg.s2script ?? {};
1866
+ pkg.s2script.pluginDependencies = pkg.s2script.pluginDependencies ?? {};
1867
+ const major = resolved.version.split(".")[0] ?? "0";
1868
+ pkg.s2script.pluginDependencies[name] = range.startsWith("^") || range === "*" ? `^${major}.0.0` : range.includes(".") ? range : `^${resolved.version}`;
1869
+ writeFileSync9(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1870
+ const registryUrl = opts.registryUrl || creds?.registryUrl || defaultRegistryUrl();
1871
+ const npmrcLine = ensureScopeNpmrc(absDir, name, registryUrl);
1872
+ return {
1873
+ name: resolved.name,
1874
+ version: resolved.version,
1875
+ reviewState: resolved.reviewState,
1876
+ typesDir,
1877
+ npmrcLine
1878
+ };
1879
+ }
1880
+
1508
1881
  // src/cli.ts
1509
1882
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1510
- import { dirname as dirname3, join as join12, resolve as resolve7 } from "node:path";
1883
+ import { dirname as dirname3, join as join15, resolve as resolve9 } from "node:path";
1511
1884
  var argv = process.argv.slice(2);
1512
1885
  var command = argv[0];
1513
1886
  function repoRootFromCli() {
1514
- return join12(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "..");
1887
+ return join15(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "..");
1515
1888
  }
1516
1889
  function parseFlag(args, name) {
1517
1890
  const eq = args.find((a) => a.startsWith(`${name}=`));
@@ -1600,6 +1973,68 @@ if (command === "gen-schema") {
1600
1973
  console.error(String(e instanceof Error ? e.message : e));
1601
1974
  process.exit(1);
1602
1975
  }
1976
+ } else if (command === "login") {
1977
+ const args = argv.slice(1);
1978
+ try {
1979
+ const creds = await loginInteractive({
1980
+ token: parseFlag(args, "--token"),
1981
+ registryUrl: parseFlag(args, "--registry") || defaultRegistryUrl(),
1982
+ ci: hasFlag(args, "--ci")
1983
+ });
1984
+ console.log(`logged in \u2192 ${creds.registryUrl} (credentials saved)`);
1985
+ } catch (e) {
1986
+ console.error(String(e instanceof Error ? e.message : e));
1987
+ process.exit(1);
1988
+ }
1989
+ } else if (command === "deploy" || command === "publish") {
1990
+ const args = argv.slice(1);
1991
+ const pos = positionals(args, ["--packages-dir", "--registry"]);
1992
+ const dir = pos[0] ?? ".";
1993
+ try {
1994
+ const packagesDir = resolvePackagesDir({
1995
+ explicit: parseFlag(args, "--packages-dir"),
1996
+ pluginDir: resolve9(dir),
1997
+ fromCliUrl: import.meta.url
1998
+ });
1999
+ const result = await deployPlugin({
2000
+ dir,
2001
+ packagesDir,
2002
+ registryUrl: parseFlag(args, "--registry"),
2003
+ ci: hasFlag(args, "--ci")
2004
+ });
2005
+ console.log(`deployed ${result.name}@${result.version} (${result.reviewState})`);
2006
+ if (result.reviewState === "unreviewed") {
2007
+ console.log("note: Not reviewed by s2script \u2014 listed with a disclaimer until approved.");
2008
+ }
2009
+ } catch (e) {
2010
+ console.error(String(e instanceof Error ? e.message : e));
2011
+ process.exit(1);
2012
+ }
2013
+ } else if (command === "add") {
2014
+ const args = argv.slice(1);
2015
+ const pos = positionals(args, ["--dir", "--registry"]);
2016
+ const spec = pos[0];
2017
+ if (!spec) {
2018
+ console.error("Usage: s2s add <pkg>[@range]");
2019
+ process.exit(1);
2020
+ }
2021
+ try {
2022
+ const result = await addPackage({
2023
+ pluginDir: parseFlag(args, "--dir") || ".",
2024
+ spec,
2025
+ registryUrl: parseFlag(args, "--registry")
2026
+ });
2027
+ console.log(`added ${result.name}@${result.version} \u2192 ${result.typesDir}`);
2028
+ if (result.npmrcLine) {
2029
+ console.log(`npmrc: ${result.npmrcLine} (types-only; npm install ${result.name} also works)`);
2030
+ }
2031
+ if (result.reviewState === "unreviewed") {
2032
+ console.log("note: Not reviewed by s2script \u2014 types pulled; use at your own risk.");
2033
+ }
2034
+ } catch (e) {
2035
+ console.error(String(e instanceof Error ? e.message : e));
2036
+ process.exit(1);
2037
+ }
1603
2038
  } else if (command === "config" && argv[1] === "gen") {
1604
2039
  const args = argv.slice(2);
1605
2040
  const outDir = parseFlag(args, "--out") ?? process.cwd();
@@ -1623,7 +2058,7 @@ if (command === "gen-schema") {
1623
2058
  try {
1624
2059
  const packagesDir = resolvePackagesDir({
1625
2060
  explicit: packagesDirFlag,
1626
- pluginDir: resolve7(dir),
2061
+ pluginDir: resolve9(dir),
1627
2062
  fromCliUrl: import.meta.url
1628
2063
  });
1629
2064
  console.log(await buildPlugin(dir, packagesDir));
@@ -1633,7 +2068,7 @@ if (command === "gen-schema") {
1633
2068
  }
1634
2069
  } else {
1635
2070
  console.error(
1636
- "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 config gen <plugin.s2sp...> --out <dir>\n s2s gen-schema [--check]\n s2s gen-events [--check]\n s2s gen-nav [--check]"
2071
+ "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)"
1637
2072
  );
1638
2073
  process.exit(1);
1639
2074
  }
package/entity.d.ts CHANGED
@@ -15,6 +15,8 @@
15
15
  * time" footgun). Obtain refs from the engine (events, `findByClass`, `readHandle`, …).
16
16
  */
17
17
  export declare class EntityRef {
18
+ /** This entity's slot index in the game entity system. Paired with the host-minted
19
+ * {@link EntityRef.id} for slot validation (books first, then identity-slot match). */
18
20
  readonly index: number;
19
21
  /** The host-minted liveness id for this ref (books key). Not the raw engine serial. */
20
22
  readonly id: number;
@@ -80,21 +82,33 @@ export declare class EntityRef {
80
82
  /** Write an int32 at the end of a pointer chain (each hop deref'd, liveness-gated at the root). Returns
81
83
  * false on a stale ref or a null hop. Used to clear a flag on a pointer-referenced sub-object. */
82
84
  writeInt32Via(pathOffs: number[], finalOff: number, value: number): boolean;
85
+ /** Read an i8 (sign-extended) at the end of a pointer chain (each hop deref'd, liveness-gated at the root); null if the root is stale or any hop is null. */
83
86
  readInt8Via(pathOffs: number[], finalOff: number): number | null;
87
+ /** Read an i16 (sign-extended) through a pointer chain; null if the root is stale or any hop is null. */
84
88
  readInt16Via(pathOffs: number[], finalOff: number): number | null;
89
+ /** Read a u8 through a pointer chain; null if the root is stale or any hop is null. */
85
90
  readUInt8Via(pathOffs: number[], finalOff: number): number | null;
91
+ /** Read a u16 through a pointer chain; null if the root is stale or any hop is null. */
86
92
  readUInt16Via(pathOffs: number[], finalOff: number): number | null;
93
+ /** Read a u32 through a pointer chain; null if the root is stale or any hop is null. */
87
94
  readUInt32Via(pathOffs: number[], finalOff: number): number | null;
95
+ /** Read an f32 through a pointer chain; null if the root is stale or any hop is null. */
88
96
  readFloat32Via(pathOffs: number[], finalOff: number): number | null;
97
+ /** Read a bool through a pointer chain; null if the root is stale or any hop is null. */
89
98
  readBoolVia(pathOffs: number[], finalOff: number): boolean | null;
99
+ /** Read a u64 as a BigInt through a pointer chain; null if the root is stale or any hop is null. */
90
100
  readUInt64Via(pathOffs: number[], finalOff: number): bigint | null;
101
+ /** Read an i64 as a BigInt through a pointer chain; null if the root is stale or any hop is null. */
91
102
  readInt64Via(pathOffs: number[], finalOff: number): bigint | null;
103
+ /** Decode a `CEntityHandle` at the end of a pointer chain into a liveness-gated {@link EntityRef}; null if the root is stale, any hop is null, or the handle is dead. */
92
104
  readHandleVia(pathOffs: number[], finalOff: number): EntityRef | null;
93
105
  /** Write a scalar through a pointer chain (write mirror of `read*Via`). Liveness-gated at the root;
94
106
  * returns false on a stale ref, an unresolved hop, or a bad offset/kind. Does NOT notifyStateChanged —
95
107
  * the caller decides (many sub-object fields, e.g. the fire gate, are server-authoritative).
96
108
  * (`writeInt32Via` is declared above alongside `readInt32Via`.) */
97
109
  writeFloat32Via(pathOffs: number[], finalOff: number, value: number): boolean;
110
+ /** Write a bool through a pointer chain (write mirror of {@link EntityRef.readBoolVia}; shares the
111
+ * no-notify semantics on {@link EntityRef.writeFloat32Via}). False on a stale ref, an unresolved hop, or a bad offset/kind. */
98
112
  writeBoolVia(pathOffs: number[], finalOff: number, value: boolean): boolean;
99
113
  /** Read a `CEntityHandle` at `offset`, decode it, and return a live `EntityRef` — or null if stale/invalid. */
100
114
  readHandle(offset: number): EntityRef | null;
@@ -138,7 +152,12 @@ export type EntityKeyValueMap = { [key: string]: string | number | boolean };
138
152
  /** Create a new entity by class name. WITHOUT keyvalues: create only — set fields, then call
139
153
  * `.spawn()`. WITH keyvalues: create + DispatchSpawn(keyvalues) in one call — a non-null result is
140
154
  * a LIVE, SPAWNED entity (on spawn failure the entity is removed and null returned). The created
141
- * entity is game-world-owned (NOT auto-removed on plugin unload) — the plugin owns cleanup. */
155
+ * entity is game-world-owned (NOT auto-removed on plugin unload) — the plugin owns cleanup.
156
+ * @example
157
+ * import { createEntity } from "@s2script/sdk/entity";
158
+ * const wt = createEntity("point_worldtext", { message: "s2-ekv-proof", fullbright: true });
159
+ * if (wt) wt.remove();
160
+ */
142
161
  export declare function createEntity(className: string, keyvalues?: EntityKeyValueMap): EntityRef | null;
143
162
 
144
163
  /** The payload delivered to an `Entity.onOutput` handler. */
@@ -155,6 +174,13 @@ export interface OutputEvent {
155
174
  delay: number;
156
175
  }
157
176
 
177
+ /**
178
+ * Entry point for entity lookup by class name.
179
+ * @example
180
+ * import { Entity } from "@s2script/sdk/entity";
181
+ * const triggers = Entity.findByClass("trigger_multiple");
182
+ * for (const t of triggers) console.log(t.index, t.name);
183
+ */
158
184
  export declare const Entity: {
159
185
  /** Find every entity whose designer-name (class) exactly matches `className`. Returns liveness-gated refs. */
160
186
  findByClass(className: string): EntityRef[];