Getting started

The amxts command

amxts is the command a project runs: it creates projects and modules, adds modules, builds, deploys, type-checks and tests. It is the package @amxts/cli, which @amxts/core depends on, so every project has it and runs it through its package manager:

npx amxts <command>

The command works with the project's own core, the @amxts/core in its node_modules: a core older or newer than the command knows is an error that says which of the two to update. Installed globally (npm install -g @amxts/cli), amxts works the same way in any project, and init anywhere.

amxts --help lists the commands, amxts <command> --help shows a command's options with examples. A mistyped command gets a suggestion:

$ npx amxts buidl
✖ Unknown command buidl
  Did you mean amxts build?

An error is one line and a hint under it. --debug adds where it happened, for a bug report.

Commands

commandwhat it does
initcreates a project, or a module package with --module
devbuilds, deploys to the server and does it again on every save
buildbuilds the plugins and the modules into dist/
typecheckchecks the plugins and amxts.config.ts as the editor does
testruns the project's tests on a fake server
modulemodule add installs a module and lists it in the config; module list shows them
checkin a module package: checks it is ready to publish
preparegets the server's includes and writes .amxts/ for the editor
upgrademoves the project to another amxts release: the packages, the code, the build and the server
infoversions and settings, for a bug report

init

Creates a project. It asks, in this order: the package manager (the one that runs the command is picked), the folder, the modules (the modules catalog's), oxlint and oxfmt, git, the server's folder for dev and, when that server's includes do not say, which server the project is for. Then it writes the files, installs the dependencies, gets the server's includes and says what to run next:

npm create amxts@latest

npm create amxts and npx amxts init are the same command. What it asks looks like this:

┌  amxts 0.2.0 · a new project
│
◇  Which package manager?
│  npm
│
◇  Where should the project go?
│  my-server
│
◇  Which modules? (space to pick, enter to go on)
│  menu-core
│
◇  Add oxlint and oxfmt? (lint and format)
│  Yes
│
◇  Initialize a git repository?
│  Yes
│
◇  Where is the server? its addons/amxts folder, for npm run dev - empty to skip
│  D:/hlds/cstrike/addons/amxts
│
◆  Created my-server in my-server
│
●  config-core is listed too: menu-core needs it
│
◇  Installed dependencies with npm 11.16.0
│
◆  Includes: the server's own (D:/hlds/cstrike/addons/amxmodx/scripting/include)
│
◆  Initialized a git repository
│
◆  Prepared the editor config (.amxts/tsconfig.json)
│
◇  Next steps ──────────────────────────────────────╮
│                                                   │
│  cd my-server                                     │
│  npm run dev    # build, deploy, rebuild on save  │
│                                                   │
├───────────────────────────────────────────────────╯
│
└  Docs: https://amxts.github.io/docs/getting-started/quick-start

Every question is also a flag, and --yes takes the defaults for the rest - for a script or CI:

npx amxts init my-server --modules menu-core,config-core --no-git --server D:/hlds/cstrike/addons/amxts --yes
optionwhat it does
[folder]where the project goes; my-server by default
--pm <npm|pnpm|yarn|bun>the package manager; the one running the command by default
--modules <list>the modules, comma-separated, by their names in the catalog or as npm packages: menu-core,config-core; "" for none
--no-lintno oxlint and oxfmt (they are added by default)
--no-gitno git init
--server <path>the server: its folder, its cstrike or its addons/amxts; the addons/amxts is written to .env as AMXTS_SERVER, and the build takes the includes installed there
--target <rehlds|hlds>the server the project is for, when no server's includes say it: rehlds - ReHLDS, ReGameDLL and ReAPI (by default) - or hlds, plain HLDS
--os <windows|linux>the server's system, when its folder does not show it: written to .env as AMXTS_SERVER_OS (the server's system)
--no-installwrite the files, do not install
--yes, -yask nothing
--forcewrite into a folder that is not empty
--locallink the core and the official modules from their folders on this machine instead of installing them from npm, for working on amxts itself; AMXTS_CORE names the core's folder

