@s2script/sdk 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4,12 +4,12 @@
4
4
  import * as esbuild from "esbuild";
5
5
  import AdmZip from "adm-zip";
6
6
  import { readFileSync as readFileSync4, mkdirSync } from "node:fs";
7
- import { resolve as resolve4, join as join4 } from "node:path";
7
+ import { resolve as resolve5, join as join6 } from "node:path";
8
8
 
9
9
  // src/typecheck/typecheck.ts
10
10
  import ts from "typescript";
11
- import { existsSync as existsSync2, readdirSync, readFileSync, writeFileSync, rmSync, mkdtempSync } from "node:fs";
12
- import { join as join2, resolve as resolve2 } from "node:path";
11
+ import { existsSync as existsSync3, readdirSync, readFileSync, writeFileSync, rmSync, mkdtempSync } from "node:fs";
12
+ import { join as join3, resolve as resolve2 } from "node:path";
13
13
  import { tmpdir } from "node:os";
14
14
 
15
15
  // src/packages-resolve.ts
@@ -55,11 +55,47 @@ function resolvePackagesDir(opts) {
55
55
  );
56
56
  }
57
57
 
58
+ // src/tsconfig-shared.ts
59
+ var sharedCompilerOptionsJson = {
60
+ strict: true,
61
+ noEmit: true,
62
+ moduleResolution: "bundler",
63
+ module: "ESNext",
64
+ target: "ES2020",
65
+ lib: ["ES2020"],
66
+ types: [],
67
+ skipLibCheck: true,
68
+ allowImportingTsExtensions: true
69
+ };
70
+ function sharedProgramOptions(tsMod) {
71
+ return {
72
+ strict: sharedCompilerOptionsJson.strict,
73
+ noEmit: sharedCompilerOptionsJson.noEmit,
74
+ allowImportingTsExtensions: sharedCompilerOptionsJson.allowImportingTsExtensions,
75
+ moduleResolution: tsMod.ModuleResolutionKind.Bundler,
76
+ module: tsMod.ModuleKind.ESNext,
77
+ target: tsMod.ScriptTarget.ES2020,
78
+ lib: sharedCompilerOptionsJson.lib.map((l) => `lib.${l.toLowerCase()}.d.ts`),
79
+ types: [...sharedCompilerOptionsJson.types],
80
+ skipLibCheck: sharedCompilerOptionsJson.skipLibCheck
81
+ };
82
+ }
83
+
84
+ // src/contracts.ts
85
+ import { existsSync as existsSync2 } from "node:fs";
86
+ import { join as join2 } from "node:path";
87
+ function localContractPath(pluginDir, dep) {
88
+ const segs = dep.split("/");
89
+ if (segs.some((s) => s === "" || s === "." || s === "..")) return null;
90
+ const p = join2(pluginDir, ".s2script", "types", ...segs, "index.d.ts");
91
+ return existsSync2(p) ? p : null;
92
+ }
93
+
58
94
  // src/typecheck/typecheck.ts
59
95
  function localDeclarationFiles(pluginDir) {
60
- const srcDir = join2(pluginDir, "src");
61
- if (!existsSync2(srcDir)) return [];
62
- return readdirSync(srcDir).filter((f) => f.endsWith(".d.ts")).map((f) => join2(srcDir, f));
96
+ const srcDir = join3(pluginDir, "src");
97
+ if (!existsSync3(srcDir)) return [];
98
+ return readdirSync(srcDir).filter((f) => f.endsWith(".d.ts")).map((f) => join3(srcDir, f));
63
99
  }
