@s2script/sdk 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/dist/cli.js +784 -303
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -668,8 +668,8 @@ var require_major = __commonJS({
668
668
  "../../node_modules/semver/functions/major.js"(exports, module) {
669
669
  "use strict";
670
670
  var SemVer = require_semver();
671
- var major = (a2, loose) => new SemVer(a2, loose).major;
672
- module.exports = major;
671
+ var major2 = (a2, loose) => new SemVer(a2, loose).major;
672
+ module.exports = major2;
673
673
  }
674
674
  });
675
675
 
@@ -915,12 +915,12 @@ var require_coerce = __commonJS({
915
915
  if (match === null) {
916
916
  return null;
917
917
  }
918
- const major = match[2];
918
+ const major2 = match[2];
919
919
  const minor = match[3] || "0";
920
920
  const patch = match[4] || "0";
921
921
  const prerelease = options.includePrerelease && match[5] ? `-${match[5]}` : "";
922
- const build2 = options.includePrerelease && match[6] ? `+${match[6]}` : "";
923
- return parse(`${major}.${minor}.${patch}${prerelease}${build2}`, options);
922
+ const build3 = options.includePrerelease && match[6] ? `+${match[6]}` : "";
923
+ return parse(`${major2}.${minor}.${patch}${prerelease}${build3}`, options);
924
924
  };
925
925
  module.exports = coerce;
926
926
  }
@@ -1991,7 +1991,7 @@ var require_semver2 = __commonJS({
1991
1991
  var clean = require_clean();
1992
1992
  var inc = require_inc();
1993
1993
  var diff = require_diff();
1994
- var major = require_major();
1994
+ var major2 = require_major();
1995
1995
  var minor = require_minor();
1996
1996
  var patch = require_patch();
1997
1997
  var prerelease = require_prerelease();
@@ -2030,7 +2030,7 @@ var require_semver2 = __commonJS({
2030
2030
  clean,
2031
2031
  inc,
2032
2032
  diff,
2033
- major,
2033
+ major: major2,
2034
2034
  minor,
2035
2035
  patch,
2036
2036
  prerelease,
@@ -2076,10 +2076,10 @@ var require_semver2 = __commonJS({
2076
2076
  });
2077
2077
 
2078
2078
  // src/commands/build.ts
2079
- import { resolve as resolve8 } from "node:path";
2079
+ import { resolve as resolve11 } from "node:path";
2080
2080
 
2081
2081
  // src/build.ts
2082
- import * as esbuild from "esbuild";
2082
+ import * as esbuild2 from "esbuild";
2083
2083
 
2084
2084
  // ../../node_modules/fflate/esm/index.mjs
2085
2085
  import { createRequire } from "module";
@@ -3068,13 +3068,13 @@ function unzipSync(data, opts) {
3068
3068
  }
3069
3069
 
3070
3070
  // src/build.ts
3071
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, mkdirSync, rmSync as rmSync2 } from "node:fs";
3072
- import { resolve as resolve7, join as join8, sep, basename } from "node:path";
3071
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, rmSync as rmSync2 } from "node:fs";
3072
+ import { resolve as resolve10, join as join11, sep as sep2, basename } from "node:path";
3073
3073
 
3074
3074
  // src/typecheck/typecheck.ts
3075
3075
  import ts from "typescript";
3076
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5, writeFileSync, rmSync, mkdtempSync } from "node:fs";
3077
- import { join as join6, resolve as resolve5 } from "node:path";
3076
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync6, writeFileSync, rmSync, mkdtempSync } from "node:fs";
3077
+ import { join as join7, resolve as resolve6 } from "node:path";
3078
3078
  import { tmpdir } from "node:os";
3079
3079
 
3080
3080
  // src/packages-resolve.ts
@@ -3414,6 +3414,11 @@ function readWorkspace(root) {
3414
3414
  }
3415
3415
  return { workspace: { root: absRoot, pkg, plugins, libs }, problems };
3416
3416
  }
3417
+ function memberForDir(ws, dir) {
3418
+ const abs = toPosix(resolve2(dir));
3419
+ for (const m of [...ws.plugins, ...ws.libs]) if (toPosix(m.dir) === abs) return m;
3420
+ return null;
3421
+ }
3417
3422
 
3418
3423
  // src/publishes.ts
3419
3424
  import { createHash } from "node:crypto";
@@ -3630,32 +3635,156 @@ function resolveSiblingContracts(pluginDir, deps) {
3630
3635
  return { siblings, shadowedCopies };
3631
3636
  }
3632
3637
 
3638
+ // src/libraries.ts
3639
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
3640
+ import { join as join6, resolve as resolve5 } from "node:path";
3641
+ function packageKind(pkg) {
3642
+ const k = pkg.s2script?.kind ?? "plugin";
3643
+ if (k !== "plugin" && k !== "library") {
3644
+ throw new Error(`unknown s2script.kind ${JSON.stringify(k)} \u2014 expected "plugin" or "library"`);
3645
+ }
3646
+ return k;
3647
+ }
3648
+ function librariesRoot(pluginDir) {
3649
+ return join6(pluginDir, ".s2script", "libs");
3650
+ }
3651
+ function localLibraryDir(pluginDir, name) {
3652
+ const segs = name.split("/");
3653
+ if (segs.some((s) => s === "" || s === "." || s === "..")) return null;
3654
+ const p = join6(librariesRoot(pluginDir), ...segs);
3655
+ return existsSync5(join6(p, "index.d.ts")) ? p : null;
3656
+ }
3657
+ function resolveLibrarySibling(pluginDir, name) {
3658
+ const absDir = resolve5(pluginDir);
3659
+ const root = findWorkspaceRoot(absDir);
3660
+ if (root === null) return null;
3661
+ const scan = scanWorkspace(root);
3662
+ const ws = scan.workspace;
3663
+ if (memberForDir(ws, absDir) === null) return null;
3664
+ const named2 = [...ws.libs, ...ws.plugins].filter((m) => m.name === name);
3665
+ let lib;
3666
+ for (const m of named2) {
3667
+ let kind;
3668
+ try {
3669
+ kind = packageKind(m.pkg);
3670
+ } catch (e) {
3671
+ throw new Error(`library ${name} (workspace member at ${m.relDir}): ${e.message}`);
3672
+ }
3673
+ if (kind === "library") {
3674
+ lib = m;
3675
+ break;
3676
+ }
3677
+ }
3678
+ if (lib === void 0) {
3679
+ if (scan.problems.length > 0) {
3680
+ console.warn(
3681
+ `WARN: ${name}: no workspace sibling found, but ${scan.problems.length} workspace member${scan.problems.length === 1 ? "" : "s"} could not be read \u2014 one of them may be this library, in which case it will be reported as missing instead. Fix:
3682
+ ` + scan.problems.join("\n ")
3683
+ );
3684
+ }
3685
+ return null;
3686
+ }
3687
+ const typesRel = lib.pkg.types;
3688
+ if (typeof typesRel !== "string" || typesRel === "") {
3689
+ throw new Error(
3690
+ `library ${name} (workspace sibling at ${lib.relDir}) has no "types" in its package.json`
3691
+ );
3692
+ }
3693
+ const typesPath = resolve5(lib.dir, typesRel);
3694
+ if (!existsSync5(typesPath)) {
3695
+ throw new Error(
3696
+ `library ${name} (workspace sibling at ${lib.relDir}) types file not found: ${typesRel}`
3697
+ );
3698
+ }
3699
+ return { dir: lib.dir, typesPath, version: lib.version };
3700
+ }
3701
+ function resolveLibraries(pluginDir, declared) {
3702
+ const paths = {};
3703
+ const manifests = {};
3704
+ const missing = [];
3705
+ const siblingDirs = {};
3706
+ for (const name of Object.keys(declared)) {
3707
+ const dir = localLibraryDir(pluginDir, name);
3708
+ if (dir !== null) {
3709
+ paths[name] = [join6(dir, "index.d.ts")];
3710
+ let version = "0.0.0";
3711
+ let apiVersion = "1.x";
3712
+ try {
3713
+ const m = JSON.parse(readFileSync5(join6(dir, "package.json"), "utf8"));
3714
+ if (typeof m.version === "string") version = m.version;
3715
+ if (typeof m.s2script?.apiVersion === "string") apiVersion = m.s2script.apiVersion;
3716
+ } catch {
3717
+ }
3718
+ manifests[name] = { version, apiVersion, source: "vendored" };
3719
+ continue;
3720
+ }
3721
+ const sib = resolveLibrarySibling(pluginDir, name);
3722
+ if (sib !== null) {
3723
+ paths[name] = [sib.typesPath];
3724
+ manifests[name] = { version: sib.version, apiVersion: "", source: "workspace" };
3725
+ siblingDirs[name] = sib.dir;
3726
+ continue;
3727
+ }
3728
+ missing.push(name);
3729
+ }
3730
+ return { paths, missing, manifests, siblingDirs };
3731
+ }
3732
+ function major(apiVersion) {
3733
+ const n3 = Number.parseInt(apiVersion.split(".")[0] ?? "1", 10);
3734
+ return Number.isNaN(n3) ? 1 : n3;
3735
+ }
3736
+ function assertLibrariesResolved(pluginDir, declared, sdkApiVersion) {
3737
+ const reserved = Object.keys(declared).filter((n3) => n3 === "@s2script" || n3.startsWith("@s2script/"));
3738
+ if (reserved.length > 0) {
3739
+ throw new Error(
3740
+ `s2script.libraries declares ${reserved.map((n3) => JSON.stringify(n3)).join(", ")} \u2014 the @s2script/* scope is reserved and always resolved as a runtime builtin, so it can never be a bundled library`
3741
+ );
3742
+ }
3743
+ const resolved = resolveLibraries(pluginDir, declared);
3744
+ if (resolved.missing.length) {
3745
+ throw new Error(
3746
+ `declared in s2script.libraries but not vendored under .s2script/libs and no workspace sibling found:
3747
+ ` + resolved.missing.map((n3) => ` ${n3} \u2014 run \`s2s add ${n3}\``).join("\n")
3748
+ );
3749
+ }
3750
+ const sdkMajor = major(sdkApiVersion);
3751
+ for (const [name, m] of Object.entries(resolved.manifests)) {
3752
+ if (m.source === "workspace") continue;
3753
+ if (major(m.apiVersion) > sdkMajor) {
3754
+ throw new Error(
3755
+ `library ${name}@${m.version} was built against apiVersion ${m.apiVersion}, newer than this SDK's ${sdkApiVersion} \u2014 upgrade @s2script/sdk`
3756
+ );
3757
+ }
3758
+ }
3759
+ return resolved;
3760
+ }
3761
+
3633
3762
  // src/typecheck/typecheck.ts
3634
3763
  function localDeclarationFiles(pluginDir) {
3635
- const srcDir = join6(pluginDir, "src");
3636
- if (!existsSync5(srcDir)) return [];
3637
- return readdirSync2(srcDir).filter((f) => f.endsWith(".d.ts")).map((f) => join6(srcDir, f));
3764
+ const srcDir = join7(pluginDir, "src");
3765
+ if (!existsSync6(srcDir)) return [];
3766
+ return readdirSync2(srcDir).filter((f) => f.endsWith(".d.ts")).map((f) => join7(srcDir, f));
3638
3767
  }
3639
3768
  function generatedDeclarationFiles(pluginDir) {
3640
- const gd = join6(pluginDir, ".s2script", "gamedata.d.ts");
3641
- return existsSync5(gd) ? [gd] : [];
3769
+ const gd = join7(pluginDir, ".s2script", "gamedata.d.ts");
3770
+ return existsSync6(gd) ? [gd] : [];
3642
3771
  }
3643
3772
  function declaredModules(dtsFiles) {
3644
3773
  const out = /* @__PURE__ */ new Set();
3645
3774
  for (const f of dtsFiles) {
3646
- const body = readFileSync5(f, "utf8");
3775
+ const body = readFileSync6(f, "utf8");
3647
3776
  for (const m of body.matchAll(/declare\s+module\s+["']([^"']+)["']/g)) out.add(m[1]);
3648
3777
  }
3649
3778
  return out;
3650
3779
  }