A module that needs another brings it: with menu-core, config-core is installed too, and modules lists it right after menu-core, with a comment that says which module needs it. The build loads it first:

// amxts.config.ts
export default 
defineConfig
({
modules
: [
"@amxts/menu-core", "@amxts/config-core", // needed by menu-core ],
target
: "rehlds",
});

The project it writes:

my-server/
├── package.json        scripts: dev, build, typecheck, test, lint
├── amxts.config.ts     the modules you picked, and the ones they need
├── tsconfig.json       { "extends": "./.amxts/tsconfig.json" }
├── .oxlintrc.json      the lint rules: @antfu/eslint-config's, run by oxlint
├── .oxfmtrc.json       oxfmt, for the JSON and YAML files
├── knip.json           entry points for knip: plugins, the config, the tests
├── .env                AMXTS_SERVER, when you gave the folder; AMXTS_SERVER_OS
├── pnpm-workspace.yaml with pnpm: bun's install script is not needed
├── .gitignore
├── .gitattributes      LF for the files oxfmt formats
├── .vscode/            settings and the recommended extensions
├── README.md
├── plugins/
│   └── hello.ts        a plugin that uses the modules you picked
└── test/
    ├── hello.test.ts   its tests, on the fake server
    └── tsconfig.json   the tests' own: Bun's types, not the plugins'

The server's includes

Where a plugin needs a Pawn include - an include it implements, a Pawn plugin's natives or forwards, a Pawn test - the build looks in the project's includes/ first, then in the server's includes, then in the ones amxts ships (AMX Mod X's own among them). The server's includes are:

  • with AMXTS_SERVER, the server's own addons/amxmodx/scripting/include, exactly what is installed there;
  • without a server, the includes of the server target in amxts.config.ts names: for "rehlds" - ReHLDS, ReGameDLL and ReAPI, the default - amxts downloads ReAPI's includes into .amxts/include, the release its API is made from; "hlds" - plain HLDS - needs nothing more than AMX Mod X's own.
// amxts.config.ts
export default 
defineConfig
({
modules
: [],
target
: "hlds",
});

init asks the target when there is no server, or the server's folder has no includes; --target answers it. prepare - after every install - and every build fetch the includes when they are not there yet.

Without a connection the includes cannot be downloaded: the command says so and goes on, and a build stops only where a plugin names an include that is not there. Connect and run npx amxts prepare, or point AMXTS_SERVER at a server with its includes.

lint checks the code with oxlint, by @antfu/eslint-config's rules, and the JSON and YAML files with oxfmt; lint:fix fixes what they can - those rules format TypeScript too. In VS Code the oxc extension, which .vscode/extensions.json recommends, does the same as you type and save.

A module package

--module creates a module package instead - a working module with its playground project and a test - see creating a module:

npx amxts init --module @you/greeter               # ./greeter
npx amxts init --module @you/greeter --natives     # with natives for Pawn plugins
npx amxts init --module greeter --dir my-greeter   # into ./my-greeter

dev

npx amxts dev

Builds every plugin and module, copies them to the server in AMXTS_SERVER, reloads it, then watches the plugins folder and the project's modules: a save rebuilds the plugins that import the saved file.

amxts 0.2.0 · dev

  Core      @amxts/core 0.2.0
  Modules   menu-core 0.2.0 · config-core 0.1.1
  Server    D:/hlds/cstrike/addons/amxts · Windows · ReHLDS · running
  Plugins   1 in plugins/

✔ hello · 6.2s
✔ 14:02:11 deployed, the server reloaded config-core, menu-core, hello (7.1s)
◇ watching plugins - save a plugin and it goes to the server (Ctrl+C stops)
✔ hello · 4.1s
✔ 14:03:40 plugins/hello.ts changed: deployed, the server reloaded hello (4.2s)

It opens with what it works with: the core and the modules with their versions - one no plugin uses says so (no plugin uses it), one from a folder on this machine rather than from npm is local - then the server - its folder, its system (below), ReHLDS or plain HLDS, and whether it is running - and how many plugins there are.

