@s2script/sdk 0.0.1 → 0.2.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 ADDED
@@ -0,0 +1,1280 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/build.ts
4
+ import * as esbuild from "esbuild";
5
+ import AdmZip from "adm-zip";
6
+ import { readFileSync as readFileSync4, mkdirSync } from "node:fs";
7
+ import { resolve as resolve4, join as join4 } from "node:path";
8
+
9
+ // src/typecheck/typecheck.ts
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";
13
+ import { tmpdir } from "node:os";
14
+
15
+ // src/packages-resolve.ts
16
+ import { existsSync } from "node:fs";
17
+ import { dirname, join, resolve } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ function isPackagesDir(dir) {
20
+ const abs = resolve(dir);
21
+ return existsSync(join(abs, "sdk", "globals.d.ts")) || existsSync(join(abs, "sdk", "entity.d.ts"));
22
+ }
23
+ function findPackagesDirNearCli(fromCliUrl = import.meta.url) {
24
+ const start = dirname(fileURLToPath(fromCliUrl));
25
+ for (const rel of ["../..", "../../.."]) {
26
+ const candidate = join(start, rel);
27
+ if (isPackagesDir(candidate)) return resolve(candidate);
28
+ }
29
+ return void 0;
30
+ }
31
+ function resolvePackagesDir(opts) {
32
+ if (opts?.explicit) {
33
+ const abs = resolve(opts.explicit);
34
+ if (!isPackagesDir(abs)) {
35
+ throw new Error(`packages dir does not look like @s2script stubs: ${abs}`);
36
+ }
37
+ return abs;
38
+ }
39
+ const env = process.env.S2SCRIPT_PACKAGES_DIR;
40
+ if (env) {
41
+ const abs = resolve(env);
42
+ if (!isPackagesDir(abs)) {
43
+ throw new Error(`S2SCRIPT_PACKAGES_DIR does not look like @s2script stubs: ${abs}`);
44
+ }
45
+ return abs;
46
+ }
47
+ const nearCli = findPackagesDirNearCli(opts?.fromCliUrl);
48
+ if (nearCli) return nearCli;
49
+ if (opts?.pluginDir) {
50
+ const nm = join(resolve(opts.pluginDir), "node_modules", "@s2script");
51
+ if (isPackagesDir(nm)) return nm;
52
+ }
53
+ throw new Error(
54
+ "cannot resolve @s2script/sdk/* types: no packages dir found.\n Install `@s2script/sdk` in the plugin (npm i -D @s2script/sdk),\n or set S2SCRIPT_PACKAGES_DIR / pass --packages-dir."
55
+ );
56
+ }
57
+
58
+ // src/typecheck/typecheck.ts
59
+ 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));
63
+ }
64
+ function declaredModules(dtsFiles) {
65
+ const out = /* @__PURE__ */ new Set();
66
+ for (const f of dtsFiles) {
67
+ const body = readFileSync(f, "utf8");
68
+ for (const m of body.matchAll(/declare\s+module\s+["']([^"']+)["']/g)) out.add(m[1]);
69
+ }
70
+ return out;
71
+ }
72
+ function typecheckPlugin(pluginDir, opts) {
73
+ const absDir = resolve2(pluginDir);
74
+ const packagesDir = opts?.packagesDir ? resolve2(opts.packagesDir) : resolvePackagesDir({ pluginDir: absDir });
75
+ const pkg = JSON.parse(readFileSync(join2(absDir, "package.json"), "utf8"));
76
+ const s2 = pkg.s2script ?? {};
77
+ const entryRel = s2.main ?? pkg.main;
78
+ if (!entryRel) throw new Error(`typecheckPlugin: no entry point in ${join2(absDir, "package.json")}`);
79
+ const entry = resolve2(absDir, entryRel);
80
+ const isAlwaysResolved = (d) => d.startsWith("@s2script/sdk/") || d === "@s2script/cs2" || d.startsWith("@s2script/cs2/");
81
+ const localDts = localDeclarationFiles(absDir);
82
+ const locallyDeclared = declaredModules(localDts);
83
+ const deps = [
84
+ ...Object.keys(s2.pluginDependencies ?? {}),
85
+ ...Object.keys(s2.optionalPluginDependencies ?? {})
86
+ // Never stub a module the plugin declares itself — a shorthand `declare module "X";` and a
87
+ // full `declare module "X" { … }` for the same X collide.
88
+ ].filter((d) => !isAlwaysResolved(d) && !locallyDeclared.has(d));
89
+ const options = {
90
+ strict: true,
91
+ noEmit: true,
92
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
93
+ module: ts.ModuleKind.ESNext,
94
+ target: ts.ScriptTarget.ES2020,
95
+ lib: ["lib.es2020.d.ts"],
96
+ types: [],
97
+ baseUrl: packagesDir,
98
+ paths: {
99
+ // Builtins are `@s2script/sdk/<cap>` → packages/sdk/<cap>.d.ts. The `@s2script/*` fallback
100
+ // now serves only @s2script/cs2 → packages/cs2/index.d.ts (the legacy per-package builtin
101
+ // dirs are deleted). tsc picks the longest matching prefix, so `@s2script/sdk/*` wins for
102
+ // sdk imports.
103
+ "@s2script/sdk/*": ["sdk/*.d.ts"],
104
+ "@s2script/*": ["*/index.d.ts"]
105
+ },
106
+ skipLibCheck: true
107
+ };
108
+ const rootNames = [entry, join2(packagesDir, "sdk", "globals.d.ts"), ...localDts];
109
+ const tmp = mkdtempSync(join2(tmpdir(), "s2tc-"));
110
+ try {
111
+ if (deps.length) {
112
+ const stub = join2(tmp, "ambient.d.ts");
113
+ writeFileSync(stub, deps.map((d) => `declare module ${JSON.stringify(d)};`).join("\n") + "\n");
114
+ rootNames.push(stub);
115
+ }
116
+ const program = ts.createProgram(rootNames, options);
117
+ const diags = [
118
+ ...program.getSyntacticDiagnostics(),
119
+ ...program.getSemanticDiagnostics(),
120
+ ...program.getGlobalDiagnostics()
121
+ ];
122
+ const out = diags.map((d) => {
123
+ let file = "?", line = 0, col = 0;
124
+ if (d.file && d.start !== void 0) {
125
+ const lc = d.file.getLineAndCharacterOfPosition(d.start);
126
+ file = d.file.fileName;
127
+ line = lc.line + 1;
128
+ col = lc.character + 1;
129
+ }
130
+ return { file, line, col, code: d.code, message: ts.flattenDiagnosticMessageText(d.messageText, "\n") };
131
+ });
132
+ return { ok: out.length === 0, diagnostics: out };
133
+ } finally {
134
+ rmSync(tmp, { recursive: true, force: true });
135
+ }
136
+ }
137
+ function formatDiagnostics(diags) {
138
+ return diags.map((d) => ` ${d.file}:${d.line}:${d.col} \u2014 TS${d.code}: ${d.message}`).join("\n");
139
+ }
140
+
141
+ // src/config-validate.ts
142
+ function validateConfigBlock(config) {
143
+ const errs = [];
144
+ if (config == null) return errs;
145
+ if (typeof config !== "object" || Array.isArray(config)) return ["s2script.config must be an object"];
146
+ for (const [key, raw] of Object.entries(config)) {
147
+ if (typeof raw !== "object" || raw === null) {
148
+ errs.push(`config '${key}': must be { type, default }`);
149
+ continue;
150
+ }
151
+ const decl = raw;
152
+ const t = decl.type, d = decl.default;
153
+ if (t !== "string" && t !== "int" && t !== "float" && t !== "bool") {
154
+ errs.push(`config '${key}': unknown type ${JSON.stringify(t)} (want string|int|float|bool)`);
155
+ continue;
156
+ }
157
+ const ok = t === "string" && typeof d === "string" || t === "int" && typeof d === "number" && Number.isInteger(d) || t === "float" && typeof d === "number" || t === "bool" && typeof d === "boolean";
158
+ if (!ok) errs.push(`config '${key}': default ${JSON.stringify(d)} does not match type '${t}'`);
159
+ }
160
+ return errs;
161
+ }
162
+
163
+ // src/publish-gate.ts
164
+ import { existsSync as existsSync3, readFileSync as readFileSync2, statSync } from "node:fs";
165
+ import { join as join3, resolve as resolve3 } from "node:path";
166
+ function hasPublishes(publishes) {
167
+ if (publishes == null) return false;
168
+ if (typeof publishes === "string") return publishes.trim().length > 0;
169
+ if (typeof publishes === "object") return Object.keys(publishes).length > 0;
170
+ return false;
171
+ }
172
+ function assertPublishesTypes(pkg, pluginDir) {
173
+ const publishes = pkg.s2script?.publishes;
174
+ if (!hasPublishes(publishes)) {
175
+ return { ok: true, typesPath: null };
176
+ }
177
+ const typesRel = pkg.types ?? pkg.typings;
178
+ if (!typesRel || typeof typesRel !== "string") {
179
+ return {
180
+ ok: false,
181
+ error: 'publishes is set but "types" is missing \u2014 add api.d.ts and set "types": "api.d.ts"'
182
+ };
183
+ }
184
+ if (!typesRel.endsWith(".d.ts")) {
185
+ return {
186
+ ok: false,
187
+ error: `published API must be a .d.ts file (got ${JSON.stringify(typesRel)})`
188
+ };
189
+ }
190
+ const typesPath = resolve3(pluginDir, typesRel);
191
+ if (!existsSync3(typesPath)) {
192
+ return { ok: false, error: `types file not found: ${typesRel}` };
193
+ }
194
+ const st = statSync(typesPath);
195
+ if (!st.isFile() || st.size === 0) {
196
+ return { ok: false, error: `types file is empty or not a file: ${typesRel}` };
197
+ }
198
+ const body = readFileSync2(typesPath, "utf8").trim();
199
+ if (!body) {
200
+ return { ok: false, error: `types file is empty: ${typesRel}` };
201
+ }
202
+ return { ok: true, typesPath };
203
+ }
204
+
205
+ // src/publishes.ts
206
+ import { createHash } from "node:crypto";
207
+ import { readFileSync as readFileSync3 } from "node:fs";
208
+ function expandPublishes(authored, pkgName, pkgVersion) {
209
+ if (authored === void 0 || authored === null) return {};
210
+ if (typeof authored === "string") {
211
+ if (authored.trim() !== "self") {
212
+ throw new Error(
213
+ `publishes: the only valid string form is "self" (got ${JSON.stringify(authored)}); use the map form to publish a differently-named contract`
214
+ );
215
+ }
216
+ return { [pkgName]: pkgVersion };
217
+ }
218
+ if (typeof authored !== "object") {
219
+ throw new Error(`publishes must be "self" or an object (got ${typeof authored})`);
220
+ }
221
+ const out = {};
222
+ for (const [iface, range] of Object.entries(authored)) {
223
+ if (typeof range !== "string") {
224
+ throw new Error(`publishes[${JSON.stringify(iface)}] must be a version range string`);
225
+ }
226
+ out[iface] = range;
227
+ }
228
+ return out;
229
+ }
230
+ function hashContract(typesPath) {
231
+ return createHash("sha256").update(readFileSync3(typesPath)).digest("hex");
232
+ }
233
+ var CONCRETE_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
234
+ function isConcreteVersion(v) {
235
+ return CONCRETE_SEMVER.test(v.trim());
236
+ }
237
+ function derivePublishes(authored, pkgName, pkgVersion, typesPath) {
238
+ const expanded = expandPublishes(authored, pkgName, pkgVersion);
239
+ const names = Object.keys(expanded);
240
+ if (names.length === 0) return {};
241
+ if (names.length > 1) {
242
+ throw new Error(
243
+ `publishes declares ${names.length} interfaces (${names.join(", ")}) but a plugin ships a single "types" contract, so they would all carry the SAME typesSha256 \u2014 a hash that cannot say which contract is which. Publish one interface per plugin (a per-interface "types" map is not supported).`
244
+ );
245
+ }
246
+ if (typesPath === null) {
247
+ throw new Error(
248
+ `publishes is set but no contract .d.ts was resolved \u2014 set "types": "api.d.ts" in package.json`
249
+ );
250
+ }
251
+ for (const [iface, v] of Object.entries(expanded)) {
252
+ if (!isConcreteVersion(v)) {
253
+ throw new Error(
254
+ `publishes[${JSON.stringify(iface)}] = ${JSON.stringify(v)} is a RANGE. Resolving a range against a published contract requires the registry (design spec \xA74.6, \xA710 \u2014 not yet implemented). Use a concrete version for a contract you ship yourself, or "self".`
255
+ );
256
+ }
257
+ }
258
+ const typesSha256 = hashContract(typesPath);
259
+ const out = {};
260
+ for (const name of names) {
261
+ out[name] = { version: expanded[name].trim(), typesSha256 };
262
+ }
263
+ return out;
264
+ }
265
+
266
+ // src/build.ts
267
+ async function buildPlugin(dir, packagesDir) {
268
+ const absDir = resolve4(dir);
269
+ const pkgPath = join4(absDir, "package.json");
270
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
271
+ const gate = assertPublishesTypes(pkg, absDir);
272
+ if (!gate.ok) {
273
+ throw new Error(`publish gate failed: ${gate.error}`);
274
+ }
275
+ const s2 = pkg.s2script ?? {};
276
+ const derivedPublishes = derivePublishes(s2.publishes, pkg.name, pkg.version, gate.typesPath);
277
+ const tc = typecheckPlugin(absDir, packagesDir !== void 0 ? { packagesDir } : void 0);
278
+ if (!tc.ok) {
279
+ throw new Error(`typecheck failed (${tc.diagnostics.length} error(s)):
280
+ ${formatDiagnostics(tc.diagnostics)}`);
281
+ }
282
+ const { name, version } = pkg;
283
+ const apiVersion = s2.apiVersion ?? "";
284
+ const pluginDependencies = s2.pluginDependencies ?? {};
285
+ const optionalPluginDependencies = s2.optionalPluginDependencies ?? {};
286
+ const config = s2.config ?? void 0;
287
+ if (config !== void 0) {
288
+ const cfgErrs = validateConfigBlock(config);
289
+ if (cfgErrs.length) throw new Error(`invalid s2script.config:
290
+ ${cfgErrs.join("\n ")}`);
291
+ }
292
+ const external = Array.from(/* @__PURE__ */ new Set([
293
+ "@s2script/*",
294
+ ...Object.keys(pluginDependencies),
295
+ ...Object.keys(optionalPluginDependencies)
296
+ ]));
297
+ const entryRelative = s2.main ?? pkg.main;
298
+ if (!entryRelative) {
299
+ throw new Error(
300
+ `buildPlugin: no entry point found in ${pkgPath} (set s2script.main or main)`
301
+ );
302
+ }
303
+ const entryPoint = join4(absDir, entryRelative);
304
+ const result = await esbuild.build({
305
+ entryPoints: [entryPoint],
306
+ bundle: true,
307
+ platform: "neutral",
308
+ format: "cjs",
309
+ external,
310
+ target: "es2020",
311
+ write: false
312
+ });
313
+ const pluginJs = result.outputFiles[0].text;
314
+ const manifest = {
315
+ id: name,
316
+ version,
317
+ apiVersion,
318
+ // <-- MUST be "apiVersion" to match #[serde(rename = "apiVersion")]
319
+ pluginDependencies,
320
+ optionalPluginDependencies
321
+ };
322
+ if (Object.keys(derivedPublishes).length > 0) {
323
+ manifest.publishes = derivedPublishes;
324
+ }
325
+ if (config !== void 0) manifest.config = config;
326
+ const zip = new AdmZip();
327
+ zip.addFile("manifest.json", Buffer.from(JSON.stringify(manifest, null, 2)));
328
+ zip.addFile("plugin.js", Buffer.from(pluginJs));
329
+ if (gate.typesPath !== null && Object.keys(derivedPublishes).length > 0) {
330
+ const contract = readFileSync4(gate.typesPath);
331
+ for (const iface of Object.keys(derivedPublishes)) {
332
+ const safe = iface.replace(/[^a-zA-Z0-9._-]/g, "_");
333
+ zip.addFile(`types/${safe}.d.ts`, contract);
334
+ }
335
+ }
336
+ const sanitizedId = name.replace(/[^a-zA-Z0-9._-]/g, "_");
337
+ const outDir = join4(absDir, "dist");
338
+ mkdirSync(outDir, { recursive: true });
339
+ const outPath = join4(outDir, `${sanitizedId}.s2sp`);
340
+ zip.writeZip(outPath);
341
+ return outPath;
342
+ }
343
+
344
+ // src/schemagen/gen.ts
345
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
346
+ import { join as join5 } from "node:path";
347
+
348
+ // src/schemagen/model.ts
349
+ var READ = {
350
+ f32: "readFloat32",
351
+ bool: "readBool",
352
+ i8: "readInt8",
353
+ i16: "readInt16",
354
+ i32: "readInt32",
355
+ u8: "readUInt8",
356
+ u16: "readUInt16",
357
+ u32: "readUInt32",
358
+ handle: "readHandle",
359
+ u64: "readUInt64",
360
+ i64: "readInt64",
361
+ f64: "readFloat64",
362
+ str: "readString",
363
+ vector: "readFloats",
364
+ qangle: "readFloats"
365
+ };
366
+ var WRITE = {
367
+ f32: "writeFloat32",
368
+ bool: "writeBool",
369
+ i8: "writeInt8",
370
+ i16: "writeInt16",
371
+ i32: "writeInt32",
372
+ u8: "writeUInt8",
373
+ u16: "writeUInt16",
374
+ u32: "writeUInt32"
375
+ };
376
+ var TSTYPE = {
377
+ f32: "number | null",
378
+ bool: "boolean | null",
379
+ i8: "number | null",
380
+ i16: "number | null",
381
+ i32: "number | null",
382
+ u8: "number | null",
383
+ u16: "number | null",
384
+ u32: "number | null",
385
+ handle: "EntityRef | null",
386
+ u64: "string | null",
387
+ i64: "string | null",
388
+ f64: "number | null",
389
+ str: "string | null",
390
+ vector: "Vector | null",
391
+ qangle: "QAngle | null"
392
+ };
393
+ var ATOMIC = {
394
+ float32: { k: "f32", w: true },
395
+ bool: { k: "bool", w: true },
396
+ int8: { k: "i8", w: true },
397
+ int16: { k: "i16", w: true },
398
+ int32: { k: "i32", w: true },
399
+ uint8: { k: "u8", w: true },
400
+ uint16: { k: "u16", w: true },
401
+ uint32: { k: "u32", w: true },
402
+ uint64: { k: "u64", w: false },
403
+ int64: { k: "i64", w: false },
404
+ float64: { k: "f64", w: false }
405
+ // no narrow 64-bit writers
406
+ };
407
+ var VEC = { Vector: "vector", VectorWS: "vector", QAngle: "qangle" };
408
+ var VEC_INFO = {
409
+ vector: { cls: "Vector", count: 3 },
410
+ qangle: { cls: "QAngle", count: 3 }
411
+ };
412
+ var KNOWN_TAGS = /* @__PURE__ */ new Set(["i", "n", "b", "h", "fl", "f", "u", "e", "p", "a", "v", "vec", "ang", "q", "sz", "isz", "ch", "clr", "un"]);
413
+ function idiomaticName(raw) {
414
+ const s = raw.replace(/^m_/, "");
415
+ const m = s.match(/^([a-z]+)([A-Z].*)$/);
416
+ const core = m && KNOWN_TAGS.has(m[1]) ? m[2] : s;
417
+ return core.charAt(0).toLowerCase() + core.slice(1);
418
+ }
419
+ function classifyField(type) {
420
+ if (type.kind === "handle") return { accessorKind: "handle", writable: false };
421
+ if (type.kind === "atomic") {
422
+ const vk = VEC[type.name ?? ""];
423
+ if (vk) return { accessorKind: vk, writable: false };
424
+ const m = ATOMIC[type.name ?? ""];
425
+ if (m) return { accessorKind: m.k, writable: m.w };
426
+ return { skip: `atomic '${type.name}' is not a scalar (string/vector/compound)` };
427
+ }
428
+ if (type.kind === "unknown") {
429
+ const cm = (type.name ?? "").match(/^char\[(\d+)\]$/);
430
+ if (cm) return { accessorKind: "str", writable: false, strLen: parseInt(cm[1], 10) };
431
+ return { skip: `unmapped 'unknown' type '${type.name ?? ""}'` };
432
+ }
433
+ if (type.kind === "enum") return { skip: "enum byte-width absent from catalog (deferred)" };
434
+ if (type.kind === "class") return { skip: `embedded class '${type.name ?? ""}' deferred` };
435
+ if (type.kind === "ptr") return { skip: "raw pointer" };
436
+ return { skip: `unmapped kind '${type.kind}'` };
437
+ }
438
+ function flattenedFields(model, className) {
439
+ const byName = new Map(model.classes.map((c) => [c.className, c]));
440
+ const chain = [];
441
+ let cur = className;
442
+ while (cur && byName.has(cur)) {
443
+ const c = byName.get(cur);
444
+ chain.unshift(c);
445
+ cur = c.parent;
446
+ }
447
+ return chain.flatMap((c) => c.ownFields);
448
+ }
449
+ function buildModel(catalog, requested) {
450
+ const inClosure = /* @__PURE__ */ new Set();
451
+ for (const start of requested) {
452
+ if (!catalog[start]) throw new Error(`gen-schema: requested class '${start}' is not in the catalog`);
453
+ let cur = start;
454
+ while (cur && catalog[cur] && !inClosure.has(cur)) {
455
+ inClosure.add(cur);
456
+ cur = catalog[cur].parent;
457
+ }
458
+ }
459
+ const depth = (c) => {
460
+ let d = 0, cur = c;
461
+ while (cur && catalog[cur]?.parent && inClosure.has(catalog[cur].parent)) {
462
+ d++;
463
+ cur = catalog[cur].parent;
464
+ }
465
+ return d;
466
+ };
467
+ const ordered = [...inClosure].sort((a, b) => depth(a) - depth(b) || (a < b ? -1 : a > b ? 1 : 0));
468
+ const classes = ordered.map((className) => {
469
+ const parent = catalog[className].parent;
470
+ const ownFields = [];
471
+ const skipped = [];
472
+ for (const f of catalog[className].fields) {
473
+ const c = classifyField(f.type);
474
+ if ("skip" in c) {
475
+ skipped.push({ className, rawName: f.name, reason: c.skip });
476
+ continue;
477
+ }
478
+ ownFields.push({ propName: idiomaticName(f.name), rawName: f.name, declaringClass: className, accessorKind: c.accessorKind, writable: c.writable, strLen: c.strLen });
479
+ }
480
+ return { className, parent: parent && inClosure.has(parent) ? parent : null, ownFields, skipped };
481
+ });
482
+ const byProp = /* @__PURE__ */ new Map();
483
+ for (const c of classes) for (const f of c.ownFields) {
484
+ (byProp.get(f.propName) ?? byProp.set(f.propName, []).get(f.propName)).push(f);
485
+ }
486
+ const collisions = [];
487
+ for (const [prop, fields] of byProp) {
488
+ const distinct = new Set(fields.map((f) => `${f.declaringClass}.${f.rawName}`));
489
+ if (distinct.size >= 2) {
490
+ for (const f of fields) f.propName = f.rawName;
491
+ collisions.push(`${prop} \u2190 ${[...distinct].sort().join(", ")}`);
492
+ }
493
+ }
494
+ collisions.sort();
495
+ return { classes, collisions };
496
+ }
497
+
498
+ // src/schemagen/emit-dts.ts
499
+ var HEADER = "// GENERATED by `s2script gen-schema` from schema-catalog.json \u2014 DO NOT EDIT.";
500
+ function emitDts(model) {
501
+ const names = new Set(model.classes.map((c) => c.className));
502
+ const out = [HEADER, 'import type { EntityRef } from "@s2script/sdk/entity";'];
503
+ const vecClasses = /* @__PURE__ */ new Set();
504
+ for (const c of model.classes) for (const f of c.ownFields) {
505
+ const vi = VEC_INFO[f.accessorKind];
506
+ if (vi) vecClasses.add(vi.cls);
507
+ }
508
+ if (vecClasses.size) out.push(`import type { ${[...vecClasses].sort().join(", ")} } from "@s2script/sdk/math";`);
509
+ out.push("");
510
+ for (const c of model.classes) {
511
+ const ext = c.parent && names.has(c.parent) ? ` extends ${c.parent}` : "";
512
+ out.push(`export interface ${c.className}${ext} {`);
513
+ for (const f of c.ownFields) {
514
+ const ro = f.writable ? "" : "readonly ";
515
+ out.push(` ${ro}${f.propName}: ${TSTYPE[f.accessorKind]};`);
516
+ }
517
+ out.push("}", "");
518
+ }
519
+ return out.join("\n");
520
+ }
521
+
522
+ // src/schemagen/emit-js.ts
523
+ var HEADER2 = "// GENERATED by `s2script gen-schema` from schema-catalog.json \u2014 DO NOT EDIT.";
524
+ var S = JSON.stringify;
525
+ function emitJs(model) {
526
+ const out = [HEADER2, "(function () {", " var off = __s2_schema_offset;", " var A = {};"];
527
+ const vecClasses = /* @__PURE__ */ new Set();
528
+ for (const c of model.classes) for (const f of flattenedFields(model, c.className)) {
529
+ const vi = VEC_INFO[f.accessorKind];
530
+ if (vi) vecClasses.add(vi.cls);
531
+ }
532
+ for (const cls of [...vecClasses].sort()) {
533
+ out.push(` var ${cls} = __s2require("@s2script/sdk/math").${cls};`);
534
+ }
535
+ for (const c of model.classes) {
536
+ out.push(` A[${S(c.className)}] = {`);
537
+ for (const f of flattenedFields(model, c.className)) {
538
+ const resolve7 = `off(${S(f.declaringClass)},${S(f.rawName)})`;
539
+ let entry;
540
+ if (f.accessorKind === "str") {
541
+ entry = `get: function () { return this.ref.readString(${resolve7}, ${f.strLen}); }`;
542
+ } else if (f.accessorKind === "u64" || f.accessorKind === "i64") {
543
+ entry = `get: function () { var v = this.ref.${READ[f.accessorKind]}(${resolve7}); return v === null ? null : v.toString(); }`;
544
+ } else if (VEC_INFO[f.accessorKind]) {
545
+ const vi = VEC_INFO[f.accessorKind];
546
+ entry = `get: function () { var a = this.ref.readFloats(${resolve7}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
547
+ } else {
548
+ entry = `get: function () { return this.ref.${READ[f.accessorKind]}(${resolve7}); }`;
549
+ }
550
+ if (f.writable) {
551
+ const wm = WRITE[f.accessorKind];
552
+ entry += `, set: function (v) { var o = ${resolve7}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(o); }`;
553
+ }
554
+ out.push(` ${S(f.propName)}: { ${entry} },`);
555
+ }
556
+ out.push(" };");
557
+ }
558
+ out.push(
559
+ " function applyAccessors(proto, className) {",
560
+ " var defs = A[className]; if (!defs) return;",
561
+ " for (var name in defs) {",
562
+ " Object.defineProperty(proto, name, { get: defs[name].get, set: defs[name].set, enumerable: true, configurable: true });",
563
+ " }",
564
+ " }",
565
+ " function wrap(className, ref) { var o = { ref: ref }; applyAccessors(o, className); return o; }",
566
+ " globalThis.__s2pkg_cs2_schema = { applyAccessors: applyAccessors, wrap: wrap };",
567
+ "})();"
568
+ );
569
+ return out.join("\n");
570
+ }
571
+
572
+ // src/schemagen/gen.ts
573
+ var CATALOG_PATH = "games/cs2/gamedata/schema-catalog.json";
574
+ var LIST_PATH = "games/cs2/codegen-classes.json";
575
+ var JS_OUT = "games/cs2/js/schema.generated.js";
576
+ var DTS_OUT = "packages/cs2/schema.generated.d.ts";
577
+ function runGenSchema(repoRoot, opts) {
578
+ const catalog = JSON.parse(readFileSync5(join5(repoRoot, CATALOG_PATH), "utf8"));
579
+ const list = JSON.parse(readFileSync5(join5(repoRoot, LIST_PATH), "utf8"));
580
+ const model = buildModel(catalog, list);
581
+ const dts = emitDts(model);
582
+ const js = emitJs(model);
583
+ const fields = model.classes.reduce((n, c) => n + c.ownFields.length, 0);
584
+ const skipped = model.classes.reduce((n, c) => n + c.skipped.length, 0);
585
+ const drift = [];
586
+ const files = [[JS_OUT, js], [DTS_OUT, dts]];
587
+ for (const [rel, content] of files) {
588
+ const abs = join5(repoRoot, rel);
589
+ if (opts.check) {
590
+ let cur = "";
591
+ try {
592
+ cur = readFileSync5(abs, "utf8");
593
+ } catch {
594
+ }
595
+ if (cur !== content) drift.push(rel);
596
+ } else {
597
+ writeFileSync2(abs, content);
598
+ }
599
+ }
600
+ if (model.collisions.length) console.error(`gen-schema: ${model.collisions.length} name collision(s) \u2192 raw fallback:
601
+ ` + model.collisions.join("\n "));
602
+ for (const c of model.classes) if (c.skipped.length) console.error(`gen-schema: ${c.className}: skipped ${c.skipped.length} field(s)`);
603
+ return { classes: model.classes.length, fields, skipped, drift };
604
+ }
605
+
606
+ // src/eventgen/gen.ts
607
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
608
+ import { join as join6 } from "node:path";
609
+
610
+ // src/eventgen/model.ts
611
+ var TYPE_TO_GETTER = {
612
+ bool: "getBool",
613
+ float: "getFloat",
614
+ int: "getInt",
615
+ player: "getPlayerSlot",
616
+ string: "getString",
617
+ uint64: "getUint64"
618
+ };
619
+ function pascalCase(name) {
620
+ return name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
621
+ }
622
+ function buildEventModel(catalog) {
623
+ return Object.keys(catalog).sort().map((event) => {
624
+ const fields = catalog[event];
625
+ const byGetter = {
626
+ getBool: [],
627
+ getFloat: [],
628
+ getInt: [],
629
+ getPlayerSlot: [],
630
+ getString: [],
631
+ getUint64: []
632
+ };
633
+ for (const field of Object.keys(fields).sort()) {
634
+ const type = fields[field];
635
+ const getter = TYPE_TO_GETTER[type];
636
+ if (getter) byGetter[getter].push(field);
637
+ }
638
+ return {
639
+ event,
640
+ iface: pascalCase(event) + "Event",
641
+ byGetter
642
+ };
643
+ });
644
+ }
645
+
646
+ // src/eventgen/emit-dts.ts
647
+ var HEADER3 = "// GENERATED by `s2script gen-events` from event-catalog.json \u2014 DO NOT EDIT.";
648
+ var GETTER_RET = {
649
+ getBool: "boolean",
650
+ getFloat: "number",
651
+ getInt: "number",
652
+ getPlayerSlot: "number",
653
+ getString: "string",
654
+ getUint64: "string"
655
+ };
656
+ function emitEventDts(model) {
657
+ const out = [
658
+ HEADER3,
659
+ 'import type { GameEvent, HookResultValue } from "@s2script/sdk/events";',
660
+ ""
661
+ ];
662
+ for (const { iface, byGetter } of model) {
663
+ out.push(`export interface ${iface} extends GameEvent {`);
664
+ for (const getter of Object.keys(byGetter)) {
665
+ const fields = byGetter[getter];
666
+ if (fields.length === 0) continue;
667
+ const keys = fields.map((f) => `"${f}"`).join(" | ");
668
+ const ret = GETTER_RET[getter];
669
+ out.push(` ${getter}(key: ${keys}): ${ret};`);
670
+ }
671
+ out.push("}", "");
672
+ }
673
+ out.push("export interface GameEvents {");
674
+ for (const { event, iface } of model) {
675
+ out.push(` ${event}: ${iface};`);
676
+ }
677
+ out.push("}", "");
678
+ out.push(
679
+ "export function on<K extends keyof GameEvents>(name: K, handler: (ev: GameEvents[K]) => void): void;",
680
+ "export function on(name: string, handler: (ev: GameEvent) => void): void;",
681
+ "export function onPre<K extends keyof GameEvents>(name: K, handler: (ev: GameEvents[K]) => HookResultValue | void): void;",
682
+ "export function onPre(name: string, handler: (ev: GameEvent) => HookResultValue | void): void;",
683
+ "export function fire<K extends keyof GameEvents>(name: K, fields?: Record<string, number | string | boolean | bigint>, dontBroadcast?: boolean): boolean;",
684
+ "export function fire(name: string, fields?: Record<string, number | string | boolean | bigint>, dontBroadcast?: boolean): boolean;"
685
+ );
686
+ return out.join("\n") + "\n";
687
+ }
688
+
689
+ // src/eventgen/gen.ts
690
+ var CATALOG_PATH2 = "games/cs2/gamedata/event-catalog.json";
691
+ var DTS_OUT2 = "packages/cs2/events.generated.d.ts";
692
+ function runGenEvents(repoRoot, opts) {
693
+ const catalog = JSON.parse(readFileSync6(join6(repoRoot, CATALOG_PATH2), "utf8"));
694
+ const model = buildEventModel(catalog);
695
+ const dts = emitEventDts(model);
696
+ const drift = [];
697
+ const abs = join6(repoRoot, DTS_OUT2);
698
+ if (opts.check) {
699
+ let cur = "";
700
+ try {
701
+ cur = readFileSync6(abs, "utf8");
702
+ } catch {
703
+ }
704
+ if (cur !== dts) drift.push(DTS_OUT2);
705
+ } else {
706
+ writeFileSync3(abs, dts);
707
+ }
708
+ return { events: model.length, drift };
709
+ }
710
+
711
+ // src/navgen/gen.ts
712
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
713
+ import { join as join7 } from "node:path";
714
+
715
+ // src/navgen/model.ts
716
+ var SUPPORTED_NAV_KINDS = /* @__PURE__ */ new Set([
717
+ "i8",
718
+ "i16",
719
+ "i32",
720
+ "u8",
721
+ "u16",
722
+ "u32",
723
+ "f32",
724
+ "bool",
725
+ "u64",
726
+ "i64",
727
+ "handle",
728
+ "vector",
729
+ "qangle"
730
+ ]);
731
+ function buildNavModel(config, catalog) {
732
+ const targetClasses = [...new Set(config.map((e) => e.target))];
733
+ const schemaModel = buildModel(catalog, targetClasses);
734
+ const sorted = [...config].sort((a, b) => a.wrapper < b.wrapper ? -1 : a.wrapper > b.wrapper ? 1 : 0);
735
+ const wrappers = sorted.map((entry) => {
736
+ const all = flattenedFields(schemaModel, entry.target);
737
+ const fields = [];
738
+ const skippedKinds = [];
739
+ for (const f of all) {
740
+ if (SUPPORTED_NAV_KINDS.has(f.accessorKind)) {
741
+ fields.push(f);
742
+ } else {
743
+ skippedKinds.push({ propName: f.propName, accessorKind: f.accessorKind });
744
+ }
745
+ }
746
+ return {
747
+ wrapper: entry.wrapper,
748
+ prop: entry.prop,
749
+ source: entry.source,
750
+ target: entry.target,
751
+ path: entry.path,
752
+ fields,
753
+ skippedKinds
754
+ };
755
+ });
756
+ return { wrappers };
757
+ }
758
+
759
+ // src/navgen/emit-js.ts
760
+ var HEADER4 = "// GENERATED by `s2script gen-nav` from nav-targets.json \u2014 DO NOT EDIT.";
761
+ var S2 = JSON.stringify;
762
+ var READ_VIA = {
763
+ f32: "readFloat32Via",
764
+ bool: "readBoolVia",
765
+ i8: "readInt8Via",
766
+ i16: "readInt16Via",
767
+ i32: "readInt32Via",
768
+ u8: "readUInt8Via",
769
+ u16: "readUInt16Via",
770
+ u32: "readUInt32Via"
771
+ };
772
+ function emitNavJs(model) {
773
+ const out = [HEADER4, "(function () {", " var off = __s2_schema_offset;"];
774
+ const vecClasses = /* @__PURE__ */ new Set();
775
+ for (const w of model.wrappers) {
776
+ for (const f of w.fields) {
777
+ const vi = VEC_INFO[f.accessorKind];
778
+ if (vi) vecClasses.add(vi.cls);
779
+ }
780
+ }
781
+ for (const cls of [...vecClasses].sort()) {
782
+ out.push(` var ${cls} = __s2require("@s2script/sdk/math").${cls};`);
783
+ }
784
+ for (const w of model.wrappers) {
785
+ out.push(` function ${w.wrapper}(root, path) { this.root = root; this.path = path; }`);
786
+ out.push(` Object.defineProperties(${w.wrapper}.prototype, {`);
787
+ for (const f of w.fields) {
788
+ const resolve7 = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
789
+ let getter;
790
+ const kind = f.accessorKind;
791
+ if (kind === "u64") {
792
+ getter = `get: function () { var v = this.root.readUInt64Via(this.path, ${resolve7}); return v === null ? null : v.toString(); }`;
793
+ } else if (kind === "i64") {
794
+ getter = `get: function () { var v = this.root.readInt64Via(this.path, ${resolve7}); return v === null ? null : v.toString(); }`;
795
+ } else if (VEC_INFO[kind]) {
796
+ const vi = VEC_INFO[kind];
797
+ 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]); }`;
798
+ } else if (kind === "handle") {
799
+ getter = `get: function () { return this.root.readHandleVia(this.path, ${resolve7}); }`;
800
+ } else {
801
+ const method = READ_VIA[kind];
802
+ if (!method) continue;
803
+ getter = `get: function () { return this.root.${method}(this.path, ${resolve7}); }`;
804
+ }
805
+ out.push(` ${S2(f.propName)}: { ${getter} },`);
806
+ }
807
+ out.push(" });");
808
+ }
809
+ const bySource = /* @__PURE__ */ new Map();
810
+ for (const w of model.wrappers) {
811
+ if (!bySource.has(w.source)) bySource.set(w.source, []);
812
+ bySource.get(w.source).push(w);
813
+ }
814
+ out.push(" function applyNav(proto, className) {");
815
+ for (const [src, wrappers] of [...bySource.entries()].sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)) {
816
+ out.push(` if (className === ${S2(src)}) {`);
817
+ for (const w of wrappers) {
818
+ out.push(` Object.defineProperty(proto, ${S2(w.prop)}, {`);
819
+ out.push(` get: function () {`);
820
+ out.push(` var _p = [];`);
821
+ for (let i = 0; i < w.path.length; i++) {
822
+ const h = w.path[i];
823
+ out.push(` var o${i} = off(${S2(h.cls)},${S2(h.field)}); if (o${i} < 0) return null; _p.push(o${i});`);
824
+ }
825
+ out.push(` return new ${w.wrapper}(this.ref, _p);`);
826
+ out.push(` }, enumerable: true, configurable: true,`);
827
+ out.push(` });`);
828
+ }
829
+ out.push(" }");
830
+ }
831
+ out.push(" }");
832
+ out.push(" globalThis.__s2pkg_cs2_nav = { applyNav: applyNav };");
833
+ out.push("})();");
834
+ out.push("");
835
+ return out.join("\n");
836
+ }
837
+
838
+ // src/navgen/emit-dts.ts
839
+ var HEADER5 = "// GENERATED by `s2script gen-nav` from nav-targets.json \u2014 DO NOT EDIT.";
840
+ function emitNavDts(model) {
841
+ let needsEntityRef = false;
842
+ let needsVector = false;
843
+ let needsQAngle = false;
844
+ for (const w of model.wrappers) {
845
+ for (const f of w.fields) {
846
+ if (f.accessorKind === "handle") needsEntityRef = true;
847
+ const vi = VEC_INFO[f.accessorKind];
848
+ if (vi?.cls === "Vector") needsVector = true;
849
+ if (vi?.cls === "QAngle") needsQAngle = true;
850
+ }
851
+ }
852
+ const out = [HEADER5];
853
+ const mathTypes = [];
854
+ if (needsVector) mathTypes.push("Vector");
855
+ if (needsQAngle) mathTypes.push("QAngle");
856
+ if (mathTypes.length) out.push(`import type { ${mathTypes.join(", ")} } from "@s2script/sdk/math";`);
857
+ if (needsEntityRef) out.push('import type { EntityRef } from "@s2script/sdk/entity";');
858
+ out.push("");
859
+ for (const w of model.wrappers) {
860
+ out.push(`export interface ${w.wrapper} {`);
861
+ for (const f of w.fields) {
862
+ out.push(` readonly ${f.propName}: ${TSTYPE[f.accessorKind]};`);
863
+ }
864
+ out.push("}", "");
865
+ }
866
+ return out.join("\n");
867
+ }
868
+
869
+ // src/navgen/gen.ts
870
+ var TARGETS_PATH = "games/cs2/nav-targets.json";
871
+ var CATALOG_PATH3 = "games/cs2/gamedata/schema-catalog.json";
872
+ var JS_OUT2 = "games/cs2/js/nav.generated.js";
873
+ var DTS_OUT3 = "packages/cs2/nav.generated.d.ts";
874
+ function runGenNav(repoRoot, opts) {
875
+ const config = JSON.parse(readFileSync7(join7(repoRoot, TARGETS_PATH), "utf8"));
876
+ const catalog = JSON.parse(readFileSync7(join7(repoRoot, CATALOG_PATH3), "utf8"));
877
+ const model = buildNavModel(config, catalog);
878
+ const js = emitNavJs(model);
879
+ const dts = emitNavDts(model);
880
+ const fields = model.wrappers.reduce((n, w) => n + w.fields.length, 0);
881
+ const drift = [];
882
+ const files = [[JS_OUT2, js], [DTS_OUT3, dts]];
883
+ for (const [rel, content] of files) {
884
+ const abs = join7(repoRoot, rel);
885
+ if (opts.check) {
886
+ let cur = "";
887
+ try {
888
+ cur = readFileSync7(abs, "utf8");
889
+ } catch {
890
+ }
891
+ if (cur !== content) drift.push(rel);
892
+ } else {
893
+ writeFileSync4(abs, content);
894
+ }
895
+ }
896
+ return { wrappers: model.wrappers.length, fields, drift };
897
+ }
898
+
899
+ // src/create/create.ts
900
+ import {
901
+ mkdirSync as mkdirSync2,
902
+ writeFileSync as writeFileSync5,
903
+ existsSync as existsSync4,
904
+ readdirSync as readdirSync2,
905
+ readFileSync as readFileSync8
906
+ } from "node:fs";
907
+ import { dirname as dirname2, join as join8, resolve as resolve5, basename } from "node:path";
908
+ import { createInterface } from "node:readline/promises";
909
+ import { stdin as input, stdout as output } from "node:process";
910
+ import { spawnSync } from "node:child_process";
911
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
912
+ function readCliVersion() {
913
+ const here = dirname2(fileURLToPath2(import.meta.url));
914
+ const candidates = [
915
+ join8(here, "..", "package.json"),
916
+ // dist/cli.js → packages/sdk/package.json
917
+ join8(here, "..", "..", "package.json")
918
+ // src/create → packages/sdk/package.json
919
+ ];
920
+ for (const p of candidates) {
921
+ if (!existsSync4(p)) continue;
922
+ try {
923
+ const pkg = JSON.parse(readFileSync8(p, "utf8"));
924
+ if (pkg.name === "@s2script/sdk" && pkg.version) return pkg.version;
925
+ } catch {
926
+ }
927
+ }
928
+ return "0.1.0";
929
+ }
930
+ function findLocalPackagesDir() {
931
+ const here = dirname2(fileURLToPath2(import.meta.url));
932
+ const candidates = [
933
+ join8(here, "..", ".."),
934
+ // dist/cli.js → packages
935
+ join8(here, "..", "..", "..")
936
+ // src/create → packages
937
+ ];
938
+ for (const c of candidates) {
939
+ if (isPackagesDir(c)) return resolve5(c);
940
+ }
941
+ return void 0;
942
+ }
943
+ function defaultNameFromPath(dir) {
944
+ const base = basename(resolve5(dir));
945
+ const slug = base.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "plugin";
946
+ return `@plugin/${slug}`;
947
+ }
948
+ async function promptSelect(rl, question, choices, defaultValue) {
949
+ console.log(question);
950
+ choices.forEach((c, i) => {
951
+ const mark = c.value === defaultValue ? "*" : " ";
952
+ console.log(` ${mark} ${i + 1}) ${c.label}`);
953
+ });
954
+ const raw = (await rl.question(` (default ${defaultValue}): `)).trim();
955
+ if (!raw) return defaultValue;
956
+ const asNum = Number(raw);
957
+ if (Number.isInteger(asNum) && asNum >= 1 && asNum <= choices.length) {
958
+ return choices[asNum - 1].value;
959
+ }
960
+ const hit = choices.find(
961
+ (c) => c.value === raw || c.label.toLowerCase() === raw.toLowerCase()
962
+ );
963
+ return hit?.value ?? defaultValue;
964
+ }
965
+ async function promptText(rl, question, defaultValue) {
966
+ const raw = (await rl.question(`${question} (${defaultValue}): `)).trim();
967
+ return raw || defaultValue;
968
+ }
969
+ function assertGame(v) {
970
+ if (v === void 0) return void 0;
971
+ if (v === "cs2" || v === "none") return v;
972
+ throw new Error(`invalid --game ${JSON.stringify(v)} (expected cs2|none)`);
973
+ }
974
+ function assertInstall(v) {
975
+ if (v === void 0) return void 0;
976
+ if (v === "npm" || v === "pnpm" || v === "yarn" || v === "bun" || v === "none") return v;
977
+ throw new Error(`invalid --install ${JSON.stringify(v)} (expected npm|pnpm|yarn|bun|none)`);
978
+ }
979
+ function createPackageNames(game) {
980
+ if (game === "cs2") {
981
+ return ["sdk", "cs2"];
982
+ }
983
+ return ["sdk"];
984
+ }
985
+ function resolvePublishedVersion(pkg) {
986
+ const r = spawnSync("npm", ["view", pkg, "version"], { encoding: "utf8", timeout: 5e3 });
987
+ return versionSpecFrom(r.status, r.stdout);
988
+ }
989
+ function versionSpecFrom(status, stdout) {
990
+ const v = (stdout ?? "").trim();
991
+ return status === 0 && /^\d+\.\d+\.\d+/.test(v) ? `^${v}` : "latest";
992
+ }
993
+ function registryDevDeps(game, sdkVersion, resolve7 = resolvePublishedVersion) {
994
+ const deps = {};
995
+ for (const n of createPackageNames(game)) {
996
+ deps[`@s2script/${n}`] = n === "sdk" ? `^${sdkVersion}` : resolve7(`@s2script/${n}`);
997
+ }
998
+ return deps;
999
+ }
1000
+ function fileDevDeps(packagesDir, game) {
1001
+ const deps = {};
1002
+ for (const n of createPackageNames(game)) {
1003
+ const abs = join8(packagesDir, n);
1004
+ if (!existsSync4(join8(abs, "package.json"))) return void 0;
1005
+ deps[`@s2script/${n}`] = `file:${abs}`;
1006
+ }
1007
+ return deps;
1008
+ }
1009
+ function pluginSource(game) {
1010
+ if (game === "cs2") {
1011
+ return `import { Commands } from "@s2script/sdk/commands";
1012
+ import { Chat } from "@s2script/sdk/chat";
1013
+
1014
+ export function onLoad(): void {
1015
+ Commands.register("hello", (ctx) => {
1016
+ ctx.reply("hello from s2script");
1017
+ if (ctx.callerSlot >= 0) {
1018
+ Chat.toSlot(ctx.callerSlot, "hello from s2script");
1019
+ }
1020
+ });
1021
+ }
1022
+
1023
+ export function onUnload(): void {}
1024
+ `;
1025
+ }
1026
+ return `import { OnGameFrame } from "@s2script/sdk/frame";
1027
+ import { delay } from "@s2script/sdk/timers";
1028
+
1029
+ export function onLoad(): void {
1030
+ let n = 0;
1031
+ OnGameFrame.subscribe(() => {
1032
+ n += 1;
1033
+ });
1034
+ void delay(1000).then(() => {
1035
+ console.log("s2script plugin alive; frames so far:", n);
1036
+ });
1037
+ }
1038
+
1039
+ export function onUnload(): void {}
1040
+ `;
1041
+ }
1042
+ function tsconfigJson() {
1043
+ return JSON.stringify(
1044
+ {
1045
+ compilerOptions: {
1046
+ strict: true,
1047
+ noEmit: true,
1048
+ moduleResolution: "bundler",
1049
+ module: "ESNext",
1050
+ target: "ES2020",
1051
+ lib: ["ES2020"],
1052
+ types: [],
1053
+ skipLibCheck: true
1054
+ },
1055
+ include: ["src", "node_modules/@s2script/sdk/globals.d.ts"]
1056
+ },
1057
+ null,
1058
+ 2
1059
+ ) + "\n";
1060
+ }
1061
+ function packageJsonContent(name, game, version, localPackagesDir) {
1062
+ const fileDeps = localPackagesDir ? fileDevDeps(localPackagesDir, game) : void 0;
1063
+ const devDependencies = fileDeps ?? registryDevDeps(game, version);
1064
+ return JSON.stringify(
1065
+ {
1066
+ name,
1067
+ version: "0.1.0",
1068
+ private: true,
1069
+ main: "src/plugin.ts",
1070
+ scripts: {
1071
+ build: "s2s build ."
1072
+ },
1073
+ devDependencies,
1074
+ s2script: {
1075
+ apiVersion: "1.x"
1076
+ }
1077
+ },
1078
+ null,
1079
+ 2
1080
+ ) + "\n";
1081
+ }
1082
+ function gitignore() {
1083
+ return `node_modules/
1084
+ dist/
1085
+ *.s2sp
1086
+ .DS_Store
1087
+ `;
1088
+ }
1089
+ function runInstall(dir, pm) {
1090
+ if (pm === "none") return;
1091
+ const r = spawnSync(pm, ["install"], {
1092
+ cwd: dir,
1093
+ stdio: "inherit",
1094
+ shell: process.platform === "win32"
1095
+ });
1096
+ if (r.error) throw r.error;
1097
+ if (r.status !== 0) throw new Error(`${pm} install failed (exit ${r.status})`);
1098
+ }
1099
+ async function createPlugin(opts = {}) {
1100
+ const targetPath = resolve5(opts.path ?? ".");
1101
+ const interactive = Boolean(input.isTTY && output.isTTY && !opts.yes);
1102
+ let game = assertGame(opts.game);
1103
+ let name = opts.name;
1104
+ let install = opts.noInstall ? "none" : assertInstall(opts.install);
1105
+ const template = opts.template ?? "minimal";
1106
+ if (template !== "minimal") {
1107
+ throw new Error(`unknown template ${JSON.stringify(template)} (v1 supports: minimal)`);
1108
+ }
1109
+ if (existsSync4(targetPath)) {
1110
+ const kids = readdirSync2(targetPath);
1111
+ const meaningful = kids.filter((k) => k !== ".git");
1112
+ if (meaningful.length) {
1113
+ throw new Error(`target directory is not empty: ${targetPath}`);
1114
+ }
1115
+ }
1116
+ if (interactive) {
1117
+ const rl = createInterface({ input, output });
1118
+ try {
1119
+ if (!game) {
1120
+ game = await promptSelect(
1121
+ rl,
1122
+ "Which game?",
1123
+ [
1124
+ { value: "cs2", label: "Counter-Strike 2" },
1125
+ { value: "none", label: "Engine-generic only (no game package)" }
1126
+ ],
1127
+ "cs2"
1128
+ );
1129
+ }
1130
+ if (!name) {
1131
+ name = await promptText(rl, "Plugin package name", defaultNameFromPath(targetPath));
1132
+ }
1133
+ if (!install) {
1134
+ install = await promptSelect(
1135
+ rl,
1136
+ "Install dependencies?",
1137
+ [
1138
+ { value: "npm", label: "npm" },
1139
+ { value: "pnpm", label: "pnpm" },
1140
+ { value: "yarn", label: "yarn" },
1141
+ { value: "bun", label: "bun" },
1142
+ { value: "none", label: "skip" }
1143
+ ],
1144
+ "npm"
1145
+ );
1146
+ }
1147
+ } finally {
1148
+ rl.close();
1149
+ }
1150
+ }
1151
+ game = game ?? "cs2";
1152
+ name = name ?? defaultNameFromPath(targetPath);
1153
+ install = install ?? (opts.noInstall ? "none" : "npm");
1154
+ const version = readCliVersion();
1155
+ const localPackagesDir = findLocalPackagesDir();
1156
+ mkdirSync2(join8(targetPath, "src"), { recursive: true });
1157
+ writeFileSync5(
1158
+ join8(targetPath, "package.json"),
1159
+ packageJsonContent(name, game, version, localPackagesDir)
1160
+ );
1161
+ writeFileSync5(join8(targetPath, "tsconfig.json"), tsconfigJson());
1162
+ writeFileSync5(join8(targetPath, "src", "plugin.ts"), pluginSource(game));
1163
+ writeFileSync5(join8(targetPath, ".gitignore"), gitignore());
1164
+ let installed = false;
1165
+ if (install !== "none") {
1166
+ runInstall(targetPath, install);
1167
+ installed = true;
1168
+ }
1169
+ return {
1170
+ dir: targetPath,
1171
+ name,
1172
+ game,
1173
+ installed,
1174
+ skippedInstall: install === "none",
1175
+ packageManager: install
1176
+ };
1177
+ }
1178
+
1179
+ // src/cli.ts
1180
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
1181
+ import { dirname as dirname3, join as join9, resolve as resolve6 } from "node:path";
1182
+ var argv = process.argv.slice(2);
1183
+ var command = argv[0];
1184
+ function repoRootFromCli() {
1185
+ return join9(dirname3(fileURLToPath3(import.meta.url)), "..", "..", "..");
1186
+ }
1187
+ function parseFlag(args, name) {
1188
+ const eq = args.find((a) => a.startsWith(`${name}=`));
1189
+ if (eq) return eq.slice(name.length + 1);
1190
+ const i = args.indexOf(name);
1191
+ if (i >= 0 && args[i + 1] && !args[i + 1].startsWith("-")) return args[i + 1];
1192
+ return void 0;
1193
+ }
1194
+ function hasFlag(args, name) {
1195
+ return args.includes(name);
1196
+ }
1197
+ if (command === "gen-schema") {
1198
+ const repoRoot = repoRootFromCli();
1199
+ const check = argv[1] === "--check";
1200
+ const r = runGenSchema(repoRoot, { check });
1201
+ if (check) {
1202
+ if (r.drift.length) {
1203
+ console.error(`FAIL: generated files out of date \u2014 run \`s2s gen-schema\`:
1204
+ ${r.drift.join("\n ")}`);
1205
+ process.exit(1);
1206
+ }
1207
+ console.log(`schema codegen up to date (${r.classes} classes, ${r.fields} fields, ${r.skipped} skipped)`);
1208
+ } else {
1209
+ console.log(`gen-schema: wrote ${r.classes} classes, ${r.fields} fields (${r.skipped} skipped)`);
1210
+ }
1211
+ } else if (command === "gen-events") {
1212
+ const repoRoot = repoRootFromCli();
1213
+ const check = argv[1] === "--check";
1214
+ const r = runGenEvents(repoRoot, { check });
1215
+ if (check) {
1216
+ if (r.drift.length) {
1217
+ console.error(`FAIL: generated files out of date \u2014 run \`s2s gen-events\`:
1218
+ ${r.drift.join("\n ")}`);
1219
+ process.exit(1);
1220
+ }
1221
+ console.log(`event codegen up to date (${r.events} events)`);
1222
+ } else {
1223
+ console.log(`gen-events: wrote ${r.events} events`);
1224
+ }
1225
+ } else if (command === "gen-nav") {
1226
+ const repoRoot = repoRootFromCli();
1227
+ const check = argv[1] === "--check";
1228
+ const r = runGenNav(repoRoot, { check });
1229
+ if (check) {
1230
+ if (r.drift.length) {
1231
+ console.error(`FAIL: generated files out of date \u2014 run \`s2s gen-nav\`:
1232
+ ${r.drift.join("\n ")}`);
1233
+ process.exit(1);
1234
+ }
1235
+ console.log(`nav codegen up to date (${r.wrappers} wrappers, ${r.fields} fields)`);
1236
+ } else {
1237
+ console.log(`gen-nav: wrote ${r.wrappers} wrappers, ${r.fields} fields`);
1238
+ }
1239
+ } else if (command === "create") {
1240
+ const args = argv.slice(1);
1241
+ const pathArg = args.find((a) => !a.startsWith("-"));
1242
+ try {
1243
+ const result = await createPlugin({
1244
+ path: pathArg,
1245
+ name: parseFlag(args, "--name"),
1246
+ game: parseFlag(args, "--game"),
1247
+ template: parseFlag(args, "--template"),
1248
+ install: parseFlag(args, "--install"),
1249
+ noInstall: hasFlag(args, "--no-install"),
1250
+ yes: hasFlag(args, "--yes") || hasFlag(args, "-y")
1251
+ });
1252
+ console.log(`created ${result.dir}`);
1253
+ if (result.installed) console.log(`dependencies installed (${result.packageManager})`);
1254
+ else if (!result.skippedInstall) console.log(`next: cd ${result.dir} && npm install && npm run build`);
1255
+ else console.log(`next: cd ${result.dir} && npm run build`);
1256
+ } catch (e) {
1257
+ console.error(String(e instanceof Error ? e.message : e));
1258
+ process.exit(1);
1259
+ }
1260
+ } else if (command === "build" && argv[1]) {
1261
+ const args = argv.slice(1);
1262
+ const dir = args.find((a) => !a.startsWith("-"));
1263
+ const packagesDirFlag = parseFlag(args, "--packages-dir");
1264
+ try {
1265
+ const packagesDir = resolvePackagesDir({
1266
+ explicit: packagesDirFlag,
1267
+ pluginDir: resolve6(dir),
1268
+ fromCliUrl: import.meta.url
1269
+ });
1270
+ console.log(await buildPlugin(dir, packagesDir));
1271
+ } catch (e) {
1272
+ console.error(String(e instanceof Error ? e.message : e));
1273
+ process.exit(1);
1274
+ }
1275
+ } else {
1276
+ console.error(
1277
+ "Usage:\n s2s create [path] [--game cs2|none] [--name <pkg>] [--template minimal]\n [--install npm|pnpm|yarn|bun|none] [--no-install] [-y]\n s2s build <dir> [--packages-dir <path>]\n s2s gen-schema [--check]\n s2s gen-events [--check]\n s2s gen-nav [--check]"
1278
+ );
1279
+ process.exit(1);
1280
+ }