64
100
  function declaredModules(dtsFiles) {
65
101
  const out = /* @__PURE__ */ new Set();
@@ -72,31 +108,33 @@ function declaredModules(dtsFiles) {
72
108
  function typecheckPlugin(pluginDir, opts) {
73
109
  const absDir = resolve2(pluginDir);
74
110
  const packagesDir = opts?.packagesDir ? resolve2(opts.packagesDir) : resolvePackagesDir({ pluginDir: absDir });
75
- const pkg = JSON.parse(readFileSync(join2(absDir, "package.json"), "utf8"));
111
+ const pkg = JSON.parse(readFileSync(join3(absDir, "package.json"), "utf8"));
76
112
  const s2 = pkg.s2script ?? {};
77
113
  const entryRel = s2.main ?? pkg.main;
78
- if (!entryRel) throw new Error(`typecheckPlugin: no entry point in ${join2(absDir, "package.json")}`);
114
+ if (!entryRel) throw new Error(`typecheckPlugin: no entry point in ${join3(absDir, "package.json")}`);
79
115
  const entry = resolve2(absDir, entryRel);
80
116
  const isAlwaysResolved = (d) => d.startsWith("@s2script/sdk/") || d === "@s2script/cs2" || d.startsWith("@s2script/cs2/");
81
117
  const localDts = localDeclarationFiles(absDir);
82
118
  const locallyDeclared = declaredModules(localDts);
119
+ const allDeclaredDeps = [
120
+ ...Object.keys(s2.pluginDependencies ?? {}),
121
+ ...Object.keys(s2.optionalPluginDependencies ?? {})
122
+ ];
123
+ const contractPaths = {};
124
+ for (const d of allDeclaredDeps) {
125
+ const p = localContractPath(absDir, d);
126
+ if (p !== null) contractPaths[d] = [p];
127
+ }
83
128
  const deps = [
84
129
  ...Object.keys(s2.pluginDependencies ?? {}),
85
130
  ...Object.keys(s2.optionalPluginDependencies ?? {})
86
131
  // Never stub a module the plugin declares itself — a shorthand `declare module "X";` and a
87
132
  // full `declare module "X" { … }` for the same X collide.
88
- ].filter((d) => !isAlwaysResolved(d) && !locallyDeclared.has(d));
133
+ ].filter((d) => !isAlwaysResolved(d) && !locallyDeclared.has(d) && contractPaths[d] === void 0);
89
134
  const options = {
90
- strict: true,
91
- noEmit: true,
92
135
  // Accept explicit `.ts` import extensions (node type-stripping requires them for source-to-source
93
136
  // imports; esbuild strips them at bundle time). Backward-compatible — extensionless imports still resolve.
94
- allowImportingTsExtensions: true,
95
- moduleResolution: ts.ModuleResolutionKind.Bundler,
96
- module: ts.ModuleKind.ESNext,
97
- target: ts.ScriptTarget.ES2020,
98
- lib: ["lib.es2020.d.ts"],
99
- types: [],
137
+ ...sharedProgramOptions(ts),
100
138
  baseUrl: packagesDir,
101
139
  paths: {
102
140
  // Builtins are `@s2script/sdk/<cap>` → packages/sdk/<cap>.d.ts. The `@s2script/*` fallback
@@ -104,15 +142,15 @@ function typecheckPlugin(pluginDir, opts) {
104
142
  // dirs are deleted). tsc picks the longest matching prefix, so `@s2script/sdk/*` wins for
105
143
  // sdk imports.
106
144
  "@s2script/sdk/*": ["sdk/*.d.ts"],
107
- "@s2script/*": ["*/index.d.ts"]
108
- },
109
- skipLibCheck: true
145
+ "@s2script/*": ["*/index.d.ts"],
146
+ ...contractPaths
147
+ }
110
148
  };
111
- const rootNames = [entry, join2(packagesDir, "sdk", "globals.d.ts"), ...localDts];
112
- const tmp = mkdtempSync(join2(tmpdir(), "s2tc-"));
149
+ const rootNames = [entry, join3(packagesDir, "sdk", "globals.d.ts"), ...localDts];
150
+ const tmp = mkdtempSync(join3(tmpdir(), "s2tc-"));
113
151
  try {
114
152
  if (deps.length) {
115
- const stub = join2(tmp, "ambient.d.ts");
153
+ const stub = join3(tmp, "ambient.d.ts");
116
154
  writeFileSync(stub, deps.map((d) => `declare module ${JSON.stringify(d)};`).join("\n") + "\n");
117
155
  rootNames.push(stub);
118
156
  }
@@ -132,7 +170,7 @@ function typecheckPlugin(pluginDir, opts) {
132
170
  }
133
171
  return { file, line, col, code: d.code, message: ts.flattenDiagnosticMessageText(d.messageText, "\n") };
134
172
  });
135
- return { ok: out.length === 0, diagnostics: out };
173
+ return { ok: out.length === 0, diagnostics: out, program };
136
174
  } finally {
137
175
  rmSync(tmp, { recursive: true, force: true });
138
176
  }
@@ -239,8 +277,8 @@ function validateDecl(full, decl, typeStr, errs) {
239
277
  }
240
278
 
241
279
  // src/publish-gate.ts
242
- import { existsSync as existsSync3, readFileSync as readFileSync2, statSync } from "node:fs";
243
- import { join as join3, resolve as resolve3 } from "node:path";
280
+ import { existsSync as existsSync4, readFileSync as readFileSync2, statSync } from "node:fs";
281
+ import { join as join4, resolve as resolve3 } from "node:path";
244
282
  function hasPublishes(publishes) {
245
283
  if (publishes == null) return false;
246
284
  if (typeof publishes === "string") return publishes.trim().length > 0;
@@ -266,7 +304,7 @@ function assertPublishesTypes(pkg, pluginDir) {
266
304
  };
267
305
  }
268
306
  const typesPath = resolve3(pluginDir, typesRel);