Each plugin and module is shown as it compiles: ◇ compiling hello 1/1 while it does, ✔ hello · 6.2s when it is done - in a terminal the one line turns into the other, in a log (CI) both stay. A plugin goes through two compilers, to WebAssembly and then to machine code, and that is what a build spends its time on. So:

  • the official modules come compiled for Windows and Linux in their npm packages, and a build takes them as they came, with nothing to compile. When the project would compile one differently - its options set in amxts.config.ts, a core of another version - the build says why (menu-core 0.2.0 was built for @amxts/core 0.2.0, the project has 0.2.1 - compiling it here) and compiles it, once. A plugin that uses a module compiles against what the module came with, too, rather than reading the module through first;
  • a plugin nothing has changed for since the last build - not its file, not what it imports - is not compiled again: it is kept from before in node_modules/.cache/amxts. A dev started again goes to the server in seconds;
  • several plugins compile at once, each in a process of its own: as many as fit in 3 GB of memory - a compile takes about one - and no more than the machine has processor cores. In a terminal the line names them all: ◇ compiling hello, shop 2/3. AMXTS_BUILD_MEMORY in .env or the environment sets the memory in megabytes (AMXTS_BUILD_MEMORY=6144), AMXTS_BUILD_JOBS the number of compiles outright (1: one at a time). A plugin comes out the same either way.

A save rebuilds only when the file holds something new - an editor that saves a file as it was, or a package manager that writes a module again, is not a change - and the line names the file.

dev compiles for speed: a small plugin in a few seconds, where build takes about ten. Its plugins do the same, a little slower - the server people play on gets npx amxts build --deploy.

AMXTS_SERVER is the addons/amxts folder of a server with amxts (installing on a server), set in the project's .env:

AMXTS_SERVER=D:/hlds/cstrike/addons/amxts

It may also name the server's own folder (D:/hlds), its cstrike or cstrike/addons: the build takes cstrike/addons/amxts under it, and the Server line says which folder that is. A folder that is none of them stops the build with one line naming where it looked.

Without it dev and build --deploy ask for the folder, as init does, and write it to .env. In CI, or with the output in a pipe, nothing is asked: the build stops and says AMXTS_SERVER is not set.

When the server's module is of another release than the project's core, dev and build say so first - the plugins a project builds load only on the module of its own release:

▲ the server runs amxts 0.1.0, this project 0.2.0 - run amxts upgrade

upgrade updates the server too. The release is read from the module file, not from the running server.

The reload goes over rcon to 127.0.0.1:27015 (AMXTS_PORT sets another port), with rcon_password from the server's cstrike/server.cfg. See hot reload. --os is as for build.

npx amxts dev --docker

The same for the server in Docker that mounts the project: it builds for Linux into dist/ and again on every save, and deploys nothing - the server reads dist/ and reloads a plugin whose file changed - and it shows the amxts lines of that server's console under the build. AMXTS_SERVER is not used.

build

npx amxts build              # dist/: the .aot files and plugins.ini
npx amxts build --deploy     # and copies them to AMXTS_SERVER, reloading the server
npx amxts build --os linux   # for a Linux server
npx amxts build --watch      # and again on every save, without deploying

plugins.ini lists the modules first, each after what it requires, then the project's plugins. A module no plugin uses is left out, unless pawn keeps it (only what is used is built). outDir in amxts.config.ts changes where it writes. A plugin nothing has changed for since the last build is taken as it was built, an official module as it came (dev); node_modules/.cache/amxts removed, every one compiles again.

The server's system

A plugin is compiled to machine code for its server's system, Windows or Linux, and a .aot built for one does not load on the other. The build takes the system from, first to last:

  1. --os windows or --os linux on build or dev;
  2. AMXTS_SERVER_OS in the environment or .env;
  3. the AMXTS_SERVER folder: hlds_linux or hlds.exe beside the game folder, else the files in its addons/amxmodx/modules;
  4. the system of the machine the build runs on.

The Server line of dev and build says which: Linux. So a project built on Windows deploys to a Linux server as it is, once AMXTS_SERVER points at it - a mounted share, say - or .env says AMXTS_SERVER_OS=linux. init asks when the server's folder does not show it, and info shows what the build would pick.

