@s2script/sdk 0.5.0 → 0.6.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/db.d.ts CHANGED
@@ -1,26 +1,64 @@
1
1
  /** @s2script/db — engine-generic async SQLite database. NO runtime code (injected as __s2pkg_db). */
2
+
3
+ /** A value bindable as a SQL parameter or returned in a {@link Row}. */
2
4
  export type SqlValue = string | number | boolean | null;
5
+ /** One query result row — a column-name→{@link SqlValue} map. */
3
6
  export type Row = Record<string, SqlValue>;
4
- export interface ExecuteResult { changes: number; lastInsertId: number; }
7
+ /** The outcome of a non-SELECT statement run via {@link Database.execute}. */
8
+ export interface ExecuteResult {
9
+ /** Rows affected by the statement (INSERT/UPDATE/DELETE). */
10
+ changes: number;
11
+ /** Rowid of the last inserted row (0 if the statement inserted nothing). */
12
+ lastInsertId: number;
13
+ }
5
14
 
15
+ /** A driver-owned connection — what a {@link Driver} hands back from {@link Driver.connect}. */
6
16
  export interface DriverConnection {
17
+ /** Run a SELECT and resolve every result {@link Row}; `params` bind `?` placeholders. */
7
18
  query(sql: string, params?: SqlValue[]): Promise<Row[]>;
19
+ /** Run a non-SELECT statement and resolve its {@link ExecuteResult}; `params` bind `?` placeholders. */
8
20
  execute(sql: string, params?: SqlValue[]): Promise<ExecuteResult>;
21
+ /** Close the underlying connection. */
9
22
  close(): Promise<void>;
10
23
  }
11
- export interface ConnectionConfig { driver: string; name: string; [k: string]: unknown; }
24
+ /** Named-connection config resolved by {@link Database.open} and passed to {@link Driver.connect}. */
25
+ export interface ConnectionConfig {
26
+ /** Driver name to connect with (e.g. `"sqlite"`). */
27
+ driver: string;
28
+ /** The connection's configured name. */
29
+ name: string;
30
+ /** Driver-specific options (path, host, credentials, …). */
31
+ [k: string]: unknown;
32
+ }
33
+ /** A registered database backend; register a custom one via {@link Database.registerDriver}. */
12
34
  export interface Driver {
35
+ /** The driver's name, matched against {@link ConnectionConfig.driver}. */
13
36
  readonly name: string;
37
+ /** Open a connection for `config`, resolving a {@link DriverConnection}. */
14
38
  connect(config: ConnectionConfig): Promise<DriverConnection>;
15
39
  }
16
40
  /** A live database connection (delegates to its driver). */
17
41
  export interface Database {
42
+ /** Run a SELECT and resolve every result {@link Row}; `params` bind `?` placeholders. */
18
43
  query(sql: string, params?: SqlValue[]): Promise<Row[]>;
44
+ /** Run a non-SELECT statement and resolve its {@link ExecuteResult}; `params` bind `?` placeholders. */
19
45
  execute(sql: string, params?: SqlValue[]): Promise<ExecuteResult>;
46
+ /** Close the connection. */
20
47
  close(): Promise<void>;
21
48
  }
49
+ /** Entry point for opening databases and registering drivers. */
22
50
  export declare const Database: {
23
- /** Open a connection by name (default "default"). Resolves the driver + config, then connects. */
51
+ /**
52
+ * Open a connection by name (default `"default"`). Resolves the driver + config, then connects.
53
+ * @param name - Connection name from config; omit for `"default"`.
54
+ * @returns Resolves the connected {@link Database} handle.
55
+ * @throws Rejects if the name is unknown or the driver fails to connect.
56
+ * @example
57
+ * import { Database } from "@s2script/sdk/db";
58
+ * const db = await Database.open("clientprefs");
59
+ * await db.execute("CREATE TABLE IF NOT EXISTS cookies (steamid TEXT, name TEXT, value TEXT)");
60
+ * const rows = await db.query("SELECT value FROM cookies WHERE steamid = ?", ["76561199999999999"]);
61
+ */
24
62
  open(name?: string): Promise<Database>;
25
63
  /** Register a custom driver (per-plugin context). SQLite is built in. */
26
64
  registerDriver(driver: Driver): void;
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
  }