269
- if (!existsSync3(typesPath)) {
307
+ if (!existsSync4(typesPath)) {
270
308
  return { ok: false, error: `types file not found: ${typesRel}` };
271
309
  }
272
310
  const st = statSync(typesPath);
@@ -341,32 +379,160 @@ function derivePublishes(authored, pkgName, pkgVersion, typesPath) {
341
379
  return out;
342
380
  }
343
381
 
382
+ // src/api-version.ts
383
+ var HOST_API_VERSION_MAJOR = 2;
384
+ var STAMPED_API_VERSION = `${HOST_API_VERSION_MAJOR}.x`;
385
+
386
+ // src/publish-scan.ts
387
+ import ts2 from "typescript";
388
+ function isPluginContext(type) {
389
+ const sym = type.getSymbol() ?? type.aliasSymbol;
390
+ return sym?.getName() === "PluginContext";
391
+ }
392
+ function scanPluginProgram(program, pluginDir) {
393
+ const checker = program.getTypeChecker();
394
+ const out = { publishNames: [], dynamicPublishSites: [], useNames: [] };
395
+ const dirPrefix = pluginDir.replace(/\\/g, "/").replace(/\/+$/, "") + "/";
396
+ for (const sf of program.getSourceFiles()) {
397
+ if (sf.isDeclarationFile) continue;
398
+ if (!sf.fileName.replace(/\\/g, "/").startsWith(dirPrefix)) continue;
399
+ const visit = (node) => {
400
+ if (ts2.isCallExpression(node) && ts2.isPropertyAccessExpression(node.expression)) {
401
+ const method = node.expression.name.text;
402
+ if (method === "publish" || method === "use" || method === "tryUse") {
403
+ const recv = checker.getTypeAtLocation(node.expression.expression);
404
+ if (isPluginContext(recv)) {
405
+ const arg0 = node.arguments[0];
406
+ if (method === "publish") {
407
+ if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
408
+ out.publishNames.push(arg0.text);
409
+ } else {
410
+ const { line } = sf.getLineAndCharacterOfPosition(node.getStart());
411
+ out.dynamicPublishSites.push(`${sf.fileName}:${line + 1}`);
412
+ }
413
+ } else if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
414
+ out.useNames.push(arg0.text);
415
+ }
416
+ }
417
+ }
418
+ }
419
+ ts2.forEachChild(node, visit);
420
+ };
421
+ visit(sf);
422
+ }
423
+ out.publishNames = [...new Set(out.publishNames)];
424
+ out.useNames = [...new Set(out.useNames)];
425
+ return out;
426
+ }
427
+
428
+ // src/lint/lint.ts
429
+ import { ESLint } from "eslint";
430
+ import { existsSync as existsSync5 } from "node:fs";
431
+ import { join as join5, resolve as resolve4 } from "node:path";
432
+ import s2lint from "@s2script/eslint-plugin";
433
+ var CONFIG_FILES = ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts"];
434
+ async function lintPlugin(pluginDir, program) {
435
+ const absDir = resolve4(pluginDir);
436
+ const hasOwnConfig = CONFIG_FILES.some((f) => existsSync5(join5(absDir, f)));
437
+ const eslint = hasOwnConfig ? new ESLint({ cwd: absDir, errorOnUnmatchedPattern: false }) : new ESLint({
438
+ cwd: absDir,
439
+ overrideConfigFile: true,
440
+ overrideConfig: s2lint.configs.build([program]),
441
+ errorOnUnmatchedPattern: false
442
+ });
443
+ const dirPrefix = absDir.replace(/\\/g, "/").replace(/\/+$/, "") + "/";
444
+ const targets = hasOwnConfig ? ["**/*.ts"] : program.getSourceFiles().filter((sf) => !sf.isDeclarationFile && sf.fileName.replace(/\\/g, "/").startsWith(dirPrefix)).map((sf) => sf.fileName);
445
+ if (targets.length === 0) return { ok: true, output: "", errorCount: 0 };
446
+ const results = await eslint.lintFiles(targets);
447
+ const errorCount = results.reduce((n, r) => n + r.errorCount, 0);
448
+ const formatter = await eslint.loadFormatter("stylish");
449
+ const output2 = String(await formatter.format(results));
450
+ return { ok: errorCount === 0, output: output2, errorCount };
451
+ }
452
+
344
453
  // src/build.ts
