@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/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)) {
@@ -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
- return process.env.S2SCRIPT_REGISTRY_URL?.replace(/\/$/, "") || "https://s2script.com";
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()).replace(/\/$/, ""),
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.replace(/\/$/, ""), token: creds.token },
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 fd2 = new FormData();
3135
- fd2.set("manifest", JSON.stringify(opts.manifest));
3136
- fd2.set("s2sp", new Blob([new Uint8Array(opts.s2sp)]), "plugin.s2sp");
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
- fd2.set("types", new Blob([new Uint8Array(opts.types)]), "types.tgz");
3527
+ entries["types.tgz"] = new Uint8Array(opts.types);
3139
3528
  }
3140
- const res = await this.fetchFn(`${this.baseUrl}/api/v1/deploy`, {
3529
+ const body = zipSync(entries, { level: 0 });
3530
+ const res = await this.request("deploy", `${this.baseUrl}/api/v1/deploy`, {
3141
3531
  method: "POST",
3142
- headers: this.headers(),
3143
- body: fd2
3532
+ body,
3533
+ contentType: "application/octet-stream"
3144
3534
  });
3145
- const body = await res.json().catch(() => ({}));
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.fetchFn(u4, { headers: this.headers() });
3160
- const body = await res.json().catch(() => ({}));
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.fetchFn(u4, { headers: this.headers() });
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.fetchFn(u4, { headers: this.headers() });
3189
- const body = await res.json().catch(() => ({}));
3190
- if (!res.ok) {
3191
- throw new RegistryError(`meta failed (${res.status})`, res.status, body);
3192
- }
3193
- return body;
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 mkdirSync4,
3430
- writeFileSync as writeFileSync6,
3431
- existsSync as existsSync9,
3992
+ mkdirSync as mkdirSync5,
3993
+ writeFileSync as writeFileSync7,
3994
+ existsSync as existsSync11,
3432
3995
  readdirSync as readdirSync2,
3433
- readFileSync as readFileSync10,
3996
+ readFileSync as readFileSync11,
3434
3997
  symlinkSync
3435
3998
  } from "node:fs";
3436
- 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";
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
- join10(here, "..", "package.json"),
4005
+ join11(here, "..", "package.json"),
3443
4006
  // dist/cli.js → packages/sdk/package.json
3444
- join10(here, "..", "..", "package.json")
4007
+ join11(here, "..", "..", "package.json")
3445
4008
  // src/create → packages/sdk/package.json
3446
4009
  ];
3447
4010
  for (const p of candidates) {
3448
- if (!existsSync9(p)) continue;
4011
+ if (!existsSync11(p)) continue;
3449
4012
  try {
3450
- const pkg = JSON.parse(readFileSync10(p, "utf8"));
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
- join10(here, "..", ".."),
4023
+ join11(here, "..", ".."),
3461
4024
  // dist/cli.js → packages
3462
- join10(here, "..", "..", "..")
4025
+ join11(here, "..", "..", "..")
3463
4026
  // src/create → packages
3464
4027
  ];
3465
4028
  for (const c2 of candidates) {
3466
- if (isPackagesDir(c2)) return resolve10(c2);
4029
+ if (isPackagesDir(c2)) return resolve12(c2);
3467
4030
  }
3468
4031
  return void 0;
3469
4032
  }