A plugin for the other system
A .aot built for the other system does not load, and the console says so: was compiled for a Linux server, and this one runs Windows (or the other way). A .ts the server compiles itself is always built for it.

typecheck

npx amxts typecheck

Checks the plugins and amxts.config.ts with TypeScript, as the editor does: a misspelled option of a module, a wrong event name, a field that is not there.

test

npx amxts test           # every test in the project
npx amxts test hello     # the files whose name has "hello"
npx amxts test --watch   # again on every save

Runs bun test: its options go to it as they are. A test puts the project on a fake server with setup() - see testing.

module

npx amxts module add menu-core      # @amxts/menu-core
npx amxts module add @you/greeter   # any module package from npm
npx amxts module add ../greeter     # a module in a folder
npx amxts module list

module add installs the package with the project's package manager (found by its lockfile) and adds it to modules in amxts.config.ts, followed by the modules it needs that the list does not have yet, each with a // needed by menu-core comment - only the list changes, the rest of the file stays as you wrote it. A short name is a module's name in the modules catalog: menu-core is @amxts/menu-core. A package the catalog does not list is installed from npm all the same, with a warning; a package that is not an amxts module is installed but not added to the config. A module from npm comes at its newest version that works with the project's core. The command reads the catalog from the registry on GitHub, and offline takes the copy it was published with.

optionwhat it does
--skip-installonly add it to amxts.config.ts
--skip-configonly install it
--localan official module from its folder on this machine, linked, for working on amxts itself

module list shows the modules in the config and whether each is installed, the installed ones the config does not list, and the catalog's modules the project does not have yet - a community module marked (community):

In amxts.config.ts
  ✔ @amxts/menu-core    0.2.0  An opinionated way to create menus
  ✔ @amxts/config-core  0.1.1  Configs in INI, YAML or JSON, read into typed objects and written back

In the amxts catalog
  ○ resemiclip          Semiclip over the ReSemiclip module - who walks through whom, as a rule over two players - npx amxts module add resemiclip
  ○ ftp                 FTP, FTPS and SFTP - upload, download and list files on another server, every call a promise - npx amxts module add ftp

check

npx amxts check

In a module's folder, before publishing it: its amxts field points at files that are there, the module file has defineModule with meta.name, README.md and LICENSE are there, and include/<name>.inc is what the build writes from the natives. It writes nothing. See creating a module.

prepare

npx amxts prepare

Gets the server's includes when they are not there yet, and writes what the editor reads into .amxts/:

  • .amxts/tsconfig.json, which the project's tsconfig.json extends: ~/ is the plugins folder, @amxts/core and its entries are the core's API, the modules are found by their names, and amxts.config.ts is typed from them;
  • .amxts/imports.d.ts: what plugins use without an import, as globals for the editor (auto-imports);
  • .amxts/api/, only when AMXTS_DOCS_LANG picks a language other than English: a copy of the core's API and the modules' with the tooltips in that language, which the editor reads instead of the installed packages. The packages in node_modules are never changed, and the build compiles them, so the plugins come out the same in every language.

build, dev and typecheck do it first; init does it after the install. .amxts/ is the command's own: it is in .gitignore, and written again whenever it is out of date.

upgrade since v0.2

npx amxts upgrade              # to the latest release
npx amxts upgrade --to 0.2.0   # to a release of your choice
npx amxts upgrade --dry-run    # what it would change, changing nothing

::: warning The command first upgrade is a command of @amxts/cli, and a project runs the version of it its core came with, or the one its own package.json names. A project whose command has no upgrade yet adds the newest command first, with its package manager:

npm i @amxts/cli@latest   # or: bun add @amxts/cli@latest, pnpm add @amxts/cli@latest, yarn add @amxts/cli@latest
npx amxts upgrade

:::