345
454
  async function buildPlugin(dir, packagesDir) {
346
- const absDir = resolve4(dir);
347
- const pkgPath = join4(absDir, "package.json");
455
+ const absDir = resolve5(dir);
456
+ const pkgPath = join6(absDir, "package.json");
348
457
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
349
- const gate = assertPublishesTypes(pkg, absDir);
350
- if (!gate.ok) {
351
- throw new Error(`publish gate failed: ${gate.error}`);
352
- }
353
458
  const s2 = pkg.s2script ?? {};
354
- const derivedPublishes = derivePublishes(s2.publishes, pkg.name, pkg.version, gate.typesPath);
459
+ const config = s2.config ?? void 0;
460
+ if (config !== void 0) {
461
+ const cfgErrs = validateConfigBlock(config);
462
+ if (cfgErrs.length) throw new Error(`invalid s2script.config:
463
+ ${cfgErrs.join("\n ")}`);
464
+ }
355
465
  const tc = typecheckPlugin(absDir, packagesDir !== void 0 ? { packagesDir } : void 0);
356
466
  if (!tc.ok) {
357
467
  throw new Error(`typecheck failed (${tc.diagnostics.length} error(s)):
358
468
  ${formatDiagnostics(tc.diagnostics)}`);
359
469
  }
360
- const { name, version } = pkg;
361
- const apiVersion = s2.apiVersion ?? "";
470
+ const scan = scanPluginProgram(tc.program, absDir);
471
+ const lint = await lintPlugin(absDir, tc.program);
472
+ if (!lint.ok) {
473
+ throw new Error(`lint failed (${lint.errorCount} error(s)):
474
+ ${lint.output}`);
475
+ }
476
+ if (lint.output.trim().length > 0) console.warn(lint.output);
477
+ if (scan.dynamicPublishSites.length > 0) {
478
+ throw new Error(
479
+ `ctx.publish name must be a string literal (the manifest publishes block is derived from code):
480
+ ` + scan.dynamicPublishSites.join("\n ")
481
+ );
482
+ }
483
+ let effectivePublishes = s2.publishes;
484
+ if (!hasPublishes(effectivePublishes)) {
485
+ if (scan.publishNames.length === 1 && scan.publishNames[0] === pkg.name) {
486
+ effectivePublishes = "self";
487
+ } else if (scan.publishNames.length > 0) {
488
+ throw new Error(
489
+ `code publishes ${JSON.stringify(scan.publishNames)} but s2script.publishes is missing \u2014 a contract named differently from the package needs an authored entry with a concrete version`
490
+ );
491
+ }
492
+ } else {
493
+ const authoredNames = Object.keys(expandPublishes(effectivePublishes, pkg.name, pkg.version)).sort();
494
+ const codeNames = [...scan.publishNames].sort();
495
+ if (JSON.stringify(authoredNames) !== JSON.stringify(codeNames)) {
496
+ throw new Error(
497
+ `publishes drift: package.json declares ${JSON.stringify(authoredNames)} but the code's ctx.publish calls are ${JSON.stringify(codeNames)} \u2014 fix whichever is wrong (the manifest is generated from code; the loader re-verifies at Active)`
498
+ );
499
+ }
500
+ }
501
+ const gate = assertPublishesTypes({ ...pkg, s2script: { ...s2, publishes: effectivePublishes } }, absDir);
502
+ if (!gate.ok) {
503
+ throw new Error(`publish gate failed: ${gate.error}`);
504
+ }
505
+ const derivedPublishes = derivePublishes(
506
+ effectivePublishes,
507
+ pkg.name,
508
+ pkg.version,
509
+ gate.typesPath
510
+ );
362
511
  const pluginDependencies = s2.pluginDependencies ?? {};
363
512
  const optionalPluginDependencies = s2.optionalPluginDependencies ?? {};
364
- const config = s2.config ?? void 0;
365
- if (config !== void 0) {
366
- const cfgErrs = validateConfigBlock(config);
367
- if (cfgErrs.length) throw new Error(`invalid s2script.config:
368
- ${cfgErrs.join("\n ")}`);
513
+ const declaredDeps = /* @__PURE__ */ new Set([
514
+ ...Object.keys(pluginDependencies),
515
+ ...Object.keys(optionalPluginDependencies)
516
+ ]);
517
+ for (const used of scan.useNames) {
518
+ if (!declaredDeps.has(used)) {
519
+ console.warn(
520
+ `WARN: ctx.use/tryUse(${JSON.stringify(used)}) is not declared under s2script.pluginDependencies/optionalPluginDependencies \u2014 it will throw at runtime`
521
+ );
522
+ }
523
+ }
524
+ for (const dep of declaredDeps) {
525
+ if (!scan.useNames.includes(dep)) {
526
+ console.warn(`WARN: dependency ${JSON.stringify(dep)} is declared but never ctx.use()d`);
527
+ }
369
528
  }
529
+ const { name, version } = pkg;
530
+ if (s2.apiVersion !== void 0) {
531
+ console.warn(
532
+ `WARN: ${pkgPath}: s2script.apiVersion is ignored \u2014 s2s build derives apiVersion from the SDK (stamping ${JSON.stringify(STAMPED_API_VERSION)}). Remove the field.`
533
+ );
534
+ }
535
+ const apiVersion = STAMPED_API_VERSION;
370
536
  const external = Array.from(/* @__PURE__ */ new Set([
371
537
  "@s2script/*",
372
538
  ...Object.keys(pluginDependencies),
@@ -378,7 +544,7 @@ ${formatDiagnostics(tc.diagnostics)}`);
378
544
  `buildPlugin: no entry point found in ${pkgPath} (set s2script.main or main)`
379
545
  );
380
546
  }
381
- const entryPoint = join4(absDir, entryRelative);
547
+ const entryPoint = join6(absDir, entryRelative);
382
548
  const result = await esbuild.build({
383
549
  entryPoints: [entryPoint],
384
550
  bundle: true,
@@ -401,6 +567,15 @@ ${formatDiagnostics(tc.diagnostics)}`);
401
567
  manifest.publishes = derivedPublishes;
402
568
  }
403
569
  if (config !== void 0) manifest.config = config;
570
+ const compiledAgainst = {};
571
+ for (const dep of [
572
+ ...Object.keys(pluginDependencies),
573
+ ...Object.keys(optionalPluginDependencies)
574
+ ]) {
575
+ const contractPath = localContractPath(absDir, dep);
576
+ if (contractPath !== null) compiledAgainst[dep] = hashContract(contractPath);
577
+ }
578
+ if (Object.keys(compiledAgainst).length > 0) manifest.compiledAgainst = compiledAgainst;
404
579
  const zip = new AdmZip();
405
580
  zip.addFile("manifest.json", Buffer.from(JSON.stringify(manifest, null, 2)));
406
581
  zip.addFile("plugin.js", Buffer.from(pluginJs));
@@ -412,16 +587,16 @@ ${formatDiagnostics(tc.diagnostics)}`);
412
587
  }
413
588
  }
414
589
  const sanitizedId = name.replace(/[^a-zA-Z0-9._-]/g, "_");
415
- const outDir = join4(absDir, "dist");
590
+ const outDir = join6(absDir, "dist");
416
591
  mkdirSync(outDir, { recursive: true });
417
- const outPath = join4(outDir, `${sanitizedId}.s2sp`);
592
+ const outPath = join6(outDir, `${sanitizedId}.s2sp`);
418
593
  zip.writeZip(outPath);
419
594
  return outPath;
420
595
  }
421
596
 
422
597
  // src/schemagen/gen.ts
423
598
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
424
- import { join as join5 } from "node:path";
599
+ import { join as join7 } from "node:path";
425
600
 
426
601
  // src/schemagen/model.ts
427
602
  var READ = {
@@ -613,21 +788,21 @@ function emitJs(model) {
613
788
  for (const c of model.classes) {
614
789
  out.push(` A[${S(c.className)}] = {`);
615
790
  for (const f of flattenedFields(model, c.className)) {
616
- const resolve7 = `off(${S(f.declaringClass)},${S(f.rawName)})`;
791
+ const resolve8 = `off(${S(f.declaringClass)},${S(f.rawName)})`;
617
792
  let entry;
618
793
  if (f.accessorKind === "str") {
619
- entry = `get: function () { return this.ref.readString(${resolve7}, ${f.strLen}); }`;
794
+ entry = `get: function () { return this.ref.readString(${resolve8}, ${f.strLen}); }`;
620
795
  } else if (f.accessorKind === "u64" || f.accessorKind === "i64") {
621
- entry = `get: function () { var v = this.ref.${READ[f.accessorKind]}(${resolve7}); return v === null ? null : v.toString(); }`;
796
+ entry = `get: function () { var v = this.ref.${READ[f.accessorKind]}(${resolve8}); return v === null ? null : v.toString(); }`;
622
797
  } else if (VEC_INFO[f.accessorKind]) {
623
798
  const vi = VEC_INFO[f.accessorKind];
624
- entry = `get: function () { var a = this.ref.readFloats(${resolve7}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
799
+ entry = `get: function () { var a = this.ref.readFloats(${resolve8}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
625
800
  } else {
626
- entry = `get: function () { return this.ref.${READ[f.accessorKind]}(${resolve7}); }`;
801
+ entry = `get: function () { return this.ref.${READ[f.accessorKind]}(${resolve8}); }`;
627
802
  }
628
803
  if (f.writable) {
629
804
  const wm = WRITE[f.accessorKind];
630
- entry += `, set: function (v) { var o = ${resolve7}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(o); }`;
805
+ entry += `, set: function (v) { var o = ${resolve8}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(o); }`;
631
806
  }
632
807
  out.push(` ${S(f.propName)}: { ${entry} },`);
633
808
  }
@@ -653,8 +828,8 @@ var LIST_PATH = "games/cs2/codegen-classes.json";
653
828
  var JS_OUT = "games/cs2/js/schema.generated.js";
654
829
  var DTS_OUT = "packages/cs2/schema.generated.d.ts";
655
830
  function runGenSchema(repoRoot, opts) {
656
- const catalog = JSON.parse(readFileSync5(join5(repoRoot, CATALOG_PATH), "utf8"));
657
- const list = JSON.parse(readFileSync5(join5(repoRoot, LIST_PATH), "utf8"));
831
+ const catalog = JSON.parse(readFileSync5(join7(repoRoot, CATALOG_PATH), "utf8"));
832
+ const list = JSON.parse(readFileSync5(join7(repoRoot, LIST_PATH), "utf8"));
658
833
  const model = buildModel(catalog, list);
659
834
  const dts = emitDts(model);
660
835
  const js = emitJs(model);
@@ -663,7 +838,7 @@ function runGenSchema(repoRoot, opts) {
663
838
  const drift = [];
664
839
  const files = [[JS_OUT, js], [DTS_OUT, dts]];
665
840
  for (const [rel, content] of files) {
666
- const abs = join5(repoRoot, rel);
841
+ const abs = join7(repoRoot, rel);
667
842
  if (opts.check) {
668
843
  let cur = "";
669
844
  try {
@@ -683,7 +858,7 @@ function runGenSchema(repoRoot, opts) {
683
858
 
684
859
  // src/eventgen/gen.ts
685
860
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
686
- import { join as join6 } from "node:path";
861
+ import { join as join8 } from "node:path";
687
862
 
688
863
  // src/eventgen/model.ts
689
864
  var TYPE_TO_GETTER = {
@@ -768,11 +943,11 @@ function emitEventDts(model) {
768
943
  var CATALOG_PATH2 = "games/cs2/gamedata/event-catalog.json";
769
944
  var DTS_OUT2 = "packages/cs2/events.generated.d.ts";
770
945
  function runGenEvents(repoRoot, opts) {
771
- const catalog = JSON.parse(readFileSync6(join6(repoRoot, CATALOG_PATH2), "utf8"));
946
+ const catalog = JSON.parse(readFileSync6(join8(repoRoot, CATALOG_PATH2), "utf8"));
772
947
  const model = buildEventModel(catalog);
773
948
  const dts = emitEventDts(model);
774
949
  const drift = [];
775
- const abs = join6(repoRoot, DTS_OUT2);
950
+ const abs = join8(repoRoot, DTS_OUT2);
776
951
  if (opts.check) {
777
952
  let cur = "";
778
953
  try {
@@ -788,7 +963,7 @@ function runGenEvents(repoRoot, opts) {
788
963
 
789
964
  // src/navgen/gen.ts
790
965
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
791
- import { join as join7 } from "node:path";
966
+ import { join as join9 } from "node:path";
792
967
 
793
968
  // src/navgen/model.ts
794
969
  var SUPPORTED_NAV_KINDS = /* @__PURE__ */ new Set([
@@ -863,22 +1038,22 @@ function emitNavJs(model) {
863
1038
  out.push(` function ${w.wrapper}(root, path) { this.root = root; this.path = path; }`);
864
1039
  out.push(` Object.defineProperties(${w.wrapper}.prototype, {`);
865
1040
  for (const f of w.fields) {
866
- const resolve7 = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
1041
+ const resolve8 = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
867
1042
  let getter;
868
1043
  const kind = f.accessorKind;
869
1044
  if (kind === "u64") {
870
- getter = `get: function () { var v = this.root.readUInt64Via(this.path, ${resolve7}); return v === null ? null : v.toString(); }`;
1045
+ getter = `get: function () { var v = this.root.readUInt64Via(this.path, ${resolve8}); return v === null ? null : v.toString(); }`;
871
1046
  } else if (kind === "i64") {
872
- getter = `get: function () { var v = this.root.readInt64Via(this.path, ${resolve7}); return v === null ? null : v.toString(); }`;
1047
+ getter = `get: function () { var v = this.root.readInt64Via(this.path, ${resolve8}); return v === null ? null : v.toString(); }`;
873
1048
  } else if (VEC_INFO[kind]) {
874
1049
  const vi = VEC_INFO[kind];
875
- getter = `get: function () { var a = this.root.readFloatsChain(this.path, ${resolve7}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
1050
+ getter = `get: function () { var a = this.root.readFloatsChain(this.path, ${resolve8}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
876
1051
  } else if (kind === "handle") {
877
- getter = `get: function () { return this.root.readHandleVia(this.path, ${resolve7}); }`;
1052
+ getter = `get: function () { return this.root.readHandleVia(this.path, ${resolve8}); }`;
878
1053
  } else {
879
1054
  const method = READ_VIA[kind];
880
1055
  if (!method) continue;
881
- getter = `get: function () { return this.root.${method}(this.path, ${resolve7}); }`;
1056
+ getter = `get: function () { return this.root.${method}(this.path, ${resolve8}); }`;
882
1057
  }
883
1058
  out.push(` ${S2(f.propName)}: { ${getter} },`);
884
1059
  }
@@ -950,8 +1125,8 @@ var CATALOG_PATH3 = "games/cs2/gamedata/schema-catalog.json";
950
1125
  var JS_OUT2 = "games/cs2/js/nav.generated.js";
951
1126
  var DTS_OUT3 = "packages/cs2/nav.generated.d.ts";
952
1127
  function runGenNav(repoRoot, opts) {
953
- const config = JSON.parse(readFileSync7(join7(repoRoot, TARGETS_PATH), "utf8"));
954
- const catalog = JSON.parse(readFileSync7(join7(repoRoot, CATALOG_PATH3), "utf8"));
1128
+ const config = JSON.parse(readFileSync7(join9(repoRoot, TARGETS_PATH), "utf8"));
1129
+ const catalog = JSON.parse(readFileSync7(join9(repoRoot, CATALOG_PATH3), "utf8"));
955
1130
  const model = buildNavModel(config, catalog);
956
1131
  const js = emitNavJs(model);
957
1132
  const dts = emitNavDts(model);
@@ -959,7 +1134,7 @@ function runGenNav(repoRoot, opts) {
959
1134
  const drift = [];
960
1135
  const files = [[JS_OUT2, js], [DTS_OUT3, dts]];
961
1136
  for (const [rel, content] of files) {
962
- const abs = join7(repoRoot, rel);
1137
+ const abs = join9(repoRoot, rel);
963
1138
  if (opts.check) {
964
1139
  let cur = "";
965
1140
  try {
@@ -978,11 +1153,12 @@ function runGenNav(repoRoot, opts) {
978
1153
  import {
979
1154
  mkdirSync as mkdirSync2,
980
1155
  writeFileSync as writeFileSync5,
981
- existsSync as existsSync4,
1156
+ existsSync as existsSync6,
982
1157
  readdirSync as readdirSync2,
983
- readFileSync as readFileSync8
1158
+ readFileSync as readFileSync8,
1159
+ symlinkSync
984
1160
  } from "node:fs";
985
- import { dirname as dirname2, join as join8, resolve as resolve5, basename } from "node:path";
1161
+ import { dirname as dirname2, join as join10, resolve as resolve6, basename } from "node:path";
986
1162
  import { createInterface } from "node:readline/promises";
987
1163
  import { stdin as input, stdout as output } from "node:process";
988
1164
  import { spawnSync } from "node:child_process";
@@ -990,13 +1166,13 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
990
1166
  function readCliVersion() {
991
1167
  const here = dirname2(fileURLToPath2(import.meta.url));
992
1168
  const candidates = [
993
- join8(here, "..", "package.json"),
1169
+ join10(here, "..", "package.json"),
994
1170
  // dist/cli.js → packages/sdk/package.json
995
- join8(here, "..", "..", "package.json")
1171
+ join10(here, "..", "..", "package.json")
996
1172
  // src/create → packages/sdk/package.json
997
1173
  ];
998
1174
  for (const p of candidates) {
999
- if (!existsSync4(p)) continue;
1175
+ if (!existsSync6(p)) continue;
1000
1176
  try {
1001
1177
  const pkg = JSON.parse(readFileSync8(p, "utf8"));
1002
1178
  if (pkg.name === "@s2script/sdk" && pkg.version) return pkg.version;
@@ -1008,18 +1184,18 @@ function readCliVersion() {
1008
1184
  function findLocalPackagesDir() {
1009
1185
  const here = dirname2(fileURLToPath2(import.meta.url));
1010
1186
  const candidates = [
1011
- join8(here, "..", ".."),
1187
+ join10(here, "..", ".."),
1012
1188
  // dist/cli.js → packages
1013
- join8(here, "..", "..", "..")
1189
+ join10(here, "..", "..", "..")
1014
1190
  // src/create → packages
1015
1191
  ];
1016
1192
  for (const c of candidates) {
1017
- if (isPackagesDir(c)) return resolve5(c);
1193
+ if (isPackagesDir(c)) return resolve6(c);
1018
1194
  }
1019
1195
  return void 0;
1020
1196
  }
1021
1197
  function defaultNameFromPath(dir) {
1022
- const base = basename(resolve5(dir));
1198
+ const base = basename(resolve6(dir));
1023
1199
  const slug = base.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "plugin";
1024
1200
  return `@plugin/${slug}`;
1025
1201
  }
@@ -1056,9 +1232,9 @@ function assertInstall(v) {
1056
1232
  }
1057
1233
  function createPackageNames(game) {
1058
1234
  if (game === "cs2") {
1059
- return ["sdk", "cs2"];
1235
+ return ["sdk", "cs2", "eslint-plugin"];
1060
1236
  }
1061
- return ["sdk"];
1237
+ return ["sdk", "eslint-plugin"];
1062
1238
  }
1063
1239
  function resolvePublishedVersion(pkg) {
1064
1240
  const r = spawnSync("npm", ["view", pkg, "version"], { encoding: "utf8", timeout: 5e3 });
@@ -1068,68 +1244,62 @@ function versionSpecFrom(status, stdout) {
1068
1244
  const v = (stdout ?? "").trim();
1069
1245
  return status === 0 && /^\d+\.\d+\.\d+/.test(v) ? `^${v}` : "latest";
1070
1246
  }
1071
- function registryDevDeps(game, sdkVersion, resolve7 = resolvePublishedVersion) {
1247
+ function registryDevDeps(game, sdkVersion, resolve8 = resolvePublishedVersion) {
1072
1248
  const deps = {};
1073
1249
  for (const n of createPackageNames(game)) {
1074
- deps[`@s2script/${n}`] = n === "sdk" ? `^${sdkVersion}` : resolve7(`@s2script/${n}`);
1250
+ deps[`@s2script/${n}`] = n === "sdk" ? `^${sdkVersion}` : resolve8(`@s2script/${n}`);
1075
1251
  }
1076
1252
  return deps;
1077
1253
  }
1078
1254
  function fileDevDeps(packagesDir, game) {
1079
1255
  const deps = {};
1080
1256
  for (const n of createPackageNames(game)) {
1081
- const abs = join8(packagesDir, n);
1082
- if (!existsSync4(join8(abs, "package.json"))) return void 0;
1257
+ const abs = join10(packagesDir, n);
1258
+ if (!existsSync6(join10(abs, "package.json"))) return void 0;
1083
1259
  deps[`@s2script/${n}`] = `file:${abs}`;
1084
1260
  }
1085
1261
  return deps;
1086
1262
  }
1087
1263
  function pluginSource(game) {
1088
1264
  if (game === "cs2") {
1089
- return `import { Commands } from "@s2script/sdk/commands";
1265
+ return `import { plugin } from "@s2script/sdk/plugin";
1090
1266
  import { Chat } from "@s2script/sdk/chat";
1091
1267
 
1092
- export function onLoad(): void {
1093
- Commands.register("hello", (ctx) => {
1094
- ctx.reply("hello from s2script");
1095
- if (ctx.callerSlot >= 0) {
1096
- Chat.toSlot(ctx.callerSlot, "hello from s2script");
1268
+ export default plugin((ctx) => {
1269
+ ctx.commands.register("hello", (cmd) => {
1270
+ cmd.reply("hello from s2script");
1271
+ if (cmd.callerSlot >= 0) {
1272
+ Chat.toSlot(cmd.callerSlot, "hello from s2script");
1097
1273
  }
1098
1274
  });
1099
- }
1100
-
1101
- export function onUnload(): void {}
1275
+ });
1102
1276
  `;
1103
1277
  }
1104
- return `import { OnGameFrame } from "@s2script/sdk/frame";
1278
+ return `import { plugin } from "@s2script/sdk/plugin";
1105
1279
  import { delay } from "@s2script/sdk/timers";
1106
1280
 
1107
- export function onLoad(): void {
1281
+ export default plugin((ctx) => {
1108
1282
  let n = 0;
1109
- OnGameFrame.subscribe(() => {
1283
+ ctx.server.onGameFrame(() => {
1110
1284
  n += 1;
1111
1285
  });
1112
- void delay(1000).then(() => {
1113
- console.log("s2script plugin alive; frames so far:", n);
1114
- });
1286
+ void delay(1000).then(() => console.log("s2script plugin alive; frames so far:", n));
1287
+ });
1288
+ `;
1115
1289
  }
1290
+ var ESLINT_RANGE = "^10.7.0";
1291
+ function eslintConfig() {
1292
+ return `import s2script from "@s2script/eslint-plugin";
1116
1293
 
1117
- export function onUnload(): void {}
1294
+ // The SAME pinned rules \`s2s build\` enforces \u2014 the editor's ESLint extension picks this up,
1295
+ // so a violation is a red squiggle before you ever build (green editor => green build).
1296
+ export default s2script.configs.recommended({ tsconfigRootDir: import.meta.dirname });
1118
1297
  `;
1119
1298
  }
1120
1299
  function tsconfigJson() {
1121
1300
  return JSON.stringify(
1122
1301
  {
1123
- compilerOptions: {
1124
- strict: true,
1125
- noEmit: true,
1126
- moduleResolution: "bundler",
1127
- module: "ESNext",
1128
- target: "ES2020",
1129
- lib: ["ES2020"],
1130
- types: [],
1131
- skipLibCheck: true
1132
- },
1302
+ compilerOptions: sharedCompilerOptionsJson,
1133
1303
  include: ["src", "node_modules/@s2script/sdk/globals.d.ts"]
1134
1304
  },
1135
1305
  null,
@@ -1138,7 +1308,10 @@ function tsconfigJson() {
1138
1308
  }
1139
1309
  function packageJsonContent(name, game, version, localPackagesDir) {
1140
1310
  const fileDeps = localPackagesDir ? fileDevDeps(localPackagesDir, game) : void 0;
1141
- const devDependencies = fileDeps ?? registryDevDeps(game, version);
1311
+ const devDependencies = {
1312
+ ...fileDeps ?? registryDevDeps(game, version),
1313
+ eslint: ESLINT_RANGE
1314
+ };
1142
1315
  return JSON.stringify(
1143
1316
  {
1144
1317
  name,
@@ -1148,10 +1321,7 @@ function packageJsonContent(name, game, version, localPackagesDir) {
1148
1321
  scripts: {
1149
1322
  build: "s2s build ."
1150
1323
  },
1151
- devDependencies,
1152
- s2script: {
1153
- apiVersion: "1.x"
1154
- }
1324
+ devDependencies
1155
1325
  },
1156
1326
  null,
1157
1327
  2
@@ -1164,6 +1334,12 @@ dist/
1164
1334
  .DS_Store
1165
1335
  `;
1166
1336
  }
1337
+ function linkLocalPackagesForNoInstall(targetPath, localPackagesDir) {
1338
+ const nm = join10(targetPath, "node_modules");
1339
+ mkdirSync2(nm, { recursive: true });
1340
+ const dest = join10(nm, "@s2script");
1341
+ if (!existsSync6(dest)) symlinkSync(localPackagesDir, dest);
1342
+ }
1167
1343
  function runInstall(dir, pm) {
1168
1344
  if (pm === "none") return;
1169
1345
  const r = spawnSync(pm, ["install"], {
@@ -1175,7 +1351,7 @@ function runInstall(dir, pm) {
1175
1351
  if (r.status !== 0) throw new Error(`${pm} install failed (exit ${r.status})`);
1176
1352
  }
1177
1353
  async function createPlugin(opts = {}) {
1178
- const targetPath = resolve5(opts.path ?? ".");
1354
+ const targetPath = resolve6(opts.path ?? ".");
1179
1355
  const interactive = Boolean(input.isTTY && output.isTTY && !opts.yes);
1180
1356
  let game = assertGame(opts.game);
1181
1357
  let name = opts.name;
@@ -1184,7 +1360,7 @@ async function createPlugin(opts = {}) {
1184
1360
  if (template !== "minimal") {
1185
1361
  throw new Error(`unknown template ${JSON.stringify(template)} (v1 supports: minimal)`);
1186
1362
  }
1187
- if (existsSync4(targetPath)) {
1363
+ if (existsSync6(targetPath)) {
1188
1364
  const kids = readdirSync2(targetPath);
1189
1365
  const meaningful = kids.filter((k) => k !== ".git");
1190
1366
  if (meaningful.length) {
@@ -1231,18 +1407,21 @@ async function createPlugin(opts = {}) {
1231
1407
  install = install ?? (opts.noInstall ? "none" : "npm");
1232
1408
  const version = readCliVersion();
1233
1409
  const localPackagesDir = findLocalPackagesDir();
1234
- mkdirSync2(join8(targetPath, "src"), { recursive: true });
1410
+ mkdirSync2(join10(targetPath, "src"), { recursive: true });
1235
1411
  writeFileSync5(
1236
- join8(targetPath, "package.json"),
1412
+ join10(targetPath, "package.json"),
1237
1413
  packageJsonContent(name, game, version, localPackagesDir)
1238
1414
  );
1239
- writeFileSync5(join8(targetPath, "tsconfig.json"), tsconfigJson());
1240
- writeFileSync5(join8(targetPath, "src", "plugin.ts"), pluginSource(game));
1241
- writeFileSync5(join8(targetPath, ".gitignore"), gitignore());
1415
+ writeFileSync5(join10(targetPath, "tsconfig.json"), tsconfigJson());
1416
+ writeFileSync5(join10(targetPath, "eslint.config.mjs"), eslintConfig());
1417
+ writeFileSync5(join10(targetPath, "src", "plugin.ts"), pluginSource(game));
1418
+ writeFileSync5(join10(targetPath, ".gitignore"), gitignore());
1242
1419
  let installed = false;
1243
1420
  if (install !== "none") {
1244
1421
  runInstall(targetPath, install);
1245
1422
  installed = true;
1423
+ } else if (localPackagesDir) {
1424
+ linkLocalPackagesForNoInstall(targetPath, localPackagesDir);
1246
1425
  }
1247
1426
  return {
1248
1427
  dir: targetPath,
@@ -1257,7 +1436,7 @@ async function createPlugin(opts = {}) {
1257
1436
  // src/config/gen.ts
1258
1437
  import AdmZip2 from "adm-zip";
1259
1438
  import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync3 } from "node:fs";
1260
- import { join as join9 } from "node:path";
1439
+ import { join as join11 } from "node:path";
1261
1440
 
1262
1441
  // src/config/config-path.ts
1263
1442
  function configFileName(id) {
@@ -1304,7 +1483,7 @@ function genConfigFromManifest(manifest, outDir) {
1304
1483
  const id = typeof manifest.id === "string" ? manifest.id : "";
1305
1484
  if (!id) throw new Error("config gen: manifest declares config but has no id");
1306
1485
  mkdirSync3(outDir, { recursive: true });
1307
- const path = join9(outDir, configFileName(id));
1486
+ const path = join11(outDir, configFileName(id));
1308
1487
  writeFileSync6(path, generateDefaultJsonc(config));
1309
1488
  return path;
1310
1489
  }
@@ -1328,11 +1507,11 @@ function runConfigGen(s2sps, outDir) {
1328
1507
 
1329
1508
  // src/cli.ts
1330
1509
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1331
- import { dirname as dirname3, join as join10, resolve as resolve6 } from "node:path";
1510
+ import { dirname as dirname3, join as join12, resolve as resolve7 } from "node:path";
1332
1511
  var argv = process.argv.slice(2);
1333
1512
  var command = argv[0];
1334
1513
  function repoRootFromCli() {
1335
- return join10(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "..");
1514
+ return join12(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "..");
1336
1515
  }
1337
1516
  function parseFlag(args, name) {
1338
1517
  const eq = args.find((a) => a.startsWith(`${name}=`));
@@ -1444,7 +1623,7 @@ if (command === "gen-schema") {
1444
1623
  try {
1445
1624
  const packagesDir = resolvePackagesDir({
1446
1625
  explicit: packagesDirFlag,
1447
- pluginDir: resolve6(dir),
1626
+ pluginDir: resolve7(dir),
1448
1627
  fromCliUrl: import.meta.url
1449
1628
  });
1450
1629
  console.log(await buildPlugin(dir, packagesDir));