3470
4033
  function defaultNameFromPath(dir) {
3471
- const base = basename(resolve10(dir));
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, resolve11 = resolvePublishedVersion) {
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}` : resolve11(`@s2script/${n3}`);
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 = join10(packagesDir, n3);
3510
- 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;
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
- 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"]
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 = join10(targetPath, "node_modules");
3591
- mkdirSync4(nm, { recursive: true });
3592
- const dest = join10(nm, "@s2script");
3593
- 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);
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 = resolve10(opts.path ?? ".");
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 (existsSync9(targetPath)) {
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
- mkdirSync4(join10(targetPath, "src"), { recursive: true });
3669
- writeFileSync6(
3670
- join10(targetPath, "package.json"),
4236
+ mkdirSync5(join11(targetPath, "src"), { recursive: true });
4237
+ writeFileSync7(
4238
+ join11(targetPath, "package.json"),
3671
4239
  packageJsonContent(name, game, version, localPackagesDir)
3672
4240
  );
3673
- writeFileSync6(join10(targetPath, "tsconfig.json"), tsconfigJson());
3674
- writeFileSync6(join10(targetPath, "eslint.config.mjs"), eslintConfig());
3675
- writeFileSync6(join10(targetPath, "src", "plugin.ts"), pluginSource(game));
3676
- 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());
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 run4(argv2) {
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()).replace(/\/$/, "");
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
- log2.info(
3740
- `Sign in (or create an account), then mint a deploy token at:
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
- token = (await password2({
3744
- message: "Paste your deploy token",
3745
- validate: (v) => (v ?? "").trim().startsWith("s2s_") ? void 0 : 'token must start with "s2s_"'
3746
- })).trim();
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 run5(argv2) {
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 readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "node:fs";
3776
- 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";
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
- mkdirSync5(outDir, { recursive: true });
3823
- const path = join11(outDir, configFileName(id));
3824
- writeFileSync7(path, generateDefaultJsonc(config));
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(readFileSync11(s2spPath))["manifest.json"];
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 run6(argv2) {
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 join15 } from "node:path";
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 readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
3880
- 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";
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 resolve11 = `off(${S(f.declaringClass)},${S(f.rawName)})`;
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(${resolve11}, ${f.strLen}); }`;
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]}(${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(); }`;
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(${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]); }`;
4081
4782
  } else {
4082
- entry = `get: function () { return this.ref.${READ[f.accessorKind]}(${resolve11}); }`;
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 = ${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); }`;
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(readFileSync12(join12(repoRoot2, CATALOG_PATH), "utf8"));
4113
- 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"));
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 = join12(repoRoot2, rel);
4823
+ const abs = join13(repoRoot2, rel);
4123
4824
  if (opts.check) {
4124
4825
  let cur = "";
4125
4826
  try {
4126
- cur = readFileSync12(abs, "utf8");
4827
+ cur = readFileSync13(abs, "utf8");
4127
4828
  } catch {
4128
4829
  }
4129
4830
  if (cur !== content) drift.push(rel);
4130
4831
  } else {
4131
- writeFileSync8(abs, content);
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 readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
4142
- 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";
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(readFileSync13(join13(repoRoot2, CATALOG_PATH2), "utf8"));
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 = join13(repoRoot2, DTS_OUT2);
4932
+ const abs = join14(repoRoot2, DTS_OUT2);
4232
4933
  if (opts.check) {
4233
4934
  let cur = "";
4234
4935
  try {
4235
- cur = readFileSync13(abs, "utf8");
4936
+ cur = readFileSync14(abs, "utf8");
4236
4937
  } catch {
4237
4938
  }
4238
4939
  if (cur !== dts) drift.push(DTS_OUT2);
4239
4940
  } else {
4240
- writeFileSync9(abs, dts);
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 readFileSync14, writeFileSync as writeFileSync10 } from "node:fs";
4247
- 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";
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 resolve11 = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
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, ${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(); }`;
4327
5028
  } else if (kind === "i64") {
4328
- 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(); }`;
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, ${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]); }`;
4332
5033
  } else if (kind === "handle") {
4333
- getter = `get: function () { return this.root.readHandleVia(this.path, ${resolve11}); }`;
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, ${resolve11}); }`;
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(readFileSync14(join14(repoRoot2, TARGETS_PATH), "utf8"));
4410
- 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"));
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 = join14(repoRoot2, rel);
5119
+ const abs = join15(repoRoot2, rel);
4419
5120
  if (opts.check) {
4420
5121
  let cur = "";
4421
5122
  try {
4422
- cur = readFileSync14(abs, "utf8");
5123
+ cur = readFileSync15(abs, "utf8");
4423
5124
  } catch {
4424
5125
  }
4425
5126
  if (cur !== content) drift.push(rel);
4426
5127
  } else {
4427
- writeFileSync10(abs, content);
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 join15(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "..");
5136
+ return join16(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "..");
4436
5137
  }
4437
- async function run7(kind, argv2) {
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: run4 },
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: "login", summary: "Save a registry deploy token", run: run5 },
4486
- { name: "config", summary: "Emit a plugin's default config file(s)", run: run6 },
4487
- { name: "gen-schema", summary: "Regenerate schema accessors", run: (a2) => run7("schema", a2) },
4488
- { name: "gen-events", summary: "Regenerate the event catalog", run: (a2) => run7("events", a2) },
4489
- { 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) }
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 run8() {
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 run8();
5219
+ await run9();
4518
5220
  } else {
4519
5221
  usage();
4520
5222
  process.exit(1);