Moves the project to another amxts release, in one go:

  1. The packages. @amxts/core goes to the latest version on the registry, or to --to. Every other @amxts/ package in package.json - the official modules, the command - has a version of its own, and goes to its newest version that works with that core: a module names the cores it works with in its peerDependencies. Each is written the way the project writes it: ^0.1.0 becomes ^0.2.0, 0.1.0 becomes 0.2.0. A package from a folder (file:) stays as it is; a module with no version for that core yet stops the upgrade before anything changes. The project's package manager - the one its lockfile names - installs them from the registry npm is set to (.npmrc, NPM_CONFIG_REGISTRY).
  2. The code, rewritten to the new API by the new core, every change listed: the code, below.
  3. The build, as amxts build does it.
  4. The server in AMXTS_SERVER: its module - and the compiler for .ts written on the server, where it has one - from the GitHub Release of the new version, each file checked against the sha256 the release lists. The file it replaces stays beside it with its version: amxts_amxx.dll.0.1.0. A line amxts_host.amxx in AMX Mod X's plugins.ini goes: the module loads its host plugin itself. With no AMXTS_SERVER - a server in Docker - it says which image to pull.
  5. A summary: the versions, the files rewritten, what to check by hand and what happened to the server.
amxts 0.2.0 · upgrade
◇ Packages: amxts 0.2.0 (latest)
i @amxts/core  ^0.1.0 → ^0.2.0
i @amxts/cli  ^0.1.0 → ^0.2.0
i @amxts/menu-core  ^0.1.0 → ^0.2.0
i @amxts/config-core  ^0.1.0 → ^0.1.1
◇ Installing with npm
✔ Installed with npm
◇ Code
  plugins/hello.ts:1  ~/natives → @amxts/core/natives
  plugins/hello.ts:9  player.account → player.money
◇ Build
  ...
✔ built hello into dist (11.4s)
◇ Server: D:/hlds/cstrike (windows)
✔ amxts_amxx.dll 0.1.0 → 0.2.0 (the old one: amxts_amxx.dll.0.1.0)
✔ Took amxts_host.amxx out of D:/hlds/cstrike/addons/amxmodx/configs/plugins.ini: the module loads it itself

Upgraded to amxts 0.2.0
  @amxts/core         0.1.0 → 0.2.0
  @amxts/cli          0.1.0 → 0.2.0
  @amxts/menu-core    0.1.0 → 0.2.0
  @amxts/config-core  0.1.0 → 0.1.1
  ✔ rewritten: plugins/hello.ts
  ▲ to check by hand:
      plugins/shop.ts:14  it reads the words after the name: write them in the usage, "/give <amount>", and take them by name, ({ player, amount })
  ✔ built
  ✔ server: amxts_amxx.dll 0.1.0 → 0.2.0 - restart the server to load it
optionwhat it does
--to <version>the release to move to, instead of the latest
--no-serverleave the server as it is
--server-onlyonly the server's step - for a server that was running
--dry-runprint what it would do: nothing is installed or written, and of the release only its list of files is read

A dry run lists the rewrites only when the project has that version of the core already: before the install, the new core is not there to ask.

The command a project runs is the one its core came with. An amxts of an older release installs the new packages, then hands the rest - the code, the build, the server - to the project's new one.

A running server
A Windows server holds its module while it runs, and the file cannot be replaced then. upgrade changes nothing on the server, says so, and does the rest: stop the server, then run npx amxts upgrade --server-only. Either system loads the new module on its next start.

The code

The new core brings the project's code to its API, and lists every line it changed. An import of the core's API by ~/ becomes one by the package's name, and one of a module package by its place in the build (~/modules/menu-core) one by the module's name:

import { 
user_slap
} from "~/natives"; // before
import {
user_slap
} from "@amxts/core/natives"; // after

It reads the code with the TypeScript parser: imports, exports, import() and declare module change; a string or a comment that only looks like an import does not. Your own files under ~/ stay as they are. A second run changes nothing. It goes through every .ts file of the project - the plugins, the tests, a module's sources - but not node_modules and dist/.

A command's handler gets one object (commands): (player) => becomes ({ player }) =>, and a function passed by its name and taking the player is called from ({ player }) => name(player); (player, args) => that never reads args becomes ({ player }) => too. A handler that reads the words after the command's name is listed, with its line, to be written by hand: the words go into the usage, "/give <amount>", and the handler takes them by name.