3651
3780
  function typecheckPlugin(pluginDir, opts) {
3652
- const absDir = resolve5(pluginDir);
3653
- const packagesDir = opts?.packagesDir ? resolve5(opts.packagesDir) : resolvePackagesDir({ pluginDir: absDir });
3654
- const pkg = JSON.parse(readFileSync5(join6(absDir, "package.json"), "utf8"));
3781
+ const absDir = resolve6(pluginDir);
3782
+ const packagesDir = opts?.packagesDir ? resolve6(opts.packagesDir) : resolvePackagesDir({ pluginDir: absDir });
3783
+ const pkg = JSON.parse(readFileSync6(join7(absDir, "package.json"), "utf8"));
3655
3784
  const s2 = pkg.s2script ?? {};
3656
3785
  const entryRel = s2.main ?? pkg.main;
3657
- if (!entryRel) throw new Error(`typecheckPlugin: no entry point in ${join6(absDir, "package.json")}`);
3658
- const entry = resolve5(absDir, entryRel);
3786
+ if (!entryRel) throw new Error(`typecheckPlugin: no entry point in ${join7(absDir, "package.json")}`);
3787
+ const entry = resolve6(absDir, entryRel);
3659
3788
  const isAlwaysResolved = (d) => d.startsWith("@s2script/sdk/") || d === "@s2script/cs2" || d.startsWith("@s2script/cs2/");
3660
3789
  const localDts = localDeclarationFiles(absDir);
3661
3790
  const locallyDeclared = declaredModules(localDts);
@@ -3674,13 +3803,15 @@ function typecheckPlugin(pluginDir, opts) {
3674
3803
  const p = localContractPath(absDir, d);
3675
3804
  if (p !== null) contractPaths[d] = [p];
3676
3805
  }
3806
+ const libraryPaths = resolveLibraries(absDir, s2.libraries ?? {}).paths;
3807
+ const libraryNames = new Set(Object.keys(s2.libraries ?? {}));
3677
3808
  const deps = [
3678
3809
  ...Object.keys(s2.pluginDependencies ?? {}),
3679
3810
  ...Object.keys(s2.optionalPluginDependencies ?? {})
3680
3811
  // Never stub a module the plugin declares itself — a shorthand `declare module "X";` and a
3681
3812
  // full `declare module "X" { … }` for the same X collide.
3682
3813
  ].filter(
3683
- (d) => !isAlwaysResolved(d) && !locallyDeclared.has(d) && contractPaths[d] === void 0 && !siblings.has(d)
3814
+ (d) => !isAlwaysResolved(d) && !locallyDeclared.has(d) && contractPaths[d] === void 0 && !siblings.has(d) && !libraryNames.has(d)
3684
3815
  );
3685
3816
  const options = {
3686
3817
  // Accept explicit `.ts` import extensions (node type-stripping requires them for source-to-source
@@ -3698,19 +3829,20 @@ function typecheckPlugin(pluginDir, opts) {
3698
3829
  // subpath. tsc picks the longest matching prefix, so this wins for cs2 subpath imports.
3699
3830
  "@s2script/cs2/*": ["cs2/*.d.ts"],
3700
3831
  "@s2script/*": ["*/index.d.ts"],
3701
- ...contractPaths
3832
+ ...contractPaths,
3833
+ ...libraryPaths
3702
3834
  }
3703
3835
  };
3704
3836
  const rootNames = [
3705
3837
  entry,
3706
- join6(packagesDir, "sdk", "globals.d.ts"),
3838
+ join7(packagesDir, "sdk", "globals.d.ts"),
3707
3839
  ...localDts,
3708
3840
  ...generatedDeclarationFiles(absDir)
3709
3841
  ];
3710
- const tmp = mkdtempSync(join6(tmpdir(), "s2tc-"));
3842
+ const tmp = mkdtempSync(join7(tmpdir(), "s2tc-"));
3711
3843
  try {
3712
3844
  if (deps.length) {
3713
- const stub = join6(tmp, "ambient.d.ts");
3845
+ const stub = join7(tmp, "ambient.d.ts");
3714
3846
  writeFileSync(stub, deps.map((d) => `declare module ${JSON.stringify(d)};`).join("\n") + "\n");
3715
3847
  rootNames.push(stub);
3716
3848
  }
@@ -3840,61 +3972,70 @@ function validateDecl(full, decl, typeStr, errs) {
3840
3972
  var HOST_API_VERSION_MAJOR = 2;
3841
3973
  var STAMPED_API_VERSION = `${HOST_API_VERSION_MAJOR}.x`;
3842
3974
 
3843
- // src/publish-scan.ts
3844
- import ts2 from "typescript";
3845
- function isPluginContext(type) {
3846
- const sym = type.getSymbol() ?? type.aliasSymbol;
3847
- return sym?.getName() === "PluginContext";
3975
+ // src/npm-deps-gate.ts
3976
+ import { lstatSync, realpathSync } from "node:fs";
3977
+ import { dirname as dirname3, join as join8, resolve as resolve7, sep } from "node:path";
3978
+ function findInNodeModules(startDir, dep) {
3979
+ const segs = dep.split("/");
3980
+ let dir = resolve7(startDir);
3981
+ for (; ; ) {
3982
+ const candidate = join8(dir, "node_modules", ...segs);
3983
+ try {
3984
+ lstatSync(candidate);
3985
+ return candidate;
3986
+ } catch {
3987
+ }
3988
+ const parent = dirname3(dir);
3989
+ if (parent === dir) return null;
3990
+ dir = parent;
3991
+ }
3848
3992
  }
3849
- function scanPluginProgram(program, pluginDir) {
3850
- const checker = program.getTypeChecker();
3851
- const out = { publishNames: [], dynamicPublishSites: [], useNames: [] };
3852
- const dirPrefix = pluginDir.replace(/\\/g, "/").replace(/\/+$/, "") + "/";
3853
- for (const sf of program.getSourceFiles()) {
3854
- if (sf.isDeclarationFile) continue;
3855
- if (!sf.fileName.replace(/\\/g, "/").startsWith(dirPrefix)) continue;
3856
- const visit = (node) => {
3857
- if (ts2.isCallExpression(node) && ts2.isPropertyAccessExpression(node.expression)) {
3858
- const method = node.expression.name.text;
3859
- if (method === "publish" || method === "use" || method === "tryUse") {
3860
- const recv = checker.getTypeAtLocation(node.expression.expression);
3861
- if (isPluginContext(recv)) {
3862
- const arg0 = node.arguments[0];
3863
- if (method === "publish") {
3864
- if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
3865
- out.publishNames.push(arg0.text);
3866
- } else {
3867
- const { line } = sf.getLineAndCharacterOfPosition(node.getStart());
3868
- out.dynamicPublishSites.push(`${sf.fileName}:${line + 1}`);
3869
- }
3870
- } else if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
3871
- out.useNames.push(arg0.text);
3872
- }
3873
- }
3874
- }
3875
- }
3876
- ts2.forEachChild(node, visit);
3877
- };
3878
- visit(sf);
3993
+ function isWorkspaceLinked(pluginDir, dep) {
3994
+ const found = findInNodeModules(pluginDir, dep);
3995
+ if (found === null) return false;
3996
+ let real;
3997
+ try {
3998
+ real = realpathSync(found);
3999
+ } catch {
4000
+ return false;
3879
4001
  }
3880
- out.publishNames = [...new Set(out.publishNames)];
3881
- out.useNames = [...new Set(out.useNames)];
3882
- return out;
4002
+ return !real.split(sep).includes("node_modules");
3883
4003
  }
4004
+ function assertNoNpmRuntimeDeps(pkg, pluginDir) {
4005
+ const offenders = [];
4006
+ for (const [dep, range] of Object.entries(pkg.dependencies ?? {})) {
4007
+ if (dep.startsWith("@s2script/")) continue;
4008
+ if (typeof range === "string" && (range.startsWith("file:") || range.startsWith("workspace:"))) continue;
4009
+ if (isWorkspaceLinked(pluginDir, dep)) continue;
4010
+ offenders.push(dep);
4011
+ }
4012
+ if (!offenders.length) return;
4013
+ throw new Error(
4014
+ `runtime dependenc${offenders.length === 1 ? "y" : "ies"} ` + offenders.map((d) => JSON.stringify(d)).join(", ") + ` ${offenders.length === 1 ? "is" : "are"} not s2script librar${offenders.length === 1 ? "y" : "ies"}.
4015
+ Plugins run in bare V8 \u2014 no Node APIs, no npm runtime. A package that touches
4016
+ fs/net/process builds green and fails on a live server.
4017
+ Use s2script.libraries (\`s2s add <pkg>\`), or vendor it as a workspace package.`
4018
+ );
4019
+ }
4020
+
4021
+ // src/build-library.ts
4022
+ import * as esbuild from "esbuild";
4023
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync8, statSync as statSync3 } from "node:fs";
4024
+ import { resolve as resolve9, join as join10 } from "node:path";
3884
4025
 
3885
4026
  // src/lint/lint.ts
3886
4027
  import { ESLint } from "eslint";
3887
- import { existsSync as existsSync6, readdirSync as readdirSync3 } from "node:fs";
3888
- import { dirname as dirname3, join as join7, resolve as resolve6 } from "node:path";
4028
+ import { existsSync as existsSync7, readdirSync as readdirSync3 } from "node:fs";
4029
+ import { dirname as dirname4, join as join9, resolve as resolve8 } from "node:path";
3889
4030
  import s2lint from "@s2script/eslint-plugin";
3890
4031
  var CONFIG_FILES = ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts"];
3891
4032
  function ownConfigDir(pluginDir) {
3892
4033
  const stopAt = findWorkspaceRoot(pluginDir);
3893
4034
  let dir = pluginDir;
3894
4035
  for (; ; ) {
3895
- if (CONFIG_FILES.some((f) => existsSync6(join7(dir, f)))) return dir;
4036
+ if (CONFIG_FILES.some((f) => existsSync7(join9(dir, f)))) return dir;
3896
4037
  if (stopAt === null || dir === stopAt) return null;
3897
- const parent = dirname3(dir);
4038
+ const parent = dirname4(dir);
3898
4039
  if (parent === dir) return null;
3899
4040
  dir = parent;
3900
4041
  }
@@ -3909,14 +4050,14 @@ function collectTsFiles(dir) {
3909
4050
  const out = [];
3910
4051
  for (const e of entries) {
3911
4052
  if (e.name === "node_modules" || e.name.startsWith(".")) continue;
3912
- const full = join7(dir, e.name);
4053
+ const full = join9(dir, e.name);
3913
4054
  if (e.isDirectory()) out.push(...collectTsFiles(full));
3914
- else if (e.isFile() && e.name.endsWith(".ts")) out.push(full);
4055
+ else if (e.isFile() && e.name.endsWith(".ts") && !e.name.endsWith(".d.ts")) out.push(full);
3915
4056
  }
3916
4057
  return out;
3917
4058
  }
3918
4059
  async function lintPlugin(pluginDir, program) {
3919
- const absDir = resolve6(pluginDir);
4060
+ const absDir = resolve8(pluginDir);
3920
4061
  const configDir = ownConfigDir(absDir);
3921
4062
  const hasOwnConfig = configDir !== null;
3922
4063
  const eslint = hasOwnConfig ? new ESLint({ cwd: configDir, errorOnUnmatchedPattern: false, warnIgnored: false }) : new ESLint({
@@ -3935,6 +4076,126 @@ async function lintPlugin(pluginDir, program) {
3935
4076
  return { ok: errorCount === 0, output, errorCount };
3936
4077
  }
3937
4078
 
4079
+ // src/build-library.ts
4080
+ async function buildLibrary(dir, packagesDir) {
4081
+ const absDir = resolve9(dir);
4082
+ const pkgPath = join10(absDir, "package.json");
4083
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
4084
+ const s2 = pkg.s2script ?? {};
4085
+ if (s2.pluginDependencies || s2.optionalPluginDependencies) {
4086
+ throw new Error(
4087
+ "a library may not declare pluginDependencies/optionalPluginDependencies \u2014 an inter-plugin proxy belongs to a loaded plugin's ledger, not to build-time code"
4088
+ );
4089
+ }
4090
+ if (s2.publishes) {
4091
+ throw new Error("a library may not declare publishes \u2014 publishes is a plugin's runtime contract");
4092
+ }
4093
+ const typesRel = pkg.types ?? pkg.typings;
4094
+ if (typeof typesRel !== "string" || !typesRel.endsWith(".d.ts")) {
4095
+ throw new Error('a library must set "types" to a .d.ts \u2014 consumers vendor it to typecheck against');
4096
+ }
4097
+ const typesPath = resolve9(absDir, typesRel);
4098
+ if (!existsSync8(typesPath) || !statSync3(typesPath).isFile() || statSync3(typesPath).size === 0) {
4099
+ throw new Error(`types file is missing or empty: ${typesRel}`);
4100
+ }
4101
+ assertNoNpmRuntimeDeps(pkg, absDir);
4102
+ const librariesResolved = assertLibrariesResolved(absDir, s2.libraries ?? {}, STAMPED_API_VERSION);
4103
+ const tc = typecheckPlugin(absDir, packagesDir !== void 0 ? { packagesDir } : void 0);
4104
+ if (!tc.ok) {
4105
+ throw new Error(`typecheck failed (${tc.diagnostics.length} error(s)):
4106
+ ${formatDiagnostics(tc.diagnostics)}`);
4107
+ }
4108
+ const lint = await lintPlugin(absDir, tc.program);
4109
+ if (!lint.ok) {
4110
+ throw new Error(`lint failed (${lint.errorCount} error(s)):
4111
+ ${lint.output}`);
4112
+ }
4113
+ if (lint.output.trim().length > 0) console.warn(lint.output);
4114
+ const entryRelative = s2.main ?? pkg.main;
4115
+ if (!entryRelative) throw new Error(`buildLibrary: no entry point in ${pkgPath} (set s2script.main or main)`);
4116
+ const result = await esbuild.build({
4117
+ entryPoints: [join10(absDir, entryRelative)],
4118
+ bundle: true,
4119
+ platform: "neutral",
4120
+ format: "cjs",
4121
+ // Builtins resolve in the CONSUMER's context at runtime, so they stay external
4122
+ // through both bundles. Everything else — including this library's own libraries
4123
+ // — is inlined, which is what makes a .s2lib self-contained.
4124
+ external: ["@s2script/*"],
4125
+ target: "es2020",
4126
+ mainFields: ["module", "main"],
4127
+ // A vendored library's directory is already named for its package, so `nodePaths` alone
4128
+ // resolves it. A WORKSPACE-sibling library (librariesResolved.siblingDirs) is not, hence the
4129
+ // alias — same reasoning `build.ts` applies to a plugin's own library imports.
4130
+ nodePaths: [librariesRoot(absDir)],
4131
+ alias: librariesResolved.siblingDirs,
4132
+ write: false
4133
+ });
4134
+ const manifest = {
4135
+ kind: "library",
4136
+ id: pkg.name,
4137
+ version: pkg.version,
4138
+ apiVersion: STAMPED_API_VERSION,
4139
+ main: "index.js",
4140
+ types: "index.d.ts"
4141
+ };
4142
+ const sanitized = String(pkg.name).replace(/[^a-zA-Z0-9._-]/g, "_");
4143
+ const outDir = join10(absDir, "dist");
4144
+ mkdirSync(outDir, { recursive: true });
4145
+ const outPath = join10(outDir, `${sanitized}.s2lib`);
4146
+ writeFileSync2(
4147
+ outPath,
4148
+ zipSync({
4149
+ "manifest.json": Buffer.from(JSON.stringify(manifest, null, 2)),
4150
+ "index.js": Buffer.from(result.outputFiles[0].text),
4151
+ "index.d.ts": readFileSync7(typesPath)
4152
+ })
4153
+ );
4154
+ return outPath;
4155
+ }
4156
+
4157
+ // src/publish-scan.ts
4158
+ import ts2 from "typescript";
4159
+ function isPluginContext(type) {
4160
+ const sym = type.getSymbol() ?? type.aliasSymbol;
4161
+ return sym?.getName() === "PluginContext";
4162
+ }
4163
+ function scanPluginProgram(program, pluginDir) {
4164
+ const checker = program.getTypeChecker();
4165
+ const out = { publishNames: [], dynamicPublishSites: [], useNames: [] };
4166
+ const dirPrefix = pluginDir.replace(/\\/g, "/").replace(/\/+$/, "") + "/";
4167
+ for (const sf of program.getSourceFiles()) {
4168
+ if (sf.isDeclarationFile) continue;
4169
+ if (!sf.fileName.replace(/\\/g, "/").startsWith(dirPrefix)) continue;
4170
+ const visit = (node) => {
4171
+ if (ts2.isCallExpression(node) && ts2.isPropertyAccessExpression(node.expression)) {
4172
+ const method = node.expression.name.text;
4173
+ if (method === "publish" || method === "use" || method === "tryUse") {
4174
+ const recv = checker.getTypeAtLocation(node.expression.expression);
4175
+ if (isPluginContext(recv)) {
4176
+ const arg0 = node.arguments[0];
4177
+ if (method === "publish") {
4178
+ if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
4179
+ out.publishNames.push(arg0.text);
4180
+ } else {
4181
+ const { line } = sf.getLineAndCharacterOfPosition(node.getStart());
4182
+ out.dynamicPublishSites.push(`${sf.fileName}:${line + 1}`);
4183
+ }
4184
+ } else if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
4185
+ out.useNames.push(arg0.text);
4186
+ }
4187
+ }
4188
+ }
4189
+ }
4190
+ ts2.forEachChild(node, visit);
4191
+ };
4192
+ visit(sf);
4193
+ }
4194
+ out.publishNames = [...new Set(out.publishNames)];
4195
+ out.useNames = [...new Set(out.useNames)];
4196
+ return out;
4197
+ }
4198
+
3938
4199
  // src/gamedata/types.ts
3939
4200
  var PLATFORM = "linuxsteamrt64";
3940
4201
  var ARG_KINDS = ["bool", "int", "float", "string", "vector", "entity"];
@@ -4189,9 +4450,12 @@ function stripJsonComments(src) {
4189
4450
 
4190
4451
  // src/build.ts
4191
4452
  async function buildPlugin(dir, packagesDir) {
4192
- const absDir = resolve7(dir);
4193
- const pkgPath = join8(absDir, "package.json");
4194
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
4453
+ const absDir = resolve10(dir);
4454
+ const pkgPath = join11(absDir, "package.json");
4455
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
4456
+ if (packageKind(pkg) === "library") {
4457
+ throw new Error(`${pkgPath} is s2script.kind="library" \u2014 build it with buildLibrary/buildPackage`);
4458
+ }
4195
4459
  const s2 = pkg.s2script ?? {};
4196
4460
  const config = s2.config ?? void 0;
4197
4461
  if (config !== void 0) {
@@ -4199,11 +4463,25 @@ async function buildPlugin(dir, packagesDir) {
4199
4463
  if (cfgErrs.length) throw new Error(`invalid s2script.config:
4200
4464
  ${cfgErrs.join("\n ")}`);
4201
4465
  }
4466
+ assertNoNpmRuntimeDeps(pkg, absDir);
4467
+ const libraries = s2.libraries ?? {};
4468
+ const pluginDepNames = /* @__PURE__ */ new Set([
4469
+ ...Object.keys(s2.pluginDependencies ?? {}),
4470
+ ...Object.keys(s2.optionalPluginDependencies ?? {})
4471
+ ]);
4472
+ for (const name2 of Object.keys(libraries)) {
4473
+ if (pluginDepNames.has(name2)) {
4474
+ throw new Error(
4475
+ `${JSON.stringify(name2)} is declared in BOTH s2script.libraries and s2script.pluginDependencies/optionalPluginDependencies \u2014 pick one: a library is build-time code bundled into this .s2sp, an inter-plugin dependency is a runtime proxy resolved by the host at load; the two cannot share a name`
4476
+ );
4477
+ }
4478
+ }
4479
+ const librariesResolved = assertLibrariesResolved(absDir, libraries, STAMPED_API_VERSION);
4202
4480
  const permissions = Array.isArray(s2.permissions) ? s2.permissions : [];
4203
4481
  let gamedata;
4204
4482
  if (typeof s2.gamedata === "string") {
4205
- const gdPath = resolve7(absDir, s2.gamedata);
4206
- const dirPrefix = absDir.endsWith(sep) ? absDir : absDir + sep;
4483
+ const gdPath = resolve10(absDir, s2.gamedata);
4484
+ const dirPrefix = absDir.endsWith(sep2) ? absDir : absDir + sep2;
4207
4485
  if (gdPath !== absDir && !gdPath.startsWith(dirPrefix)) {
4208
4486
  throw new Error(
4209
4487
  `s2script.gamedata escapes the plugin directory: ${JSON.stringify(s2.gamedata)} resolves to ${gdPath}`
@@ -4217,7 +4495,7 @@ async function buildPlugin(dir, packagesDir) {
4217
4495
  `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}'`
4218
4496
  );
4219
4497
  }
4220
- const raw = stripJsonComments(readFileSync6(gdPath, "utf8"));
4498
+ const raw = stripJsonComments(readFileSync8(gdPath, "utf8"));
4221
4499
  try {
4222
4500
  gamedata = JSON.parse(raw);
4223
4501
  } catch (e) {
@@ -4226,11 +4504,11 @@ async function buildPlugin(dir, packagesDir) {
4226
4504
  const gdErrs = validatePluginGamedata(gamedata, { permissions });
4227
4505
  if (gdErrs.length) throw new Error(`invalid gamedata:
4228
4506
  ${gdErrs.join("\n ")}`);
4229
- const genDir = join8(absDir, ".s2script");
4230
- mkdirSync(genDir, { recursive: true });
4231
- writeFileSync2(join8(genDir, "gamedata.d.ts"), generateGamedataTypes(gamedata));
4507
+ const genDir = join11(absDir, ".s2script");
4508
+ mkdirSync2(genDir, { recursive: true });
4509
+ writeFileSync3(join11(genDir, "gamedata.d.ts"), generateGamedataTypes(gamedata));
4232
4510
  } else {
4233
- rmSync2(join8(absDir, ".s2script", "gamedata.d.ts"), { force: true });
4511
+ rmSync2(join11(absDir, ".s2script", "gamedata.d.ts"), { force: true });
4234
4512
  }
4235
4513
  const declaredDepNames = [
4236
4514
  ...Object.keys(s2.pluginDependencies ?? {}),
@@ -4320,8 +4598,8 @@ ${lint.output}`);
4320
4598
  `buildPlugin: no entry point found in ${pkgPath} (set s2script.main or main)`
4321
4599
  );
4322
4600
  }
4323
- const entryPoint = join8(absDir, entryRelative);
4324
- const result = await esbuild.build({
4601
+ const entryPoint = join11(absDir, entryRelative);
4602
+ const result = await esbuild2.build({
4325
4603
  entryPoints: [entryPoint],
4326
4604
  bundle: true,
4327
4605
  platform: "neutral",
@@ -4332,7 +4610,15 @@ ${lint.output}`);
4332
4610
  // declares `main` (rather than `exports`) fails to resolve with an undiagnosable error.
4333
4611
  // Set it explicitly so a monorepo plugin bundles whichever field the author wrote.
4334
4612
  mainFields: ["module", "main"],
4335
- write: false
4613
+ write: false,
4614
+ // NODE_PATH semantics: `@edge/base64` resolves to .s2script/libs/@edge/base64 with no
4615
+ // per-package alias and no change to `external`. Libraries are BUNDLED IN by design.
4616
+ nodePaths: [librariesRoot(absDir)],
4617
+ // A WORKSPACE-sibling library (librariesResolved.siblingDirs) is not vendored under a
4618
+ // directory named for its package, so nodePaths alone cannot find it — alias each declared
4619
+ // name straight at the sibling's own directory instead, same "bundle the real source, no
4620
+ // copy" reasoning `resolveSiblingContracts` applies to contracts. Empty outside a workspace.
4621
+ alias: librariesResolved.siblingDirs
4336
4622
  });
4337
4623
  const pluginJs = result.outputFiles[0].text;
4338
4624
  const manifest = {
@@ -4368,19 +4654,23 @@ ${lint.output}`);
4368
4654
  };
4369
4655
  if (gamedata) zipFiles["gamedata.json"] = Buffer.from(JSON.stringify(gamedata, null, 2));
4370
4656
  if (gate.typesPath !== null && Object.keys(derivedPublishes).length > 0) {
4371
- const contract = readFileSync6(gate.typesPath);
4657
+ const contract = readFileSync8(gate.typesPath);
4372
4658
  for (const iface of Object.keys(derivedPublishes)) {
4373
4659
  const safe = iface.replace(/[^a-zA-Z0-9._-]/g, "_");
4374
4660
  zipFiles[`types/${safe}.d.ts`] = contract;
4375
4661
  }
4376
4662
  }
4377
4663
  const sanitizedId = name.replace(/[^a-zA-Z0-9._-]/g, "_");
4378
- const outDir = join8(absDir, "dist");
4379
- mkdirSync(outDir, { recursive: true });
4380
- const outPath = join8(outDir, `${sanitizedId}.s2sp`);
4381
- writeFileSync2(outPath, zipSync(zipFiles));
4664
+ const outDir = join11(absDir, "dist");
4665
+ mkdirSync2(outDir, { recursive: true });
4666
+ const outPath = join11(outDir, `${sanitizedId}.s2sp`);
4667
+ writeFileSync3(outPath, zipSync(zipFiles));
4382
4668
  return outPath;
4383
4669
  }
4670
+ async function buildPackage(dir, packagesDir) {
4671
+ const pkg = JSON.parse(readFileSync8(join11(resolve10(dir), "package.json"), "utf8"));
4672
+ return packageKind(pkg) === "library" ? buildLibrary(dir, packagesDir) : buildPlugin(dir, packagesDir);
4673
+ }
4384
4674
 
4385
4675
  // src/cli/args.ts
4386
4676
  function parseFlag(args, name) {
@@ -4409,8 +4699,8 @@ function positionals(args, flagsWithValue) {
4409
4699
  }
4410
4700
 
4411
4701
  // src/workspace/build-all.ts
4412
- import { writeFileSync as writeFileSync3 } from "node:fs";
4413
- import { join as join9 } from "node:path";
4702
+ import { writeFileSync as writeFileSync4 } from "node:fs";
4703
+ import { join as join12 } from "node:path";
4414
4704
 
4415
4705
  // src/workspace/graph.ts
4416
4706
  function topoOrder(batch, warn = console.warn) {
@@ -4532,6 +4822,16 @@ function preflightWorkspace(ws) {
4532
4822
  }
4533
4823
 
4534
4824
  // src/workspace/build-all.ts
4825
+ function declaredLibraries(ws) {
4826
+ return ws.libs.filter((m) => packageKind(m.pkg) === "library");
4827
+ }
4828
+ function matchHits(pattern, members) {
4829
+ let hits = members.filter((m) => matchGlob(pattern, m.name));
4830
+ if (hits.length === 0 && pattern.includes("/")) {
4831
+ hits = members.filter((m) => matchGlob(pattern, m.relDir));
4832
+ }
4833
+ return hits;
4834
+ }
4535
4835
  function collectFilters(argv2) {
4536
4836
  const out = [];
4537
4837
  for (let i2 = 0; i2 < argv2.length; i2++) {
@@ -4554,14 +4854,12 @@ function collectFilters(argv2) {
4554
4854
  }
4555
4855
  function selectPlugins(ws, filters) {
4556
4856
  if (filters.length === 0) return ws.plugins;
4857
+ const libs = declaredLibraries(ws);
4557
4858
  const chosen = /* @__PURE__ */ new Set();
4558
4859
  const unmatched = [];
4559
4860
  for (const pattern of filters) {
4560
- let hits = ws.plugins.filter((p) => matchGlob(pattern, p.name));
4561
- if (hits.length === 0 && pattern.includes("/")) {
4562
- hits = ws.plugins.filter((p) => matchGlob(pattern, p.relDir));
4563
- }
4564
- if (hits.length === 0) {
4861
+ const hits = matchHits(pattern, ws.plugins);
4862
+ if (hits.length === 0 && matchHits(pattern, libs).length === 0) {
4565
4863
  unmatched.push(pattern);
4566
4864
  continue;
4567
4865
  }
@@ -4577,6 +4875,15 @@ function selectPlugins(ws, filters) {
4577
4875
  }
4578
4876
  return ws.plugins.filter((p) => chosen.has(p.name));
4579
4877
  }
4878
+ function selectLibraries(ws, filters) {
4879
+ const libs = declaredLibraries(ws);
4880
+ if (filters.length === 0) return libs;
4881
+ const chosen = /* @__PURE__ */ new Set();
4882
+ for (const pattern of filters) {
4883
+ for (const m of matchHits(pattern, libs)) chosen.add(m.name);
4884
+ }
4885
+ return libs.filter((m) => chosen.has(m.name));
4886
+ }
4580
4887
  function restampRange(range, version) {
4581
4888
  const m = /^\s*(\^|~|>=|=)?\s*(\d+\.\d+\.\d+[0-9A-Za-z.+-]*)\s*$/.exec(range);
4582
4889
  const rewritten = m ? `${m[1] ?? ""}${version}` : `^${version}`;
@@ -4614,7 +4921,7 @@ function stampWorkspaceVersion(ws, version) {
4614
4921
  dirty = true;
4615
4922
  }
4616
4923
  }
4617
- if (dirty) writeFileSync3(join9(p.dir, "package.json"), `${JSON.stringify(next, null, 2)}
4924
+ if (dirty) writeFileSync4(join12(p.dir, "package.json"), `${JSON.stringify(next, null, 2)}
4618
4925
  `);
4619
4926
  }
4620
4927
  return result;
@@ -4643,16 +4950,30 @@ async function buildWorkspace(opts) {
4643
4950
  );
4644
4951
  }
4645
4952
  const selected = selectPlugins(ws, opts.filters ?? []);
4953
+ const selectedLibraries = selectLibraries(ws, opts.filters ?? []);
4646
4954
  preflightWorkspace(ws);
4647
4955
  const ordered = orderPlugins(selected, warn);
4648
4956
  opts.onPlan?.(ordered, stamp);
4649
4957
  const outcomes = [];
4958
+ for (const lib of selectedLibraries) {
4959
+ const base = { name: lib.name, version: lib.version, relDir: lib.relDir };
4960
+ try {
4961
+ const outPath = await step(
4962
+ `Building ${lib.name}`,
4963
+ () => buildPackage(lib.dir, opts.packagesDir),
4964
+ (out) => `Built ${out}`
4965
+ );
4966
+ outcomes.push({ ...base, outPath });
4967
+ } catch (e) {
4968
+ outcomes.push({ ...base, error: e instanceof Error ? e.message : String(e) });
4969
+ }
4970
+ }
4650
4971
  for (const p of ordered) {
4651
4972
  const base = { name: p.name, version: p.version, relDir: p.relDir };
4652
4973
  try {
4653
4974
  const outPath = await step(
4654
4975
  `Building ${p.name}`,
4655
- () => buildPlugin(p.dir, opts.packagesDir),
4976
+ () => buildPackage(p.dir, opts.packagesDir),
4656
4977
  (out) => `Built ${out}`
4657
4978
  );
4658
4979
  outcomes.push({ ...base, outPath });
@@ -5995,12 +6316,12 @@ async function run(argv2) {
5995
6316
  const filters = collectFilters(argv2);
5996
6317
  const stampVersion = parseFlag(argv2, "--stamp-version");
5997
6318
  let dir = positionals(argv2, VALUE_FLAGS)[0];
5998
- const wsRoot = findWorkspaceRoot(dir === void 0 ? process.cwd() : resolve8(dir));
5999
- const workspaceMode = wsRoot !== null && (dir === void 0 || resolve8(dir) === wsRoot);
6319
+ const wsRoot = findWorkspaceRoot(dir === void 0 ? process.cwd() : resolve11(dir));
6320
+ const workspaceMode = wsRoot !== null && (dir === void 0 || resolve11(dir) === wsRoot);
6000
6321
  if (!workspaceMode && (filters.length > 0 || stampVersion !== void 0)) {
6001
6322
  const flag = filters.length > 0 ? "--filter" : "--stamp-version";
6002
6323
  throw new Error(
6003
- wsRoot === null ? `${flag} is a workspace flag, but no workspace root was found above ${dir === void 0 ? process.cwd() : resolve8(dir)} \u2014 a workspace root is a package.json carrying an "s2script": { "workspace": { "plugins": [...] } } block` : `${flag} narrows a WORKSPACE build, but ${dir} names a single plugin \u2014 drop the argument (or pass ${wsRoot}) to build the workspace`
6324
+ wsRoot === null ? `${flag} is a workspace flag, but no workspace root was found above ${dir === void 0 ? process.cwd() : resolve11(dir)} \u2014 a workspace root is a package.json carrying an "s2script": { "workspace": { "plugins": [...] } } block` : `${flag} narrows a WORKSPACE build, but ${dir} names a single plugin \u2014 drop the argument (or pass ${wsRoot}) to build the workspace`
6004
6325
  );
6005
6326
  }
6006
6327
  if (wsRoot !== null && workspaceMode) {
@@ -6018,10 +6339,10 @@ async function run(argv2) {
6018
6339
  }
6019
6340
  const packagesDir = resolvePackagesDir({
6020
6341
  explicit: parseFlag(argv2, "--packages-dir"),
6021
- pluginDir: resolve8(dir),
6342
+ pluginDir: resolve11(dir),
6022
6343
  fromCliUrl: import.meta.url
6023
6344
  });
6024
- const out = await task(`Building ${dir}`, () => buildPlugin(dir, packagesDir), {
6345
+ const out = await task(`Building ${dir}`, () => buildPackage(dir, packagesDir), {
6025
6346
  interactive,
6026
6347
  done: (p) => `Built ${p}`
6027
6348
  });
@@ -6078,14 +6399,14 @@ async function runWorkspace(argv2, root, interactive, filters, stampVersion) {
6078
6399
  }
6079
6400
 
6080
6401
  // src/commands/deploy.ts
6081
- import { resolve as resolve10 } from "node:path";
6402
+ import { resolve as resolve13 } from "node:path";
6082
6403
 
6083
6404
  // src/registry/deploy.ts
6084
- import { readFileSync as readFileSync9 } from "node:fs";
6085
- import { resolve as resolve9 } from "node:path";
6405
+ import { readFileSync as readFileSync11 } from "node:fs";
6406
+ import { resolve as resolve12 } from "node:path";
6086
6407
 
6087
6408
  // src/types-pack.ts
6088
- import { readFileSync as readFileSync7 } from "node:fs";
6409
+ import { readFileSync as readFileSync9 } from "node:fs";
6089
6410
  import { gzipSync } from "node:zlib";
6090
6411
  function pad(buf, size) {
6091
6412
  if (buf.length > size) throw new Error(`tar field overflow (${buf.length}>${size})`);
@@ -6119,7 +6440,7 @@ function tarFile(name, content) {
6119
6440
  return Buffer.concat([header, content, Buffer.alloc(padLen, 0)]);
6120
6441
  }
6121
6442
  function packTypesTarball(input) {
6122
- const dts = readFileSync7(input.typesPath);
6443
+ const dts = readFileSync9(input.typesPath);
6123
6444
  const pkgJson = Buffer.from(
6124
6445
  JSON.stringify(
6125
6446
  {
@@ -6146,11 +6467,11 @@ function packTypesTarball(input) {
6146
6467
  }
6147
6468
 
6148
6469
  // src/registry/credentials.ts
6149
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "node:fs";
6470
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
6150
6471
  import { homedir } from "node:os";
6151
- import { join as join10 } from "node:path";
6472
+ import { join as join13 } from "node:path";
6152
6473
  function credentialsPath() {
6153
- return join10(homedir(), ".s2script", "credentials.json");
6474
+ return join13(homedir(), ".s2script", "credentials.json");
6154
6475
  }
6155
6476
  function normalizeRegistryUrl(url) {
6156
6477
  const trimmed = url.replace(/\/$/, "");
@@ -6168,9 +6489,9 @@ function loadCredentials() {
6168
6489
  return { registryUrl: defaultRegistryUrl(), token: fromEnv };
6169
6490
  }
6170
6491
  const p = credentialsPath();
6171
- if (!existsSync7(p)) return null;
6492
+ if (!existsSync9(p)) return null;
6172
6493
  try {
6173
- const j = JSON.parse(readFileSync8(p, "utf8"));
6494
+ const j = JSON.parse(readFileSync10(p, "utf8"));
6174
6495
  if (!j.token) return null;
6175
6496
  return {
6176
6497
  registryUrl: normalizeRegistryUrl(j.registryUrl || defaultRegistryUrl()),
@@ -6181,9 +6502,9 @@ function loadCredentials() {
6181
6502
  }
6182
6503
  }
6183
6504
  function saveCredentials(creds) {
6184
- const dir = join10(homedir(), ".s2script");
6185
- mkdirSync2(dir, { recursive: true });
6186
- writeFileSync4(
6505
+ const dir = join13(homedir(), ".s2script");
6506
+ mkdirSync3(dir, { recursive: true });
6507
+ writeFileSync5(
6187
6508
  credentialsPath(),
6188
6509
  JSON.stringify(
6189
6510
  { registryUrl: normalizeRegistryUrl(creds.registryUrl), token: creds.token },
@@ -6213,7 +6534,8 @@ var TIMEOUTS_MS = {
6213
6534
  meta: 3e4,
6214
6535
  plan: 3e4,
6215
6536
  "types download": 12e4,
6216
- "s2sp download": 12e4
6537
+ "s2sp download": 12e4,
6538
+ "library download": 12e4
6217
6539
  };
6218
6540
  function rootCause(err2) {
6219
6541
  let cur = err2;
@@ -6306,9 +6628,10 @@ var RegistryClient = class {
6306
6628
  }
6307
6629
  async deploy(opts) {
6308
6630
  const entries = {
6309
- "manifest.json": new TextEncoder().encode(JSON.stringify(opts.manifest)),
6310
- "plugin.s2sp": new Uint8Array(opts.s2sp)
6631
+ "manifest.json": new TextEncoder().encode(JSON.stringify(opts.manifest))
6311
6632
  };
6633
+ if (opts.s2sp && opts.s2sp.length) entries["plugin.s2sp"] = new Uint8Array(opts.s2sp);
6634
+ if (opts.lib && opts.lib.length) entries["library.s2lib"] = new Uint8Array(opts.lib);
6312
6635
  if (opts.types && opts.types.length) {
6313
6636
  entries["types.tgz"] = new Uint8Array(opts.types);
6314
6637
  }
@@ -6334,6 +6657,14 @@ var RegistryClient = class {
6334
6657
  const res = await this.request("types download", u4);
6335
6658
  return Buffer.from(await res.arrayBuffer());
6336
6659
  }
6660
+ /** A library's own artifact — a .s2lib, never the plugin .s2sp shape. */
6661
+ async downloadLib(name, version) {
6662
+ const u4 = new URL(`${this.baseUrl}/api/v1/download/lib`);
6663
+ u4.searchParams.set("name", name);
6664
+ u4.searchParams.set("version", version);
6665
+ const res = await this.request("library download", u4);
6666
+ return Buffer.from(await res.arrayBuffer());
6667
+ }
6337
6668
  async meta(name) {
6338
6669
  const u4 = new URL(`${this.baseUrl}/api/v1/meta`);
6339
6670
  u4.searchParams.set("name", name);
@@ -6365,10 +6696,19 @@ function assertDeployable(pkg, label) {
6365
6696
  }
6366
6697
  }
6367
6698
  function assembleDeployArchive(pluginDir, pkg, outPath) {
6368
- const absDir = resolve9(pluginDir);
6699
+ const absDir = resolve12(pluginDir);
6700
+ if (packageKind(pkg) === "library") {
6701
+ const lib = readFileSync11(outPath);
6702
+ const manifestEntry2 = unzipSync(lib)["manifest.json"];
6703
+ if (!manifestEntry2) {
6704
+ throw new Error(`built archive missing manifest.json: ${outPath}`);
6705
+ }
6706
+ const manifest2 = JSON.parse(Buffer.from(manifestEntry2).toString("utf8"));
6707
+ return { manifest: manifest2, s2sp: null, lib, types: null };
6708
+ }
6369
6709
  const gate = assertPublishesTypes(pkg, absDir);
6370
6710
  if (!gate.ok) throw new Error(`publish gate failed: ${gate.error}`);
6371
- const s2sp = readFileSync9(outPath);
6711
+ const s2sp = readFileSync11(outPath);
6372
6712
  const manifestEntry = unzipSync(s2sp)["manifest.json"];
6373
6713
  if (!manifestEntry) {
6374
6714
  throw new Error(`built archive missing manifest.json: ${outPath}`);
@@ -6383,11 +6723,11 @@ function assembleDeployArchive(pluginDir, pkg, outPath) {
6383
6723
  publishes: hasPublishes(manifest.publishes) ? manifest.publishes : void 0
6384
6724
  });
6385
6725
  }
6386
- return { manifest, s2sp, types };
6726
+ return { manifest, s2sp, lib: null, types };
6387
6727
  }
6388
6728
  async function deployPlugin(opts) {
6389
- const absDir = resolve9(opts.dir);
6390
- const pkg = JSON.parse(readFileSync9(resolve9(absDir, "package.json"), "utf8"));
6729
+ const absDir = resolve12(opts.dir);
6730
+ const pkg = JSON.parse(readFileSync11(resolve12(absDir, "package.json"), "utf8"));
6391
6731
  assertDeployable(pkg, absDir);
6392
6732
  const creds = loadCredentials();
6393
6733
  if (!creds?.token) {
@@ -6395,13 +6735,14 @@ async function deployPlugin(opts) {
6395
6735
  opts.ci ? "S2SCRIPT_TOKEN required in CI" : "not logged in \u2014 run `s2s login` or set S2SCRIPT_TOKEN"
6396
6736
  );
6397
6737
  }
6398
- const outPath = await buildPlugin(absDir, opts.packagesDir);
6399
- const { manifest, s2sp, types } = assembleDeployArchive(absDir, pkg, outPath);
6738
+ const outPath = packageKind(pkg) === "library" ? await buildLibrary(absDir, opts.packagesDir) : await buildPlugin(absDir, opts.packagesDir);
6739
+ const { manifest, s2sp, lib, types } = assembleDeployArchive(absDir, pkg, outPath);
6400
6740
  const client = new RegistryClient({
6401
6741
  baseUrl: opts.registryUrl || creds.registryUrl || defaultRegistryUrl(),
6402
- token: creds.token
6742
+ token: creds.token,
6743
+ fetch: opts.fetch
6403
6744
  });
6404
- return client.deploy({ manifest, s2sp, types });
6745
+ return client.deploy({ manifest, s2sp, lib, types });
6405
6746
  }
6406
6747
 
6407
6748
  // src/workspace/deploy-all.ts
@@ -6409,13 +6750,22 @@ function builtPluginsFromOutcomes(ws, outcomes) {
6409
6750
  const byName = new Map(ws.plugins.map((p) => [p.name, p]));
6410
6751
  const ordered = [];
6411
6752
  const built = /* @__PURE__ */ new Map();
6753
+ const unpublishableLibraries = [];
6412
6754
  for (const o of outcomes) {
6413
6755
  const plugin = byName.get(o.name);
6414
- if (!plugin) continue;
6756
+ if (!plugin) {
6757
+ unpublishableLibraries.push(o);
6758
+ continue;
6759
+ }
6415
6760
  ordered.push(plugin);
6416
6761
  if (o.outPath !== void 0) built.set(o.name, { plugin, outPath: o.outPath });
6417
6762
  }
6418
- return { ordered, built };
6763
+ return { ordered, built, unpublishableLibraries };
6764
+ }
6765
+ function formatUnpublishableLibraries(outcomes) {
6766
+ return outcomes.map(
6767
+ (o) => ` skip ${o.name} (library outside s2script.workspace.plugins \u2014 publish with \`s2s deploy ${o.relDir}\`)`
6768
+ ).join("\n");
6419
6769
  }
6420
6770
  async function isAlreadyPublished(client, name, version) {
6421
6771
  try {
@@ -6497,9 +6847,9 @@ async function run2(argv2) {
6497
6847
  "--stamp-version is a build-only flag (design spec 2026-07-27 \xA75.4) \u2014 s2s deploy never rewrites versions; run `s2s build --stamp-version <v>` first, then `s2s deploy`"
6498
6848
  );
6499
6849
  }
6500
- const absDir = resolve10(dir);
6850
+ const absDir = resolve13(dir);
6501
6851
  const root = findWorkspaceRoot(absDir);
6502
- if (root !== null && resolve10(root) === absDir) {
6852
+ if (root !== null && resolve13(root) === absDir) {
6503
6853
  await runWorkspaceDeploy({ root: absDir, argv: argv2, interactive, filters, dryRun, yes, ci });
6504
6854
  return;
6505
6855
  }
@@ -6560,7 +6910,7 @@ async function runWorkspaceDeploy(opts) {
6560
6910
  console.log(out);
6561
6911
  return out;
6562
6912
  };
6563
- const build2 = await buildWorkspace({
6913
+ const build3 = await buildWorkspace({
6564
6914
  workspace: ws,
6565
6915
  packagesDir,
6566
6916
  filters,
@@ -6570,13 +6920,16 @@ async function runWorkspaceDeploy(opts) {
6570
6920
  label(`building ${plugins.length} plugin${plugins.length === 1 ? "" : "s"} (dependency order)`);
6571
6921
  }
6572
6922
  });
6573
- if (build2.failed.length > 0) {
6574
- throw new Error(formatBuildFailures(build2.failed, build2.outcomes.length));
6923
+ if (build3.failed.length > 0) {
6924
+ throw new Error(formatBuildFailures(build3.failed, build3.outcomes.length));
6575
6925
  }
6576
- const { ordered, built } = builtPluginsFromOutcomes(ws, build2.outcomes);
6926
+ const { ordered, built, unpublishableLibraries } = builtPluginsFromOutcomes(ws, build3.outcomes);
6577
6927
  const plan = await computePlan(ordered, (name, version) => isAlreadyPublished(client, name, version));
6578
6928
  label("plan:");
6579
6929
  console.log(formatPlan(plan));
6930
+ if (unpublishableLibraries.length > 0) {
6931
+ console.log(formatUnpublishableLibraries(unpublishableLibraries));
6932
+ }
6580
6933
  const toPublish = publishCount(plan);
6581
6934
  if (toPublish === 0) {
6582
6935
  label("nothing to publish");
@@ -6623,17 +6976,17 @@ async function runWorkspaceDeploy(opts) {
6623
6976
  }
6624
6977
 
6625
6978
  // src/commands/version.ts
6626
- import { resolve as resolve11 } from "node:path";
6979
+ import { resolve as resolve14 } from "node:path";
6627
6980
 
6628
6981
  // src/version/version.ts
6629
- import { existsSync as existsSync8 } from "node:fs";
6630
- import { join as join13 } from "node:path";
6982
+ import { existsSync as existsSync10 } from "node:fs";
6983
+ import { join as join16 } from "node:path";
6631
6984
 
6632
6985
  // src/version/changesets.ts
6633
6986
  var import_semver = __toESM(require_semver2(), 1);
6634
- import { readFileSync as readFileSync10 } from "node:fs";
6987
+ import { readFileSync as readFileSync12 } from "node:fs";
6635
6988
  import { createRequire as createRequire2 } from "node:module";
6636
- import { join as join11 } from "node:path";
6989
+ import { join as join14 } from "node:path";
6637
6990
  import { pathToFileURL } from "node:url";
6638
6991
  var SUPPORTED = {
6639
6992
  "@changesets/assemble-release-plan": ">=6.0.0 <7.0.0",
@@ -6653,7 +7006,7 @@ function named(mod, key) {
6653
7006
  return outer?.[key];
6654
7007
  }
6655
7008
  async function loadChangesets(workspaceRoot) {
6656
- const require3 = createRequire2(join11(workspaceRoot, "package.json"));
7009
+ const require3 = createRequire2(join14(workspaceRoot, "package.json"));
6657
7010
  const missing = [];
6658
7011
  const wrongVersion = [];
6659
7012
  const resolved = /* @__PURE__ */ new Map();
@@ -6668,7 +7021,7 @@ async function loadChangesets(workspaceRoot) {
6668
7021
  missing.push(name);
6669
7022
  continue;
6670
7023
  }
6671
- const version = String(JSON.parse(readFileSync10(manifest, "utf8")).version ?? "");
7024
+ const version = String(JSON.parse(readFileSync12(manifest, "utf8")).version ?? "");
6672
7025
  if (!(0, import_semver.satisfies)(version, range, { includePrerelease: true })) {
6673
7026
  wrongVersion.push(`${name}@${version} (supported: ${range})`);
6674
7027
  continue;
@@ -6768,8 +7121,8 @@ function mirrorPackages(ws) {
6768
7121
 
6769
7122
  // src/version/ranges.ts
6770
7123
  var import_semver2 = __toESM(require_semver2(), 1);
6771
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "node:fs";
6772
- import { join as join12 } from "node:path";
7124
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "node:fs";
7125
+ import { join as join15 } from "node:path";
6773
7126
  var BUMP_LEVEL = { none: 0, patch: 1, minor: 2, major: 3 };
6774
7127
  function rangePrefix(range) {
6775
7128
  if (range.startsWith(">=")) return ">=";
@@ -6788,7 +7141,7 @@ function shouldUpdate(release, range, config) {
6788
7141
  return BUMP_LEVEL[release.type] >= minLevel;
6789
7142
  }
6790
7143
  function readJsonFile(path) {
6791
- const raw = readFileSync11(path, "utf8");
7144
+ const raw = readFileSync13(path, "utf8");
6792
7145
  const match = /^[ \t]*[{[]\s*\n([ \t]+)/.exec(raw);
6793
7146
  return {
6794
7147
  json: JSON.parse(raw),
@@ -6829,12 +7182,12 @@ function rewriteSiblingRanges(ws, releases, config) {
6829
7182
  for (const [consumerName, ifaceRanges] of edits) {
6830
7183
  const plugin = ws.plugins.find((p) => p.name === consumerName);
6831
7184
  if (plugin === void 0) continue;
6832
- const path = join12(plugin.dir, "package.json");
7185
+ const path = join15(plugin.dir, "package.json");
6833
7186
  const { json, indent, eol } = readJsonFile(path);
6834
7187
  const s2 = json.s2script;
6835
7188
  if (s2?.pluginDependencies === void 0) continue;
6836
7189
  for (const [iface, range] of ifaceRanges) s2.pluginDependencies[iface] = range;
6837
- writeFileSync5(path, JSON.stringify(json, null, indent) + eol);
7190
+ writeFileSync6(path, JSON.stringify(json, null, indent) + eol);
6838
7191
  touchedFiles.push(path);
6839
7192
  }
6840
7193
  return { rewrites, skips, touchedFiles };
@@ -6843,8 +7196,8 @@ function rewriteSiblingRanges(ws, releases, config) {
6843
7196
  // src/version/version.ts
6844
7197
  async function versionWorkspace(root, opts = {}) {
6845
7198
  const ws = loadWorkspace(root);
6846
- const configPath = join13(ws.root, ".changeset", "config.json");
6847
- if (!existsSync8(configPath)) {
7199
+ const configPath = join16(ws.root, ".changeset", "config.json");
7200
+ if (!existsSync10(configPath)) {
6848
7201
  throw new Error(
6849
7202
  `s2s version: ${configPath} does not exist \u2014 this workspace is not set up for changesets. Run \`npx changeset init\` at ${ws.root}.`
6850
7203
  );
@@ -6882,7 +7235,7 @@ function resolveVersionRoot(from) {
6882
7235
  async function run3(argv2) {
6883
7236
  const interactive = isInteractive();
6884
7237
  const [dirArg] = positionals(argv2, ["--since"]);
6885
- const from = resolve11(dirArg ?? ".");
7238
+ const from = resolve14(dirArg ?? ".");
6886
7239
  try {
6887
7240
  const root = resolveVersionRoot(from);
6888
7241
  const result = await task(
@@ -6919,21 +7272,21 @@ async function run3(argv2) {
6919
7272
  }
6920
7273
 
6921
7274
  // src/commands/add.ts
6922
- import { resolve as resolve13 } from "node:path";
7275
+ import { resolve as resolve16 } from "node:path";
6923
7276
 
6924
7277
  // src/registry/add.ts
6925
7278
  import {
6926
- mkdirSync as mkdirSync3,
6927
- writeFileSync as writeFileSync7,
6928
- readFileSync as readFileSync13,
6929
- existsSync as existsSync10
7279
+ mkdirSync as mkdirSync5,
7280
+ writeFileSync as writeFileSync9,
7281
+ readFileSync as readFileSync15,
7282
+ existsSync as existsSync12
6930
7283
  } from "node:fs";
6931
- import { join as join15, resolve as resolve12 } from "node:path";
7284
+ import { join as join19, resolve as resolve15 } from "node:path";
6932
7285
  import { gunzipSync } from "node:zlib";
6933
7286
 
6934
7287
  // src/registry/npmrc.ts
6935
- import { existsSync as existsSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "node:fs";
6936
- import { join as join14 } from "node:path";
7288
+ import { existsSync as existsSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
7289
+ import { join as join17 } from "node:path";
6937
7290
  function npmCompatRegistryUrl(registryUrl) {
6938
7291
  return `${registryUrl.replace(/\/$/, "")}/npm/`;
6939
7292
  }
@@ -6949,8 +7302,8 @@ function ensureScopeNpmrc(projectDir, packageName, registryUrl) {
6949
7302
  const compat = npmCompatRegistryUrl(registryUrl);
6950
7303
  const key = `@${scope}:registry`;
6951
7304
  const line = `${key}=${compat}`;
6952
- const npmrcPath = join14(projectDir, ".npmrc");
6953
- let existing = existsSync9(npmrcPath) ? readFileSync12(npmrcPath, "utf8") : "";
7305
+ const npmrcPath = join17(projectDir, ".npmrc");
7306
+ let existing = existsSync11(npmrcPath) ? readFileSync14(npmrcPath, "utf8") : "";
6954
7307
  const lines = existing.split(/\r?\n/);
6955
7308
  const idx = lines.findIndex((l2) => l2.trimStart().startsWith(`${key}=`) || l2.trimStart().startsWith(`${key} =`));
6956
7309
  if (idx >= 0) {
@@ -6960,10 +7313,39 @@ function ensureScopeNpmrc(projectDir, packageName, registryUrl) {
6960
7313
  lines.push(line);
6961
7314
  }
6962
7315
  const out = lines.filter((l2, i2, a2) => !(l2 === "" && a2[i2 - 1] === "")).join("\n");
6963
- writeFileSync6(npmrcPath, out.endsWith("\n") ? out : out + "\n");
7316
+ writeFileSync7(npmrcPath, out.endsWith("\n") ? out : out + "\n");
6964
7317
  return line;
6965
7318
  }
6966
7319
 
7320
+ // src/registry/lib-extract.ts
7321
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync8 } from "node:fs";
7322
+ import { join as join18 } from "node:path";
7323
+ var WANTED = /* @__PURE__ */ new Set(["index.js", "index.d.ts"]);
7324
+ function extractLibArchive(s2lib, outDir, meta) {
7325
+ const entries = unzipSync(new Uint8Array(s2lib));
7326
+ if (!entries["index.js"]?.length) throw new Error("library archive missing index.js");
7327
+ if (!entries["index.d.ts"]?.length) throw new Error("library archive missing index.d.ts");
7328
+ mkdirSync4(outDir, { recursive: true });
7329
+ for (const [name, bytes] of Object.entries(entries)) {
7330
+ if (!WANTED.has(name)) continue;
7331
+ writeFileSync8(join18(outDir, name), Buffer.from(bytes));
7332
+ }
7333
+ let apiVersion = "1.x";
7334
+ try {
7335
+ const m = JSON.parse(Buffer.from(entries["manifest.json"] ?? new Uint8Array()).toString("utf8"));
7336
+ if (typeof m.apiVersion === "string") apiVersion = m.apiVersion;
7337
+ } catch {
7338
+ }
7339
+ writeFileSync8(
7340
+ join18(outDir, "package.json"),
7341
+ JSON.stringify(
7342
+ { name: meta.name, version: meta.version, main: "index.js", types: "index.d.ts", s2script: { kind: "library", apiVersion } },
7343
+ null,
7344
+ 2
7345
+ ) + "\n"
7346
+ );
7347
+ }
7348
+
6967
7349
  // src/registry/add.ts
6968
7350
  function parseSpec(spec) {
6969
7351
  if (spec.startsWith("@")) {
@@ -6977,7 +7359,7 @@ function parseSpec(spec) {
6977
7359
  }
6978
7360
  function extractTypesTarball(tgz, outDir) {
6979
7361
  const tar = gunzipSync(tgz);
6980
- mkdirSync3(outDir, { recursive: true });
7362
+ mkdirSync5(outDir, { recursive: true });
6981
7363
  let offset = 0;
6982
7364
  let wroteContract = false;
6983
7365
  while (offset + 512 <= tar.length) {
@@ -6992,45 +7374,73 @@ function extractTypesTarball(tgz, outDir) {
6992
7374
  if (!name || name.endsWith("/")) continue;
6993
7375
  const base = name.replace(/^package\//, "");
6994
7376
  if (base === "package.json") {
6995
- writeFileSync7(join15(outDir, "package.json"), content);
7377
+ writeFileSync9(join19(outDir, "package.json"), content);
6996
7378
  } else if (base === "api.d.ts" || base === "index.d.ts") {
6997
- writeFileSync7(join15(outDir, "index.d.ts"), content);
7379
+ writeFileSync9(join19(outDir, "index.d.ts"), content);
6998
7380
  wroteContract = true;
6999
7381
  }
7000
7382
  }
7001
- if (!wroteContract && !existsSync10(join15(outDir, "index.d.ts"))) {
7383
+ if (!wroteContract && !existsSync12(join19(outDir, "index.d.ts"))) {
7002
7384
  throw new Error("types tarball missing package/api.d.ts (or index.d.ts)");
7003
7385
  }
7004
7386
  }
7387
+ function rangeForResolved(range, version) {
7388
+ const major2 = version.split(".")[0] ?? "0";
7389
+ return range.startsWith("^") || range === "*" ? `^${major2}.0.0` : range.includes(".") ? range : `^${version}`;
7390
+ }
7005
7391
  async function addPackage(opts) {
7006
- const absDir = resolve12(opts.pluginDir);
7392
+ const absDir = resolve15(opts.pluginDir);
7007
7393
  const { name, range } = parseSpec(opts.spec);
7008
7394
  const creds = loadCredentials();
7009
7395
  const client = new RegistryClient({
7010
7396
  baseUrl: opts.registryUrl || creds?.registryUrl || defaultRegistryUrl(),
7011
- token: creds?.token
7397
+ token: creds?.token,
7398
+ fetch: opts.fetch
7012
7399
  });
7013
7400
  const resolved = await client.resolve(name, range);
7401
+ if (resolved.kind === "library") {
7402
+ const s2lib = await client.downloadLib(name, resolved.version);
7403
+ const libDir = join19(absDir, ".s2script", "libs", ...name.split("/"));
7404
+ extractLibArchive(s2lib, libDir, { name, version: resolved.version });
7405
+ const pkgPath2 = join19(absDir, "package.json");
7406
+ const pkg2 = JSON.parse(readFileSync15(pkgPath2, "utf8"));
7407
+ pkg2.s2script = pkg2.s2script ?? {};
7408
+ pkg2.s2script.libraries = pkg2.s2script.libraries ?? {};
7409
+ pkg2.s2script.libraries[name] = rangeForResolved(range, resolved.version);
7410
+ writeFileSync9(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
7411
+ return {
7412
+ kind: "library",
7413
+ name: resolved.name,
7414
+ version: resolved.version,
7415
+ reviewState: resolved.reviewState,
7416
+ typesDir: null,
7417
+ libDir,
7418
+ // Deliberately NO .npmrc line: nothing about a library may look like an npm
7419
+ // package — that is the exact confusion this feature exists to prevent.
7420
+ npmrcLine: null
7421
+ };
7422
+ }
7014
7423
  if (!resolved.hasTypes) {
7015
7424
  throw new Error(`${name}@${resolved.version} has no types artifact (runtime-only package?)`);
7016
7425
  }
7017
7426
  const tgz = await client.downloadTypes(name, resolved.version);
7018
- const typesDir = join15(absDir, ".s2script", "types", ...name.split("/"));
7427
+ const typesDir = join19(absDir, ".s2script", "types", ...name.split("/"));
7019
7428
  extractTypesTarball(tgz, typesDir);
7020
- const pkgPath = join15(absDir, "package.json");
7021
- const pkg = JSON.parse(readFileSync13(pkgPath, "utf8"));
7429
+ const pkgPath = join19(absDir, "package.json");
7430
+ const pkg = JSON.parse(readFileSync15(pkgPath, "utf8"));
7022
7431
  pkg.s2script = pkg.s2script ?? {};
7023
7432
  pkg.s2script.pluginDependencies = pkg.s2script.pluginDependencies ?? {};
7024
- const major = resolved.version.split(".")[0] ?? "0";
7025
- pkg.s2script.pluginDependencies[name] = range.startsWith("^") || range === "*" ? `^${major}.0.0` : range.includes(".") ? range : `^${resolved.version}`;
7026
- writeFileSync7(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
7433
+ pkg.s2script.pluginDependencies[name] = rangeForResolved(range, resolved.version);
7434
+ writeFileSync9(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
7027
7435
  const registryUrl = opts.registryUrl || creds?.registryUrl || defaultRegistryUrl();
7028
7436
  const npmrcLine = ensureScopeNpmrc(absDir, name, registryUrl);
7029
7437
  return {
7438
+ kind: "plugin",
7030
7439
  name: resolved.name,
7031
7440
  version: resolved.version,
7032
7441
  reviewState: resolved.reviewState,
7033
7442
  typesDir,
7443
+ libDir: null,
7034
7444
  npmrcLine
7035
7445
  };
7036
7446
  }
@@ -7049,9 +7459,9 @@ async function run4(argv2) {
7049
7459
  }
7050
7460
  try {
7051
7461
  const pluginDir = parseFlag(argv2, "--dir") || ".";
7052
- const absDir = resolve13(pluginDir);
7462
+ const absDir = resolve16(pluginDir);
7053
7463
  const root = findWorkspaceRoot(absDir);
7054
- if (root !== null && resolve13(root) === absDir) {
7464
+ if (root !== null && resolve16(root) === absDir) {
7055
7465
  throw new Error(
7056
7466
  `${absDir} is a workspace root (design spec 2026-07-27 \xA74.1), not a plugin \u2014 \`s2s add\` needs a plugin directory: pass --dir <plugin> (e.g. --dir plugins/shop)`
7057
7467
  );
@@ -7063,14 +7473,24 @@ async function run4(argv2) {
7063
7473
  spec,
7064
7474
  registryUrl: parseFlag(argv2, "--registry")
7065
7475
  }),
7066
- { interactive, done: (r2) => `Added ${r2.name}@${r2.version} \u2192 ${r2.typesDir}` }
7476
+ {
7477
+ interactive,
7478
+ done: (r2) => r2.kind === "library" ? `Added library ${r2.name}@${r2.version} \u2192 ${r2.libDir} (commit this directory)` : `Added ${r2.name}@${r2.version} \u2192 ${r2.typesDir}`
7479
+ }
7067
7480
  );
7068
7481
  if (interactive) {
7069
7482
  if (result.npmrcLine) {
7070
7483
  log2.info(`npmrc: ${result.npmrcLine} (types-only; npm install ${result.name} also works)`);
7071
7484
  }
7072
7485
  if (result.reviewState === "unreviewed") {
7073
- log2.warn("Not reviewed by s2script \u2014 types pulled; use at your own risk.");
7486
+ log2.warn(
7487
+ result.kind === "library" ? "Not reviewed by s2script \u2014 library pulled; use at your own risk." : "Not reviewed by s2script \u2014 types pulled; use at your own risk."
7488
+ );
7489
+ }
7490
+ } else if (result.kind === "library") {
7491
+ console.log(`added library ${result.name}@${result.version} \u2192 ${result.libDir}`);
7492
+ if (result.reviewState === "unreviewed") {
7493
+ console.log("note: Not reviewed by s2script \u2014 library pulled; use at your own risk.");
7074
7494
  }
7075
7495
  } else {
7076
7496
  console.log(`added ${result.name}@${result.version} \u2192 ${result.typesDir}`);
@@ -7088,13 +7508,13 @@ async function run4(argv2) {
7088
7508
  }
7089
7509
 
7090
7510
  // src/commands/install.ts
7091
- import { resolve as resolve15 } from "node:path";
7092
- import { existsSync as existsSync12 } from "node:fs";
7511
+ import { resolve as resolve18 } from "node:path";
7512
+ import { existsSync as existsSync14 } from "node:fs";
7093
7513
 
7094
7514
  // src/registry/install.ts
7095
7515
  import { createHash as createHash2 } from "node:crypto";
7096
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync8, renameSync, existsSync as existsSync11 } from "node:fs";
7097
- import { basename as basename2, join as join16, resolve as resolve14, sep as sep2 } from "node:path";
7516
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync10, renameSync, existsSync as existsSync13 } from "node:fs";
7517
+ import { basename as basename2, join as join20, resolve as resolve17, sep as sep3 } from "node:path";
7098
7518
  function parseManifest(input) {
7099
7519
  if (!input || typeof input !== "object" || Array.isArray(input)) {
7100
7520
  throw new Error("manifest must be a JSON object");
@@ -7177,14 +7597,14 @@ async function installPlan(opts) {
7177
7597
  );
7178
7598
  }
7179
7599
  }
7180
- const dirResolved = resolve14(opts.dir);
7600
+ const dirResolved = resolve17(opts.dir);
7181
7601
  const targets = install.map((e) => {
7182
7602
  if (!e.sha256) {
7183
7603
  throw new Error(`refusing ${e.name}@${e.version}: registry did not provide a sha256 to verify against`);
7184
7604
  }
7185
7605
  const filename = safeFilename(e);
7186
- const dest = resolve14(join16(opts.dir, filename));
7187
- if (dest !== dirResolved && !dest.startsWith(dirResolved + sep2)) {
7606
+ const dest = resolve17(join20(opts.dir, filename));
7607
+ if (dest !== dirResolved && !dest.startsWith(dirResolved + sep3)) {
7188
7608
  throw new Error(`refusing ${e.name}@${e.version}: destination "${dest}" escapes plugins dir "${dirResolved}"`);
7189
7609
  }
7190
7610
  return { entry: e, filename, dest };
@@ -7194,12 +7614,12 @@ async function installPlan(opts) {
7194
7614
  for (const e of install) log3(`would install ${e.name}@${e.version} -> ${e.filename}`);
7195
7615
  return { written: [], skipped: [], warnings };
7196
7616
  }
7197
- mkdirSync4(opts.dir, { recursive: true });
7617
+ mkdirSync6(opts.dir, { recursive: true });
7198
7618
  const written = [];
7199
7619
  const skipped = [];
7200
7620
  for (const { entry: e, filename, dest } of targets) {
7201
- if (existsSync11(dest)) {
7202
- const have = sha256Hex(readFileSync14(dest));
7621
+ if (existsSync13(dest)) {
7622
+ const have = sha256Hex(readFileSync16(dest));
7203
7623
  if (have === e.sha256) {
7204
7624
  skipped.push(filename);
7205
7625
  log3(`up to date ${filename}`);
@@ -7212,7 +7632,7 @@ async function installPlan(opts) {
7212
7632
  throw new Error(`sha256 mismatch for ${e.name}@${e.version}: expected ${e.sha256}, got ${got}`);
7213
7633
  }
7214
7634
  const tmp = `${dest}.${process.pid}.tmp`;
7215
- writeFileSync8(tmp, bytes);
7635
+ writeFileSync10(tmp, bytes);
7216
7636
  renameSync(tmp, dest);
7217
7637
  written.push(filename);
7218
7638
  log3(`installed ${e.name}@${e.version} -> ${filename}`);
@@ -7220,8 +7640,8 @@ async function installPlan(opts) {
7220
7640
  return { written, skipped, warnings };
7221
7641
  }
7222
7642
  function loadManifestFile(path) {
7223
- if (!existsSync11(path)) return { plugins: {} };
7224
- return parseManifest(JSON.parse(readFileSync14(path, "utf8")));
7643
+ if (!existsSync13(path)) return { plugins: {} };
7644
+ return parseManifest(JSON.parse(readFileSync16(path, "utf8")));
7225
7645
  }
7226
7646
 
7227
7647
  // src/commands/install.ts
@@ -7234,7 +7654,7 @@ async function run5(argv2) {
7234
7654
  const reviewedOnly = hasFlag(argv2, "--reviewed-only");
7235
7655
  let dir = parseFlag(argv2, "--dir");
7236
7656
  if (!dir) {
7237
- if (existsSync12(resolve15(AUTODETECT))) dir = resolve15(AUTODETECT);
7657
+ if (existsSync14(resolve18(AUTODETECT))) dir = resolve18(AUTODETECT);
7238
7658
  else {
7239
7659
  console.error(
7240
7660
  `Usage: s2s install [name[@range]...] --dir <plugins dir>
@@ -7245,7 +7665,7 @@ async function run5(argv2) {
7245
7665
  }
7246
7666
  const client = new RegistryClient({ baseUrl: registry });
7247
7667
  try {
7248
- const manifest = loadManifestFile(resolve15(file));
7668
+ const manifest = loadManifestFile(resolve18(file));
7249
7669
  const specs = mergeSpecs(manifest, args);
7250
7670
  if (Object.keys(specs).length === 0) {
7251
7671
  console.error(
@@ -7275,32 +7695,32 @@ async function run5(argv2) {
7275
7695
  }
7276
7696
 
7277
7697
  // src/commands/create.ts
7278
- import { resolve as resolve18 } from "node:path";
7698
+ import { resolve as resolve21 } from "node:path";
7279
7699
 
7280
7700
  // src/create/create.ts
7281
7701
  import {
7282
- mkdirSync as mkdirSync5,
7283
- writeFileSync as writeFileSync9,
7284
- existsSync as existsSync13,
7702
+ mkdirSync as mkdirSync7,
7703
+ writeFileSync as writeFileSync11,
7704
+ existsSync as existsSync15,
7285
7705
  readdirSync as readdirSync4,
7286
- readFileSync as readFileSync15,
7706
+ readFileSync as readFileSync17,
7287
7707
  symlinkSync
7288
7708
  } from "node:fs";
7289
- import { dirname as dirname4, isAbsolute, join as join17, relative as relative2, resolve as resolve16, basename as basename3 } from "node:path";
7709
+ import { dirname as dirname5, isAbsolute, join as join21, relative as relative2, resolve as resolve19, basename as basename3 } from "node:path";
7290
7710
  import { spawnSync } from "node:child_process";
7291
7711
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7292
7712
  function readCliVersion() {
7293
- const here = dirname4(fileURLToPath2(import.meta.url));
7713
+ const here = dirname5(fileURLToPath2(import.meta.url));
7294
7714
  const candidates = [
7295
- join17(here, "..", "package.json"),
7715
+ join21(here, "..", "package.json"),
7296
7716
  // dist/cli.js → packages/sdk/package.json
7297
- join17(here, "..", "..", "package.json")
7717
+ join21(here, "..", "..", "package.json")
7298
7718
  // src/create → packages/sdk/package.json
7299
7719
  ];
7300
7720
  for (const p of candidates) {
7301
- if (!existsSync13(p)) continue;
7721
+ if (!existsSync15(p)) continue;
7302
7722
  try {
7303
- const pkg = JSON.parse(readFileSync15(p, "utf8"));
7723
+ const pkg = JSON.parse(readFileSync17(p, "utf8"));
7304
7724
  if (pkg.name === "@s2script/sdk" && pkg.version) return pkg.version;
7305
7725
  } catch {
7306
7726
  }
@@ -7308,27 +7728,27 @@ function readCliVersion() {
7308
7728
  return "0.1.0";
7309
7729
  }
7310
7730
  function findLocalPackagesDir() {
7311
- const here = dirname4(fileURLToPath2(import.meta.url));
7731
+ const here = dirname5(fileURLToPath2(import.meta.url));
7312
7732
  const candidates = [
7313
- join17(here, "..", ".."),
7733
+ join21(here, "..", ".."),
7314
7734
  // dist/cli.js → packages
7315
- join17(here, "..", "..", "..")
7735
+ join21(here, "..", "..", "..")
7316
7736
  // src/create → packages
7317
7737
  ];
7318
7738
  for (const c2 of candidates) {
7319
- if (isPackagesDir(c2)) return resolve16(c2);
7739
+ if (isPackagesDir(c2)) return resolve19(c2);
7320
7740
  }
7321
7741
  return void 0;
7322
7742
  }
7323
7743
  function slugifyBasename(dir, fallback = "plugin") {
7324
- const base = basename3(resolve16(dir));
7744
+ const base = basename3(resolve19(dir));
7325
7745
  return base.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || fallback;
7326
7746
  }
7327
- function defaultNameFromPath(dir) {
7328
- return `@plugin/${slugifyBasename(dir)}`;
7747
+ function defaultNameFromPath(dir, kind = "plugin") {
7748
+ return `@${kind}/${slugifyBasename(dir)}`;
7329
7749
  }
7330
7750
  function assertEmptyTarget(targetPath) {
7331
- if (!existsSync13(targetPath)) return;
7751
+ if (!existsSync15(targetPath)) return;
7332
7752
  const kids = readdirSync4(targetPath);
7333
7753
  const meaningful = kids.filter((k) => k !== ".git");
7334
7754
  if (meaningful.length) {
@@ -7367,18 +7787,18 @@ function versionSpecFrom(status, stdout2) {
7367
7787
  const v = (stdout2 ?? "").trim();
7368
7788
  return status === 0 && /^\d+\.\d+\.\d+/.test(v) ? `^${v}` : "latest";
7369
7789
  }
7370
- function registryDevDeps(game, sdkVersion, resolve19 = resolvePublishedVersion) {
7790
+ function registryDevDeps(game, sdkVersion, resolve22 = resolvePublishedVersion) {
7371
7791
  const deps = {};
7372
7792
  for (const n3 of createPackageNames(game)) {
7373
- deps[`@s2script/${n3}`] = n3 === "sdk" ? `^${sdkVersion}` : resolve19(`@s2script/${n3}`);
7793
+ deps[`@s2script/${n3}`] = n3 === "sdk" ? `^${sdkVersion}` : resolve22(`@s2script/${n3}`);
7374
7794
  }
7375
7795
  return deps;
7376
7796
  }
7377
7797
  function fileDevDeps(packagesDir, game) {
7378
7798
  const deps = {};
7379
7799
  for (const n3 of createPackageNames(game)) {
7380
- const abs = join17(packagesDir, n3);
7381
- if (!existsSync13(join17(abs, "package.json"))) return void 0;
7800
+ const abs = join21(packagesDir, n3);
7801
+ if (!existsSync15(join21(abs, "package.json"))) return void 0;
7382
7802
  deps[`@s2script/${n3}`] = `file:${abs}`;
7383
7803
  }
7384
7804
  return deps;
@@ -7410,6 +7830,13 @@ export default plugin((ctx) => {
7410
7830
  });
7411
7831
  `;
7412
7832
  }
7833
+ var LIBRARY_ENTRY = `/** Encode a string. Replace this with the library's real surface. */
7834
+ export function encode(input: string): string {
7835
+ return input;
7836
+ }
7837
+ `;
7838
+ var LIBRARY_TYPES = `export declare function encode(input: string): string;
7839
+ `;
7413
7840
  var ESLINT_RANGE = "^10.7.0";
7414
7841
  function eslintConfig() {
7415
7842
  return `import s2script from "@s2script/eslint-plugin";
@@ -7435,7 +7862,7 @@ function tsconfigJson() {
7435
7862
  ) + "\n";
7436
7863
  }
7437
7864
  function workspaceTsconfigJson(targetPath, workspaceRoot) {
7438
- const rel = relative2(targetPath, join17(workspaceRoot, "tsconfig.base.json")).replace(/\\/g, "/");
7865
+ const rel = relative2(targetPath, join21(workspaceRoot, "tsconfig.base.json")).replace(/\\/g, "/");
7439
7866
  return JSON.stringify(
7440
7867
  {
7441
7868
  extends: rel.startsWith(".") ? rel : `./${rel}`,
@@ -7481,6 +7908,28 @@ function packageJsonContent(name, game, version, localPackagesDir) {
7481
7908
  2
7482
7909
  ) + "\n";
7483
7910
  }
7911
+ function libraryPackageJsonContent(name, game, version, localPackagesDir) {
7912
+ const fileDeps = localPackagesDir ? fileDevDeps(localPackagesDir, game) : void 0;
7913
+ const devDependencies = {
7914
+ ...fileDeps ?? registryDevDeps(game, version),
7915
+ eslint: ESLINT_RANGE
7916
+ };
7917
+ return JSON.stringify(
7918
+ {
7919
+ name,
7920
+ version: "0.1.0",
7921
+ main: "src/index.ts",
7922
+ types: "src/index.d.ts",
7923
+ scripts: {
7924
+ build: "s2s build ."
7925
+ },
7926
+ s2script: { kind: "library" },
7927
+ devDependencies
7928
+ },
7929
+ null,
7930
+ 2
7931
+ ) + "\n";
7932
+ }
7484
7933
  function gitignore() {
7485
7934
  return `node_modules/
7486
7935
  dist/
@@ -7489,10 +7938,10 @@ dist/
7489
7938
  `;
7490
7939
  }
7491
7940
  function linkLocalPackagesForNoInstall(targetPath, localPackagesDir) {
7492
- const nm = join17(targetPath, "node_modules");
7493
- mkdirSync5(nm, { recursive: true });
7494
- const dest = join17(nm, "@s2script");
7495
- if (!existsSync13(dest)) symlinkSync(localPackagesDir, dest);
7941
+ const nm = join21(targetPath, "node_modules");
7942
+ mkdirSync7(nm, { recursive: true });
7943
+ const dest = join21(nm, "@s2script");
7944
+ if (!existsSync15(dest)) symlinkSync(localPackagesDir, dest);
7496
7945
  }
7497
7946
  function runInstall(dir, pm) {
7498
7947
  if (pm === "none") return;
@@ -7508,14 +7957,20 @@ async function createPlugin(opts = {}) {
7508
7957
  const interactive = isInteractive({ yes: opts.yes });
7509
7958
  let game = assertGame(opts.game);
7510
7959
  let name = opts.name;
7960
+ let kind = opts.kind ?? "plugin";
7511
7961
  let install = opts.noInstall ? "none" : assertInstall(opts.install);
7512
7962
  const template = opts.template ?? "minimal";
7513
7963
  if (template !== "minimal") {
7514
7964
  throw new Error(`unknown template ${JSON.stringify(template)} (v1 supports: minimal)`);
7515
7965
  }
7516
7966
  const workspaceRoot = opts.workspaceRoot;
7967
+ if (kind === "library" && workspaceRoot !== void 0) {
7968
+ throw new Error(
7969
+ `s2s create --library does not support workspace mode yet (workspace detected at ${workspaceRoot}) \u2014 a workspace library needs to sit outside the plugins/ glob (e.g. a sibling directory like libs/<name>, matched by npm's own "workspaces" field but not by s2script.workspace.plugins \u2014 see examples/workspace-library for a worked one); add it by hand for now`
7970
+ );
7971
+ }
7517
7972
  let slug = workspaceRoot !== void 0 && opts.path !== void 0 ? assertBareSlug(opts.path) : opts.path;
7518
- let targetPath = workspaceRoot === void 0 ? resolve16(opts.path ?? ".") : slug === void 0 ? void 0 : join17(workspaceRoot, "plugins", slug);
7973
+ let targetPath = workspaceRoot === void 0 ? resolve19(opts.path ?? ".") : slug === void 0 ? void 0 : join21(workspaceRoot, "plugins", slug);
7519
7974
  if (targetPath !== void 0) assertEmptyTarget(targetPath);
7520
7975
  if (interactive) {
7521
7976
  intro2("Create an s2script plugin");
@@ -7525,10 +7980,20 @@ async function createPlugin(opts = {}) {
7525
7980
  slug = assertBareSlug(
7526
7981
  await text2({ message: "Plugin name (directory under plugins/)", placeholder: "myplugin" })
7527
7982
  );
7528
- targetPath = join17(workspaceRoot, "plugins", slug);
7983
+ targetPath = join21(workspaceRoot, "plugins", slug);
7529
7984
  assertEmptyTarget(targetPath);
7530
7985
  }
7531
7986
  }
7987
+ if (opts.kind === void 0 && workspaceRoot === void 0) {
7988
+ kind = await select2({
7989
+ message: "What are you creating?",
7990
+ options: [
7991
+ { value: "plugin", label: "Plugin \u2014 loads on a server" },
7992
+ { value: "library", label: "Library \u2014 build-time code other plugins bundle" }
7993
+ ],
7994
+ initialValue: "plugin"
7995
+ });
7996
+ }
7532
7997
  if (!game) {
7533
7998
  game = await select2({
7534
7999
  message: "Which game?",
@@ -7542,9 +8007,9 @@ async function createPlugin(opts = {}) {
7542
8007
  if (!name) {
7543
8008
  const known = targetPath;
7544
8009
  name = await text2({
7545
- message: "Plugin package name",
7546
- defaultValue: defaultNameFromPath(known),
7547
- placeholder: defaultNameFromPath(known)
8010
+ message: kind === "library" ? "Library package name" : "Plugin package name",
8011
+ defaultValue: defaultNameFromPath(known, kind),
8012
+ placeholder: defaultNameFromPath(known, kind)
7548
8013
  });
7549
8014
  }
7550
8015
  if (workspaceRoot === void 0 && !install) {
@@ -7561,7 +8026,7 @@ async function createPlugin(opts = {}) {
7561
8026
  });
7562
8027
  }
7563
8028
  const proceed = await confirm2({
7564
- message: workspaceRoot !== void 0 ? `Add ${name} to the workspace at ${workspaceRoot}?` : `Create ${name} (${game}) in ${targetPath}?`,
8029
+ message: workspaceRoot !== void 0 ? `Add ${name} to the workspace at ${workspaceRoot}?` : kind === "library" ? `Create library ${name} in ${targetPath}?` : `Create ${name} (${game}) in ${targetPath}?`,
7565
8030
  initialValue: true
7566
8031
  });
7567
8032
  if (!proceed) {
@@ -7574,28 +8039,41 @@ async function createPlugin(opts = {}) {
7574
8039
  );
7575
8040
  }
7576
8041
  game = game ?? "cs2";
7577
- name = name ?? defaultNameFromPath(targetPath);
8042
+ name = name ?? defaultNameFromPath(targetPath, kind);
7578
8043
  install = workspaceRoot !== void 0 ? "none" : install ?? (opts.noInstall ? "none" : "npm");
7579
8044
  const finalTargetPath = targetPath;
7580
8045
  const localPackagesDir = workspaceRoot === void 0 ? findLocalPackagesDir() : void 0;
7581
- mkdirSync5(join17(finalTargetPath, "src"), { recursive: true });
8046
+ mkdirSync7(join21(finalTargetPath, "src"), { recursive: true });
7582
8047
  if (workspaceRoot !== void 0) {
7583
- writeFileSync9(join17(finalTargetPath, "package.json"), minimalPackageJsonContent(name));
7584
- writeFileSync9(
7585
- join17(finalTargetPath, "tsconfig.json"),
8048
+ writeFileSync11(join21(finalTargetPath, "package.json"), minimalPackageJsonContent(name));
8049
+ writeFileSync11(
8050
+ join21(finalTargetPath, "tsconfig.json"),
7586
8051
  workspaceTsconfigJson(finalTargetPath, workspaceRoot)
7587
8052
  );
8053
+ } else if (kind === "library") {
8054
+ const version = readCliVersion();
8055
+ writeFileSync11(
8056
+ join21(finalTargetPath, "package.json"),
8057
+ libraryPackageJsonContent(name, game, version, localPackagesDir)
8058
+ );
8059
+ writeFileSync11(join21(finalTargetPath, "tsconfig.json"), tsconfigJson());
8060
+ writeFileSync11(join21(finalTargetPath, "eslint.config.mjs"), eslintConfig());
7588
8061
  } else {
7589
8062
  const version = readCliVersion();
7590
- writeFileSync9(
7591
- join17(finalTargetPath, "package.json"),
8063
+ writeFileSync11(
8064
+ join21(finalTargetPath, "package.json"),
7592
8065
  packageJsonContent(name, game, version, localPackagesDir)
7593
8066
  );
7594
- writeFileSync9(join17(finalTargetPath, "tsconfig.json"), tsconfigJson());
7595
- writeFileSync9(join17(finalTargetPath, "eslint.config.mjs"), eslintConfig());
8067
+ writeFileSync11(join21(finalTargetPath, "tsconfig.json"), tsconfigJson());
8068
+ writeFileSync11(join21(finalTargetPath, "eslint.config.mjs"), eslintConfig());
8069
+ }
8070
+ if (kind === "library") {
8071
+ writeFileSync11(join21(finalTargetPath, "src", "index.ts"), LIBRARY_ENTRY);
8072
+ writeFileSync11(join21(finalTargetPath, "src", "index.d.ts"), LIBRARY_TYPES);
8073
+ } else {
8074
+ writeFileSync11(join21(finalTargetPath, "src", "plugin.ts"), pluginSource(game));
7596
8075
  }
7597
- writeFileSync9(join17(finalTargetPath, "src", "plugin.ts"), pluginSource(game));
7598
- writeFileSync9(join17(finalTargetPath, ".gitignore"), gitignore());
8076
+ writeFileSync11(join21(finalTargetPath, ".gitignore"), gitignore());
7599
8077
  let installed = false;
7600
8078
  if (workspaceRoot === void 0) {
7601
8079
  if (install !== "none") {
@@ -7613,6 +8091,7 @@ async function createPlugin(opts = {}) {
7613
8091
  dir: finalTargetPath,
7614
8092
  name,
7615
8093
  game,
8094
+ kind,
7616
8095
  installed,
7617
8096
  skippedInstall: install === "none",
7618
8097
  packageManager: install,
@@ -7621,8 +8100,8 @@ async function createPlugin(opts = {}) {
7621
8100
  }
7622
8101
 
7623
8102
  // src/create/workspace.ts
7624
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync10 } from "node:fs";
7625
- import { join as join18, resolve as resolve17 } from "node:path";
8103
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync12 } from "node:fs";
8104
+ import { join as join22, resolve as resolve20 } from "node:path";
7626
8105
  var ROOT_DEFAULT_GAME = "cs2";
7627
8106
  var CHANGESETS_CLI_RANGE = "^2.29.5";
7628
8107
  function defaultWorkspaceName(dir) {
@@ -7675,7 +8154,7 @@ function changesetConfigJson() {
7675
8154
  ) + "\n";
7676
8155
  }
7677
8156
  async function createWorkspace(opts = {}) {
7678
- const targetPath = resolve17(opts.path ?? ".");
8157
+ const targetPath = resolve20(opts.path ?? ".");
7679
8158
  const interactive = isInteractive({ yes: opts.yes });
7680
8159
  let name = opts.name;
7681
8160
  assertEmptyTarget(targetPath);
@@ -7706,14 +8185,14 @@ async function createWorkspace(opts = {}) {
7706
8185
  name = name ?? defaultWorkspaceName(targetPath);
7707
8186
  const version = readCliVersion();
7708
8187
  const localPackagesDir = findLocalPackagesDir();
7709
- mkdirSync6(join18(targetPath, "plugins"), { recursive: true });
7710
- mkdirSync6(join18(targetPath, "packages"), { recursive: true });
7711
- mkdirSync6(join18(targetPath, ".changeset"), { recursive: true });
7712
- writeFileSync10(join18(targetPath, "package.json"), rootPackageJsonContent(name, version, localPackagesDir));
7713
- writeFileSync10(join18(targetPath, "tsconfig.base.json"), tsconfigBaseJson());
7714
- writeFileSync10(join18(targetPath, "eslint.config.mjs"), eslintConfig());
7715
- writeFileSync10(join18(targetPath, ".changeset", "config.json"), changesetConfigJson());
7716
- writeFileSync10(join18(targetPath, ".gitignore"), gitignore());
8188
+ mkdirSync8(join22(targetPath, "plugins"), { recursive: true });
8189
+ mkdirSync8(join22(targetPath, "packages"), { recursive: true });
8190
+ mkdirSync8(join22(targetPath, ".changeset"), { recursive: true });
8191
+ writeFileSync12(join22(targetPath, "package.json"), rootPackageJsonContent(name, version, localPackagesDir));
8192
+ writeFileSync12(join22(targetPath, "tsconfig.base.json"), tsconfigBaseJson());
8193
+ writeFileSync12(join22(targetPath, "eslint.config.mjs"), eslintConfig());
8194
+ writeFileSync12(join22(targetPath, ".changeset", "config.json"), changesetConfigJson());
8195
+ writeFileSync12(join22(targetPath, ".gitignore"), gitignore());
7717
8196
  return { dir: targetPath, name };
7718
8197
  }
7719
8198
 
@@ -7741,7 +8220,7 @@ async function run6(argv2) {
7741
8220
  return;
7742
8221
  }
7743
8222
  const pathArg = positionals(argv2, VALUE_FLAGS2)[0];
7744
- const workspaceRoot = findWorkspaceRoot(pathArg === void 0 ? process.cwd() : resolve18(pathArg));
8223
+ const workspaceRoot = findWorkspaceRoot(pathArg === void 0 ? process.cwd() : resolve21(pathArg));
7745
8224
  const result = await createPlugin({
7746
8225
  path: pathArg,
7747
8226
  name: parseFlag(argv2, "--name"),
@@ -7749,16 +8228,18 @@ async function run6(argv2) {
7749
8228
  template: parseFlag(argv2, "--template"),
7750
8229
  install: parseFlag(argv2, "--install"),
7751
8230
  noInstall: hasFlag(argv2, "--no-install"),
8231
+ kind: hasFlag(argv2, "--library") ? "library" : void 0,
7752
8232
  workspaceRoot: workspaceRoot ?? void 0,
7753
8233
  yes
7754
8234
  });
7755
8235
  const next = result.workspaceRoot ? `next: npm install at the workspace root (${result.workspaceRoot}), then cd ${result.dir} && npm run build` : result.installed ? `dependencies installed (${result.packageManager})` : result.skippedInstall ? `next: cd ${result.dir} && npm run build` : `next: cd ${result.dir} && npm install && npm run build`;
8236
+ const libraryHint = result.kind === "library" ? " (publish with `s2s deploy`; consumers get it with `s2s add`)" : "";
7756
8237
  if (interactive) {
7757
8238
  log2.success(`created ${result.dir}`);
7758
- outro2(next);
8239
+ outro2(next + libraryHint);
7759
8240
  } else {
7760
8241
  console.log(`created ${result.dir}`);
7761
- console.log(next);
8242
+ console.log(next + libraryHint);
7762
8243
  }
7763
8244
  } catch (e) {
7764
8245
  console.error(String(e instanceof Error ? e.message : e));
@@ -7947,8 +8428,8 @@ async function run7(argv2) {
7947
8428
  }
7948
8429
 
7949
8430
  // src/config/gen.ts
7950
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync11, mkdirSync as mkdirSync7 } from "node:fs";
7951
- import { join as join19 } from "node:path";
8431
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, mkdirSync as mkdirSync9 } from "node:fs";
8432
+ import { join as join23 } from "node:path";
7952
8433
 
7953
8434
  // src/config/config-path.ts
7954
8435
  function configFileName(id) {
@@ -7994,13 +8475,13 @@ function genConfigFromManifest(manifest, outDir) {
7994
8475
  }
7995
8476
  const id = typeof manifest.id === "string" ? manifest.id : "";
7996
8477
  if (!id) throw new Error("config gen: manifest declares config but has no id");
7997
- mkdirSync7(outDir, { recursive: true });
7998
- const path = join19(outDir, configFileName(id));
7999
- writeFileSync11(path, generateDefaultJsonc(config));
8478
+ mkdirSync9(outDir, { recursive: true });
8479
+ const path = join23(outDir, configFileName(id));
8480
+ writeFileSync13(path, generateDefaultJsonc(config));
8000
8481
  return path;
8001
8482
  }
8002
8483
  function genConfigForS2sp(s2spPath, outDir) {
8003
- const entry = unzipSync(readFileSync16(s2spPath))["manifest.json"];
8484
+ const entry = unzipSync(readFileSync18(s2spPath))["manifest.json"];
8004
8485
  if (!entry) throw new Error(`config gen: ${s2spPath} has no manifest.json`);
8005
8486
  const manifest = JSON.parse(Buffer.from(entry).toString("utf8"));
8006
8487
  return genConfigFromManifest(manifest, outDir);
@@ -8047,12 +8528,12 @@ async function run8(argv2) {
8047
8528
  }
8048
8529
 
8049
8530
  // src/commands/codegen.ts
8050
- import { dirname as dirname5, join as join23 } from "node:path";
8531
+ import { dirname as dirname6, join as join27 } from "node:path";
8051
8532
  import { fileURLToPath as fileURLToPath3 } from "node:url";
8052
8533
 
8053
8534
  // src/schemagen/gen.ts
8054
- import { readFileSync as readFileSync17, writeFileSync as writeFileSync12 } from "node:fs";
8055
- import { join as join20 } from "node:path";
8535
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "node:fs";
8536
+ import { join as join24 } from "node:path";
8056
8537
 
8057
8538
  // src/schemagen/model.ts
8058
8539
  function enumMemberNames(enumName, members) {
@@ -8066,8 +8547,8 @@ function enumMemberNames(enumName, members) {
8066
8547
  for (const m of members) {
8067
8548
  const up = m.toUpperCase();
8068
8549
  let rest = null;
8069
- for (const sep3 of ["_", ""]) {
8070
- const pfx = stem + sep3;
8550
+ for (const sep4 of ["_", ""]) {
8551
+ const pfx = stem + sep4;
8071
8552
  if (up.startsWith(pfx) && m.length > pfx.length) {
8072
8553
  rest = m.slice(pfx.length);
8073
8554
  break;
@@ -8431,23 +8912,23 @@ function resolveExpr(f) {
8431
8912
  const base = `off(${S(f.declaringClass)},${S(f.rawName)})`;
8432
8913
  return f.addOffset ? `${base} + ${f.addOffset}` : base;
8433
8914
  }
8434
- function accessorEntry(f, resolve19, guard2) {
8915
+ function accessorEntry(f, resolve22, guard2) {
8435
8916
  const g = guard2 === "" ? "" : `${guard2} `;
8436
8917
  const gs = guard2 === "" ? "" : `${guard2.replace("return null;", "return;")} `;
8437
8918
  let entry;
8438
8919
  if (f.accessorKind === "str") {
8439
- entry = `get: function () { ${g}return this.ref.readString(${resolve19}, ${f.strLen}); }`;
8920
+ entry = `get: function () { ${g}return this.ref.readString(${resolve22}, ${f.strLen}); }`;
8440
8921
  } else if (f.accessorKind === "u64" || f.accessorKind === "i64") {
8441
- entry = `get: function () { ${g}var v = this.ref.${READ[f.accessorKind]}(${resolve19}); return v === null ? null : v.toString(); }`;
8922
+ entry = `get: function () { ${g}var v = this.ref.${READ[f.accessorKind]}(${resolve22}); return v === null ? null : v.toString(); }`;
8442
8923
  } else if (VEC_INFO[f.accessorKind]) {
8443
8924
  const vi = VEC_INFO[f.accessorKind];
8444
- entry = `get: function () { ${g}var a = this.ref.readFloats(${resolve19}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
8925
+ entry = `get: function () { ${g}var a = this.ref.readFloats(${resolve22}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
8445
8926
  } else {
8446
- entry = `get: function () { ${g}return this.ref.${READ[f.accessorKind]}(${resolve19}); }`;
8927
+ entry = `get: function () { ${g}return this.ref.${READ[f.accessorKind]}(${resolve22}); }`;
8447
8928
  }
8448
8929
  if (f.writable) {
8449
8930
  const wm = WRITE[f.accessorKind];
8450
- entry += `, set: function (v) { ${gs}var o = ${resolve19}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(o); }`;
8931
+ entry += `, set: function (v) { ${gs}var o = ${resolve22}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(o); }`;
8451
8932
  }
8452
8933
  return entry;
8453
8934
  }
@@ -8476,9 +8957,9 @@ function emitJs(model) {
8476
8957
  out.push(` E[${S(e.className)}] = {`);
8477
8958
  for (const f of e.fields) {
8478
8959
  const sub = `off(${S(f.declaringClass)},${S(f.rawName)})`;
8479
- const resolve19 = f.addOffset ? `this.base + ${sub} + ${f.addOffset}` : `this.base + ${sub}`;
8960
+ const resolve22 = f.addOffset ? `this.base + ${sub} + ${f.addOffset}` : `this.base + ${sub}`;
8480
8961
  const guard2 = `if (this.base < 0 || ${sub} < 0) return null;`;
8481
- out.push(` ${S(f.propName)}: { ${accessorEntry(f, resolve19, guard2)} },`);
8962
+ out.push(` ${S(f.propName)}: { ${accessorEntry(f, resolve22, guard2)} },`);
8482
8963
  }
8483
8964
  for (const f of e.embeddedFields) {
8484
8965
  const sub = `off(${S(f.declaringClass)},${S(f.rawName)})`;
@@ -8535,16 +9016,16 @@ var EMBED_PATH = "games/cs2/codegen-embedded.json";
8535
9016
  var JS_OUT = "games/cs2/js/schema.generated.js";
8536
9017
  var DTS_OUT = "packages/cs2/schema.generated.d.ts";
8537
9018
  function runGenSchema(repoRoot2, opts) {
8538
- const catalog = JSON.parse(readFileSync17(join20(repoRoot2, CATALOG_PATH), "utf8"));
8539
- const list = JSON.parse(readFileSync17(join20(repoRoot2, LIST_PATH), "utf8"));
9019
+ const catalog = JSON.parse(readFileSync19(join24(repoRoot2, CATALOG_PATH), "utf8"));
9020
+ const list = JSON.parse(readFileSync19(join24(repoRoot2, LIST_PATH), "utf8"));
8540
9021
  let embed = [];
8541
9022
  try {
8542
- embed = JSON.parse(readFileSync17(join20(repoRoot2, EMBED_PATH), "utf8"));
9023
+ embed = JSON.parse(readFileSync19(join24(repoRoot2, EMBED_PATH), "utf8"));
8543
9024
  } catch {
8544
9025
  }
8545
9026
  let enumCatalog = {};
8546
9027
  try {
8547
- enumCatalog = JSON.parse(readFileSync17(join20(repoRoot2, ENUMS_PATH), "utf8"));
9028
+ enumCatalog = JSON.parse(readFileSync19(join24(repoRoot2, ENUMS_PATH), "utf8"));
8548
9029
  } catch {
8549
9030
  }
8550
9031
  const model = buildModel(catalog, list, embed, enumCatalog);
@@ -8555,16 +9036,16 @@ function runGenSchema(repoRoot2, opts) {
8555
9036
  const drift = [];
8556
9037
  const files = [[JS_OUT, js], [DTS_OUT, dts]];
8557
9038
  for (const [rel, content] of files) {
8558
- const abs = join20(repoRoot2, rel);
9039
+ const abs = join24(repoRoot2, rel);
8559
9040
  if (opts.check) {
8560
9041
  let cur = "";
8561
9042
  try {
8562
- cur = readFileSync17(abs, "utf8");
9043
+ cur = readFileSync19(abs, "utf8");
8563
9044
  } catch {
8564
9045
  }
8565
9046
  if (cur !== content) drift.push(rel);
8566
9047
  } else {
8567
- writeFileSync12(abs, content);
9048
+ writeFileSync14(abs, content);
8568
9049
  }
8569
9050
  }
8570
9051
  if (model.collisions.length) console.error(`gen-schema: ${model.collisions.length} name collision(s) \u2192 raw fallback:
@@ -8574,8 +9055,8 @@ function runGenSchema(repoRoot2, opts) {
8574
9055
  }
8575
9056
 
8576
9057
  // src/eventgen/gen.ts
8577
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync13 } from "node:fs";
8578
- import { join as join21 } from "node:path";
9058
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync15 } from "node:fs";
9059
+ import { join as join25 } from "node:path";
8579
9060
 
8580
9061
  // src/eventgen/model.ts
8581
9062
  var TYPE_TO_GETTER = {
@@ -8660,27 +9141,27 @@ function emitEventDts(model) {
8660
9141
  var CATALOG_PATH2 = "games/cs2/gamedata/event-catalog.json";
8661
9142
  var DTS_OUT2 = "packages/cs2/events.generated.d.ts";
8662
9143
  function runGenEvents(repoRoot2, opts) {
8663
- const catalog = JSON.parse(readFileSync18(join21(repoRoot2, CATALOG_PATH2), "utf8"));
9144
+ const catalog = JSON.parse(readFileSync20(join25(repoRoot2, CATALOG_PATH2), "utf8"));
8664
9145
  const model = buildEventModel(catalog);
8665
9146
  const dts = emitEventDts(model);
8666
9147
  const drift = [];
8667
- const abs = join21(repoRoot2, DTS_OUT2);
9148
+ const abs = join25(repoRoot2, DTS_OUT2);
8668
9149
  if (opts.check) {
8669
9150
  let cur = "";
8670
9151
  try {
8671
- cur = readFileSync18(abs, "utf8");
9152
+ cur = readFileSync20(abs, "utf8");
8672
9153
  } catch {
8673
9154
  }
8674
9155
  if (cur !== dts) drift.push(DTS_OUT2);
8675
9156
  } else {
8676
- writeFileSync13(abs, dts);
9157
+ writeFileSync15(abs, dts);
8677
9158
  }
8678
9159
  return { events: model.length, drift };
8679
9160
  }
8680
9161
 
8681
9162
  // src/navgen/gen.ts
8682
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "node:fs";
8683
- import { join as join22 } from "node:path";
9163
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync16 } from "node:fs";
9164
+ import { join as join26 } from "node:path";
8684
9165
 
8685
9166
  // src/navgen/model.ts
8686
9167
  var SUPPORTED_NAV_KINDS = /* @__PURE__ */ new Set([
@@ -8782,25 +9263,25 @@ function emitNavJs(model) {
8782
9263
  for (const f of w.fields) {
8783
9264
  const own = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
8784
9265
  const withDelta = f.addOffset ? `${own} + ${f.addOffset}` : own;
8785
- const resolve19 = `${withDelta} + this.base`;
9266
+ const resolve22 = `${withDelta} + this.base`;
8786
9267
  let getter;
8787
9268
  const kind = f.accessorKind;
8788
9269
  if (kind === "u64") {
8789
- getter = `get: function () { var v = this.root.readUInt64Via(this.path, ${resolve19}); return v === null ? null : v.toString(); }`;
9270
+ getter = `get: function () { var v = this.root.readUInt64Via(this.path, ${resolve22}); return v === null ? null : v.toString(); }`;
8790
9271
  } else if (kind === "i64") {
8791
- getter = `get: function () { var v = this.root.readInt64Via(this.path, ${resolve19}); return v === null ? null : v.toString(); }`;
9272
+ getter = `get: function () { var v = this.root.readInt64Via(this.path, ${resolve22}); return v === null ? null : v.toString(); }`;
8792
9273
  } else if (VEC_INFO[kind]) {
8793
9274
  const vi = VEC_INFO[kind];
8794
- getter = `get: function () { var a = this.root.readFloatsChain(this.path, ${resolve19}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
9275
+ getter = `get: function () { var a = this.root.readFloatsChain(this.path, ${resolve22}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
8795
9276
  } else if (kind === "handle") {
8796
- getter = `get: function () { return this.root.readHandleVia(this.path, ${resolve19}); }`;
9277
+ getter = `get: function () { return this.root.readHandleVia(this.path, ${resolve22}); }`;
8797
9278
  } else {
8798
9279
  const method = READ_VIA[kind];
8799
9280
  if (!method) continue;
8800
- getter = `get: function () { return this.root.${method}(this.path, ${resolve19}); }`;
9281
+ getter = `get: function () { return this.root.${method}(this.path, ${resolve22}); }`;
8801
9282
  }
8802
9283
  const wmethod = f.navWritable ? WRITE_VIA[kind] : void 0;
8803
- const setter = wmethod ? `, set: function (v) { this.root.${wmethod}(this.path, ${resolve19}, v); }` : "";
9284
+ const setter = wmethod ? `, set: function (v) { this.root.${wmethod}(this.path, ${resolve22}, v); }` : "";
8804
9285
  out.push(` ${S2(f.propName)}: { ${getter}${setter} },`);
8805
9286
  }
8806
9287
  out.push(" });");
@@ -8901,8 +9382,8 @@ var CATALOG_PATH3 = "games/cs2/gamedata/schema-catalog.json";
8901
9382
  var JS_OUT2 = "games/cs2/js/nav.generated.js";
8902
9383
  var DTS_OUT3 = "packages/cs2/nav.generated.d.ts";
8903
9384
  function runGenNav(repoRoot2, opts) {
8904
- const config = JSON.parse(readFileSync19(join22(repoRoot2, TARGETS_PATH), "utf8"));
8905
- const catalog = JSON.parse(readFileSync19(join22(repoRoot2, CATALOG_PATH3), "utf8"));
9385
+ const config = JSON.parse(readFileSync21(join26(repoRoot2, TARGETS_PATH), "utf8"));
9386
+ const catalog = JSON.parse(readFileSync21(join26(repoRoot2, CATALOG_PATH3), "utf8"));
8906
9387
  const model = buildNavModel(config, catalog);
8907
9388
  const js = emitNavJs(model);
8908
9389
  const dts = emitNavDts(model);
@@ -8910,16 +9391,16 @@ function runGenNav(repoRoot2, opts) {
8910
9391
  const drift = [];
8911
9392
  const files = [[JS_OUT2, js], [DTS_OUT3, dts]];
8912
9393
  for (const [rel, content] of files) {
8913
- const abs = join22(repoRoot2, rel);
9394
+ const abs = join26(repoRoot2, rel);
8914
9395
  if (opts.check) {
8915
9396
  let cur = "";
8916
9397
  try {
8917
- cur = readFileSync19(abs, "utf8");
9398
+ cur = readFileSync21(abs, "utf8");
8918
9399
  } catch {
8919
9400
  }
8920
9401
  if (cur !== content) drift.push(rel);
8921
9402
  } else {
8922
- writeFileSync14(abs, content);
9403
+ writeFileSync16(abs, content);
8923
9404
  }
8924
9405
  }
8925
9406
  return { wrappers: model.wrappers.length, fields, drift };
@@ -8927,7 +9408,7 @@ function runGenNav(repoRoot2, opts) {
8927
9408
 
8928
9409
  // src/commands/codegen.ts
8929
9410
  function repoRoot() {
8930
- return join23(dirname5(fileURLToPath3(import.meta.url)), "..", "..", "..");
9411
+ return join27(dirname6(fileURLToPath3(import.meta.url)), "..", "..", "..");
8931
9412
  }
8932
9413
  async function run9(kind, argv2) {
8933
9414
  const root = repoRoot();
@@ -9006,7 +9487,7 @@ var argv = process.argv.slice(2);
9006
9487
  var command = argv[0];
9007
9488
  function usage() {
9008
9489
  console.error(
9009
- "Usage:\n s2s create [path] [--game cs2|none] [--name <pkg>] [--template minimal]\n [--install npm|pnpm|yarn|bun|none] [--no-install] [-y]\n s2s create --workspace <dir>\n s2s build [dir|workspace root] [--filter <pattern>]... [--stamp-version <v>]\n [--packages-dir <path>]\n s2s login [--token s2s_\u2026] [--registry <url>]\n s2s deploy [dir|workspace root] [--filter <pattern>]... [--dry-run] [--yes|--ci]\n [--registry <url>] [--packages-dir <path>]\n s2s version [dir] [--since <ref>]\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)"
9490
+ "Usage:\n s2s create [path] [--game cs2|none] [--name <pkg>] [--template minimal] [--library]\n [--install npm|pnpm|yarn|bun|none] [--no-install] [-y]\n s2s create --workspace <dir>\n s2s build [dir|workspace root] [--filter <pattern>]... [--stamp-version <v>]\n [--packages-dir <path>]\n s2s login [--token s2s_\u2026] [--registry <url>]\n s2s deploy [dir|workspace root] [--filter <pattern>]... [--dry-run] [--yes|--ci]\n [--registry <url>] [--packages-dir <path>]\n s2s version [dir] [--since <ref>]\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)"
9010
9491
  );
9011
9492
  }
9012
9493
  if (!command) {