menu-core's code is brought to the menu's context (menus): addItem("text", options) becomes addItem({ title: "text", ... }), and so does addFixedItem; (player) => and (player, target) => become ({ player }) => and ({ player, target }) => - in an item, a menu's options, a filter, a list source and what is registered by name. A function passed by its name and declared in the file is called from an arrow that hands it what it took. Listed to change by hand: a function declared in another file, a menu's name read as text, a target given as a number, and menus.runActions, which is a menu's method (menu.runActions).

The names follow the API: Player.all() becomes server.players, and its options a filter - Player.all({ alive: true, team: "CT" }) is server.players.filter(player => player.isAlive && player.team === "CT"); where the code has a player already, as a command's handler does, the filter names its player other (or p), so it hides none. A field, a method or a game event named in the engine's words gets the player's: player.account becomes player.money, player.authidplayer.steamId, game.numCtWins game.ctWins, weapon.inReloadweapon.isReloading, the event restartRound newRound, and an event's class follows its name. A value is taken for a player, a weapon or the game where the file says it is one - event.player, a Player, Client or Weapon annotation, { player } from an event or a command, the first parameter of a command's handler, an element of server.players, player.activeItem, game. Listed to change by hand: an old name on any other value, options that are not written out, and a field or an event the API does not have, which the natives in @amxts/core/natives still reach (get_member, RegisterHookChain). A test's player joins with steamId too: server.join("Alice", { authid }) becomes server.join("Alice", { steamId: authid }).

A server event is named in the author's words (server events): server.addEventListener("putinserver", ...) becomes server.addEventListener("putInServer", ...), "cfg" "pluginsLoaded", "kill" "suicide", and a forward's own name ("client_putinserver") the event's. One that is a game event is heard through game: server.addEventListener("PreThink", ...) becomes game.addEventListener("preThink", ...), and so do "PostThink", "pfnTouch" ("touch") and "infochanged" ("userInfoChange"). A field of an event named after its Pawn parameter gets the author's word where the file says which event it is - in a listener, or a parameter with the event's class: event.weapon_entity becomes event.weapon, event.authidevent.steamId, event.tracehandle event.trace, event.infobufferevent.info, and ({ dir }) becomes ({ direction: dir }). Listed to change by hand: cstrike's "CS_OnBuy" and "CS_OnBuyAttempt", which are the game events buyWeapon, buyItem, buyAmmo and itemRestricted.

A game message is heard through its own method, by its name in the player's words (messages): server.addEventListener("message:DeathMsg", ...) becomes server.addMessageListener("death", ...), and removeEventListenerremoveMessageListener. A message name the game does not have is listed.

A flag's name is lowerCamelCase (flags): player.buttons.includes("Jump") becomes player.buttons.includes("jump"), { access: "Kick" } { access: "kick" }. A string is taken for a flag where the file says it is one: assigned to a flag property or the buttons and access options, given to includes, push or concat of one or to player.screen.hideHud, compared with an element of one - in its filter, a for of, a switch - or held by a name of a flag type (Button, HideHud[]). A name given where flags go is followed to the list it was declared with; one the file does not say - a parameter without a type, an import, a function's result - is listed.

info

npx amxts info

What a bug report needs: the operating system, Node, Bun, the package manager, the command, the core and its compilers, the project's modules, AMXTS_SERVER and the system the plugins are compiled for.

amxts info - paste this into a bug report

  Operating system  Windows 10.0.19045 (x64)
  Node.js           24.18.0
  Bun               1.4.2
  Package manager   npm 11.16.0
  @amxts/cli        0.2.0
  @amxts/core       0.2.0
  AssemblyScript    0.28.20 + amxts patch
  WAMR              2.4.5 + amxts patch, wamrc found
  Project           my-server
  Modules           @amxts/menu-core 0.2.0
  AMXTS_SERVER      D:/hlds/cstrike/addons/amxts
  Server system     Windows (hlds.exe)
  AMXTS_DOCS_LANG   en