@amxts/cli 0.1.0 → 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/README.md +1 -0
- package/package.json +1 -1
- package/src/catalog.mjs +1 -1
- package/src/core.mjs +37 -12
- package/src/includes.mjs +2 -1
- package/src/info.mjs +10 -1
- package/src/init-module.mjs +1 -1
- package/src/init.mjs +34 -6
- package/src/main.mjs +28 -2
- package/src/module.mjs +7 -3
- package/src/pm.mjs +63 -6
- package/src/registry.mjs +83 -0
- package/src/server-update.mjs +173 -0
- package/src/server.mjs +56 -10
- package/src/upgrade.mjs +266 -0
- package/templates/module/README.md +1 -1
- package/templates/module/README.ru.md +1 -1
- package/templates/module/playground/plugins/welcome.ts +1 -1
- package/templates/project/README.md +2 -1
- package/templates/project/plugins/hello.ts +32 -20
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ its package manager:
|
|
|
11
11
|
npx amxts --help
|
|
12
12
|
npx amxts dev # build, deploy, again on every save
|
|
13
13
|
npx amxts module add menu-core
|
|
14
|
+
npx amxts upgrade # the latest amxts: packages, code, build, server
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
A new project: `npm create amxts@latest`. Installed globally
|
package/package.json
CHANGED
package/src/catalog.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// ../greeter, file:../x the package in that folder
|
|
9
9
|
// menu-core --local the folder the core on this machine takes an
|
|
10
10
|
// official module from (its package.json's
|
|
11
|
-
// `file:` link)
|
|
11
|
+
// `file:` link), for working on them
|
|
12
12
|
import { spawnSync } from 'node:child_process';
|
|
13
13
|
import { existsSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
package/src/core.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { createRequire } from 'node:module';
|
|
|
15
15
|
import { dirname, join, resolve } from 'node:path';
|
|
16
16
|
import process from 'node:process';
|
|
17
17
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
18
|
-
import { addArgs, commandLine, detectPackageManager, versionOf } from './pm.mjs';
|
|
18
|
+
import { addArgs, commandLine, detectPackageManager, execAmxts, versionOf } from './pm.mjs';
|
|
19
19
|
import { CliError, debug } from './ui.mjs';
|
|
20
20
|
|
|
21
21
|
/** The command's own package folder: packages/cli in its repository. */
|
|
@@ -24,14 +24,15 @@ export const CLI_PACKAGE = readJson(join(CLI_DIR, 'package.json'));
|
|
|
24
24
|
export const VERSION = String(CLI_PACKAGE.version);
|
|
25
25
|
|
|
26
26
|
/** The cores this command works with: the range a new project asks for. */
|
|
27
|
-
export const CORE_RANGE = '^0.
|
|
27
|
+
export const CORE_RANGE = '^0.2.0';
|
|
28
28
|
/** The version of `@amxts/core/cli-api` this command speaks. */
|
|
29
|
-
export const CLI_API =
|
|
29
|
+
export const CLI_API = 5;
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Run from a checkout of the command's repository (npm link, a clone) rather
|
|
33
33
|
* than from npm: then a new project takes the core and the official modules
|
|
34
|
-
* from this machine by default, as --local says
|
|
34
|
+
* from this machine by default, as --local says: the checkout is for working
|
|
35
|
+
* on them together.
|
|
35
36
|
*/
|
|
36
37
|
export const FROM_SOURCE = existsSync(join(CLI_DIR, '..', '..', '.git')) && readJson(join(CLI_DIR, '..', '..', 'package.json'))?.name === 'amxts-cli';
|
|
37
38
|
|
|
@@ -85,14 +86,38 @@ function parts(version) {
|
|
|
85
86
|
return String(version).split('-')[0].split('.').map(each => Number.parseInt(each, 10) || 0);
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
/**
|
|
89
|
+
/** Two versions compared: below 0 when `a` comes first. */
|
|
90
|
+
export function compareVersions(a, b) {
|
|
91
|
+
const [x, y] = [parts(a), parts(b)];
|
|
92
|
+
return x[0] - y[0] || x[1] - y[1] || x[2] - y[2];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* One comparator of a range as a test of a version: `^0.2.0` (0.2.x from
|
|
97
|
+
* 0.2.0 up - below 1.0 the minor is the breaking number), `~0.2.1`, `>=0.2.0`,
|
|
98
|
+
* `<1.0.0`, `0.2.0`, `*`.
|
|
99
|
+
*/
|
|
100
|
+
function comparator(text) {
|
|
101
|
+
const [, op = '=', want = '*'] = /^([<>]=?|[\^~=])?v?(.*)$/.exec(text);
|
|
102
|
+
if (/^[*x]?$/i.test(want)) return () => true;
|
|
103
|
+
const [major, minor, patch] = parts(want);
|
|
104
|
+
const upper = op === '^' ? (major ? `${major + 1}.0.0` : minor ? `0.${minor + 1}.0` : `0.0.${patch + 1}`) : `${major}.${minor + 1}.0`;
|
|
105
|
+
const within = version => compareVersions(version, want) >= 0 && compareVersions(version, upper) < 0;
|
|
106
|
+
const tests = {
|
|
107
|
+
'^': within,
|
|
108
|
+
'~': within,
|
|
109
|
+
'>=': version => compareVersions(version, want) >= 0,
|
|
110
|
+
'<=': version => compareVersions(version, want) <= 0,
|
|
111
|
+
'>': version => compareVersions(version, want) > 0,
|
|
112
|
+
'<': version => compareVersions(version, want) < 0,
|
|
113
|
+
'=': version => compareVersions(version, want) === 0,
|
|
114
|
+
};
|
|
115
|
+
return tests[op];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Whether a range takes a version, as npm reads one: `^0.2.0`, `>=0.2.0 <0.4.0`, `^0.2.0 || ^0.3.0`. */
|
|
89
119
|
export function satisfies(version, range = CORE_RANGE) {
|
|
90
|
-
|
|
91
|
-
const [wantMajor, wantMinor, wantPatch] = parts(range.replace(/^\^/, ''));
|
|
92
|
-
if (major !== wantMajor) return false;
|
|
93
|
-
// Below 1.0 the minor is the breaking number: ^0.1.0 does not take 0.2.0.
|
|
94
|
-
if (major === 0 && minor !== wantMinor) return false;
|
|
95
|
-
return minor > wantMinor || (minor === wantMinor && patch >= wantPatch);
|
|
120
|
+
return range.split('||').some(alternative => alternative.trim().split(/\s+/).every(each => comparator(each)(version)));
|
|
96
121
|
}
|
|
97
122
|
|
|
98
123
|
/** Whether a version is past a caret range, not below it: its major, or below 1.0 its minor, is the higher. */
|
|
@@ -107,7 +132,7 @@ function updateHint(coreIsNewer) {
|
|
|
107
132
|
const pm = detectPackageManager();
|
|
108
133
|
return coreIsNewer
|
|
109
134
|
? `Update the command: npm install -g @amxts/cli@latest - a project's own ${pm === 'npm' ? 'npx amxts' : `${pm} amxts`} comes with its core`
|
|
110
|
-
: `
|
|
135
|
+
: `Move the project to amxts ${VERSION}: ${execAmxts(pm)} upgrade`;
|
|
111
136
|
}
|
|
112
137
|
|
|
113
138
|
/**
|
package/src/includes.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import process from 'node:process';
|
|
|
23
23
|
import { inflateRawSync } from 'node:zlib';
|
|
24
24
|
import { readConfigTarget } from './config.mjs';
|
|
25
25
|
import { findProject, runTaskOrExit, setting } from './core.mjs';
|
|
26
|
+
import { serverFolderOrNull } from './server.mjs';
|
|
26
27
|
import { c, CliError, log } from './ui.mjs';
|
|
27
28
|
|
|
28
29
|
/** The servers a project can be for, as `target` in amxts.config.ts and `--target` name them. */
|
|
@@ -132,7 +133,7 @@ export async function fetchIncludes(source) {
|
|
|
132
133
|
* @returns {Promise<Includes>} where the includes are
|
|
133
134
|
*/
|
|
134
135
|
export async function ensureIncludes(core, root, options = {}) {
|
|
135
|
-
const own = serverIncludes(setting(root, 'AMXTS_SERVER'));
|
|
136
|
+
const own = serverIncludes(serverFolderOrNull(setting(root, 'AMXTS_SERVER')));
|
|
136
137
|
if (own) return { from: 'server', target: targetOf(own), dir: own };
|
|
137
138
|
|
|
138
139
|
const target = options.target ?? (await readConfigTarget(root)) ?? 'rehlds';
|
package/src/info.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import process from 'node:process';
|
|
|
8
8
|
import { readConfigModules } from './config.mjs';
|
|
9
9
|
import { bunFor, bunVersion, CLI_DIR, installedCore, readJson, setting, VERSION } from './core.mjs';
|
|
10
10
|
import { detectPackageManager, versionOf } from './pm.mjs';
|
|
11
|
+
import { serverFolderOrNull } from './server.mjs';
|
|
11
12
|
import { c, CliError } from './ui.mjs';
|
|
12
13
|
|
|
13
14
|
/** The project's core, or why it cannot be used: not installed, or not one this command works with. */
|
|
@@ -21,6 +22,14 @@ async function core(dir) {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/** After AMXTS_SERVER: the addons/amxts it names when it is a folder above it, or why it is no server. */
|
|
26
|
+
function serverNote(server) {
|
|
27
|
+
const folder = serverFolderOrNull(server);
|
|
28
|
+
if (folder === null) return c.red(' (not a server)');
|
|
29
|
+
const shown = folder === server ? '' : ` - ${folder.replace(/\\/g, '/')}`;
|
|
30
|
+
return existsSync(folder) ? c.dim(shown) : c.red(`${shown} (not found)`);
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
/**
|
|
25
34
|
* The system the build compiles for, as the core decides it from .env and the
|
|
26
35
|
* server's folder: "Linux (hlds_linux)". A core without serverSystem() builds
|
|
@@ -73,7 +82,7 @@ export async function info() {
|
|
|
73
82
|
const version = readJson(join(dir, 'node_modules', name, 'package.json'))?.version;
|
|
74
83
|
return version ? `${name} ${version}` : `${name} ${c.red('(not installed)')}`;
|
|
75
84
|
}).join(', ')],
|
|
76
|
-
['AMXTS_SERVER', server ? `${server}${
|
|
85
|
+
['AMXTS_SERVER', server ? `${server}${serverNote(server)}` : c.dim('not set')],
|
|
77
86
|
['Server system', system],
|
|
78
87
|
['AMXTS_DOCS_LANG', setting(dir, 'AMXTS_DOCS_LANG') || c.dim('en')],
|
|
79
88
|
];
|
package/src/init-module.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// user.name) as {{placeholders}}; `// #if natives` blocks are kept only with
|
|
11
11
|
// --natives. The core is a peer dependency, the range this command works with;
|
|
12
12
|
// with --local (the default from a checkout) it is also a dev dependency
|
|
13
|
-
// linked to the core on this machine,
|
|
13
|
+
// linked to the core on this machine, which it is developed against.
|
|
14
14
|
import { existsSync, readdirSync } from 'node:fs';
|
|
15
15
|
import { join, relative, resolve } from 'node:path';
|
|
16
16
|
import process from 'node:process';
|
package/src/init.mjs
CHANGED
|
@@ -18,15 +18,22 @@ import { modulesList } from './config.mjs';
|
|
|
18
18
|
import { bunFor, CLI_DIR, CORE_RANGE, coreDirFrom, FROM_SOURCE, loadCore, needLocalCore, readJson, VERSION } from './core.mjs';
|
|
19
19
|
import { ensureIncludes, FETCHED, serverIncludes, targetOf, TARGETS } from './includes.mjs';
|
|
20
20
|
import { LINT_DEPENDENCIES, LINT_FILES, LINT_SCRIPTS } from './lint.mjs';
|
|
21
|
-
import { commandLine, execAmxts, installArgs, PACKAGE_MANAGERS, packageManagerOfAgent, run, runScript, versionOf } from './pm.mjs';
|
|
22
|
-
import {
|
|
21
|
+
import { commandLine, execAmxts, installArgs, installLinked, linkedFolders, PACKAGE_MANAGERS, packageManagerOfAgent, run, runScript, versionOf } from './pm.mjs';
|
|
22
|
+
import { npmRegistry, versionsFor } from './registry.mjs';
|
|
23
|
+
import { askServer, canAsk, missingServer, serverEnv, serverFolder } from './server.mjs';
|
|
23
24
|
import { HOST_SYSTEM, serverSystemOf, SYSTEMS } from './system.mjs';
|
|
24
25
|
import { copyTemplate, gitAuthor, writeFile } from './template.mjs';
|
|
25
26
|
import { c, CliError } from './ui.mjs';
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* The tools a project gets besides the core and its modules. knip reads the
|
|
30
|
+
* knip.json the template writes; from 6 on it counts a package imported by
|
|
31
|
+
* its name as used wherever the name resolves, so a core and modules linked
|
|
32
|
+
* from this machine's folders (--local) are not called unused.
|
|
33
|
+
*/
|
|
28
34
|
const TOOLS = {
|
|
29
35
|
'@types/bun': '^1.4.2',
|
|
36
|
+
'knip': '^6.39.0',
|
|
30
37
|
};
|
|
31
38
|
|
|
32
39
|
const DEFAULT_DIR = 'my-server';
|
|
@@ -144,7 +151,8 @@ export async function initProject(options) {
|
|
|
144
151
|
const git = hasGit && (options.git ?? (interactive
|
|
145
152
|
? answer(await p.confirm({ message: 'Initialize a git repository?', initialValue: true }))
|
|
146
153
|
: true));
|
|
147
|
-
|
|
154
|
+
// Its folder, cstrike or addons/amxts: .env keeps the addons/amxts.
|
|
155
|
+
const server = options.server === undefined ? (interactive ? answer(await askServer(pm)) : '') : serverFolder(options.server);
|
|
148
156
|
|
|
149
157
|
// 7. Which server the project is for: its includes say, when it has them
|
|
150
158
|
// (addons/amxmodx/scripting/include beside AMXTS_SERVER); else asked.
|
|
@@ -196,9 +204,10 @@ export async function initProject(options) {
|
|
|
196
204
|
const written = copyTemplate(join(CLI_DIR, 'templates', 'project'), root, values, flags, path => LINT_FILES.includes(path) && !lint);
|
|
197
205
|
|
|
198
206
|
const core = { name: '@amxts/core', dir: localCore?.dir ?? null };
|
|
207
|
+
const versions = await moduleVersions(installed);
|
|
199
208
|
const devDependencies = {
|
|
200
209
|
[core.name]: specFor(core, root, pm, CORE_RANGE),
|
|
201
|
-
...Object.fromEntries(installed.map(module => [module.name, specFor(module, root, pm)])),
|
|
210
|
+
...Object.fromEntries(installed.map(module => [module.name, specFor(module, root, pm, versions[module.name] ? `^${versions[module.name]}` : 'latest')])),
|
|
202
211
|
...TOOLS,
|
|
203
212
|
...(lint ? LINT_DEPENDENCIES : {}),
|
|
204
213
|
};
|
|
@@ -245,7 +254,7 @@ export async function initProject(options) {
|
|
|
245
254
|
// A spinner in a terminal; in a log (CI, a pipe) one line before and one after.
|
|
246
255
|
const spinner = process.stdout.isTTY ? p.spinner() : { start: text => p.log.step(text), stop: (text, code) => (code ? p.log.error(text) : p.log.success(text)) };
|
|
247
256
|
spinner.start(`Installing dependencies with ${pm}`);
|
|
248
|
-
const result = await
|
|
257
|
+
const result = await installLinked(installArgs(pm), linkedFolders(root), { cwd: root, quiet: true });
|
|
249
258
|
if (result.code === 0) {
|
|
250
259
|
pinLatest(root);
|
|
251
260
|
installedOk = true;
|
|
@@ -301,6 +310,25 @@ export async function initProject(options) {
|
|
|
301
310
|
if (options.install !== false && pmVersion && !installedOk) process.exitCode = 1;
|
|
302
311
|
}
|
|
303
312
|
|
|
313
|
+
/**
|
|
314
|
+
* The modules from the registry, each at its newest version that works with
|
|
315
|
+
* the newest core of CORE_RANGE - the core the project gets; a module from a
|
|
316
|
+
* folder, or a package named with its version, is left to its spec. When the
|
|
317
|
+
* registry does not answer, none: `latest`, and the install says why.
|
|
318
|
+
* @param {import('./catalog.mjs').ModuleRef[]} modules
|
|
319
|
+
* @returns {Promise<Record<string, string>>} the versions, by name
|
|
320
|
+
*/
|
|
321
|
+
async function moduleVersions(modules) {
|
|
322
|
+
const names = modules.filter(module => !module.dir && !/^@?[^@]+@/.test(module.name)).map(module => module.name);
|
|
323
|
+
if (!names.length) return {};
|
|
324
|
+
try {
|
|
325
|
+
return (await versionsFor(npmRegistry(process.cwd()), CORE_RANGE, names)).versions;
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (error instanceof CliError && !/npm view/.test(error.message)) throw error;
|
|
328
|
+
return {};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
304
332
|
/** `"latest"` in package.json, after the install, as the version it installed: `^1.6.2`. */
|
|
305
333
|
function pinLatest(root) {
|
|
306
334
|
const path = join(root, 'package.json');
|
package/src/main.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { GLOBAL_FLAGS, parseArgs } from './args.mjs';
|
|
|
12
12
|
import { needBun, needProject, projectCore, runTask, runTaskOrExit, VERSION } from './core.mjs';
|
|
13
13
|
import { prepare, TARGETS } from './includes.mjs';
|
|
14
14
|
import { detectPackageManager, PACKAGE_MANAGERS, run } from './pm.mjs';
|
|
15
|
+
import { serverMismatch } from './server-update.mjs';
|
|
15
16
|
import { ensureServer } from './server.mjs';
|
|
16
17
|
import { SYSTEMS } from './system.mjs';
|
|
17
18
|
import { banner, c, CliError, closest, log, report } from './ui.mjs';
|
|
@@ -19,6 +20,12 @@ import { banner, c, CliError, closest, log, report } from './ui.mjs';
|
|
|
19
20
|
/** --os, for the commands that compile: the plugins are built for that system. */
|
|
20
21
|
const OS_FLAG = { type: 'string', value: Object.keys(SYSTEMS).join('|'), description: 'The server\'s system, when AMXTS_SERVER does not show it: the plugins are compiled for it' };
|
|
21
22
|
|
|
23
|
+
/** The line for a server whose module is of another release than the project's core. */
|
|
24
|
+
function warnServer(core, os) {
|
|
25
|
+
const line = serverMismatch(core, needProject(), os);
|
|
26
|
+
if (line) log.warn(line);
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
/** --os as the build task takes it. */
|
|
23
30
|
function osArgs(values) {
|
|
24
31
|
if (values.os === undefined) return [];
|
|
@@ -31,7 +38,7 @@ const INIT_FLAGS = {
|
|
|
31
38
|
modules: { type: 'string', value: 'list', description: 'Modules to add, comma-separated: menu-core,config-core ("" for none)' },
|
|
32
39
|
lint: { type: 'boolean', description: 'oxlint and oxfmt, with @antfu/eslint-config\'s rules (--no-lint to leave them out)' },
|
|
33
40
|
git: { type: 'boolean', description: 'git init (--no-git to skip)' },
|
|
34
|
-
server: { type: 'string', value: 'path', description: 'The server
|
|
41
|
+
server: { type: 'string', value: 'path', description: 'The server: its folder, its cstrike or its addons/amxts; .env keeps the addons/amxts as AMXTS_SERVER, and the build takes its includes' },
|
|
35
42
|
os: { type: 'string', value: Object.keys(SYSTEMS).join('|'), description: 'The server\'s system, when the server\'s folder does not show it: written to .env as AMXTS_SERVER_OS' },
|
|
36
43
|
target: { type: 'string', value: Object.keys(TARGETS).join('|'), description: 'The server the project is for, when no server is given: rehlds - ReHLDS, ReGameDLL and ReAPI (recommended) - or hlds' },
|
|
37
44
|
install: { type: 'boolean', description: 'Install the dependencies (--no-install to skip)' },
|
|
@@ -87,7 +94,10 @@ export const COMMANDS = {
|
|
|
87
94
|
const core = await projectCore();
|
|
88
95
|
banner(core.version, 'dev');
|
|
89
96
|
// The container reads dist/ where it is: nothing to deploy, and AMXTS_SERVER is not asked.
|
|
90
|
-
if (!values.docker)
|
|
97
|
+
if (!values.docker) {
|
|
98
|
+
await ensureServer(needProject(), detectPackageManager());
|
|
99
|
+
warnServer(core, osArgs(values));
|
|
100
|
+
}
|
|
91
101
|
await prepare(core, { quiet: true });
|
|
92
102
|
if (values.docker) runTaskOrExit(core, 'build', ['--watch', '--docker', '--os', 'linux']);
|
|
93
103
|
else runTaskOrExit(core, 'build', ['--deploy', '--watch', ...osArgs(values)]);
|
|
@@ -106,6 +116,7 @@ export const COMMANDS = {
|
|
|
106
116
|
const os = osArgs(values);
|
|
107
117
|
const core = await projectCore();
|
|
108
118
|
banner(core.version, 'build');
|
|
119
|
+
warnServer(core, os);
|
|
109
120
|
if (values.deploy) await ensureServer(needProject(), detectPackageManager());
|
|
110
121
|
await prepare(core, { quiet: true });
|
|
111
122
|
runTaskOrExit(core, 'build', [...(values.deploy ? ['--deploy'] : []), ...(values.watch ? ['--watch'] : []), ...os]);
|
|
@@ -168,6 +179,21 @@ export const COMMANDS = {
|
|
|
168
179
|
await prepare(await projectCore());
|
|
169
180
|
},
|
|
170
181
|
},
|
|
182
|
+
upgrade: {
|
|
183
|
+
description: 'Move the project to the latest amxts: its packages, its code, the build and the server',
|
|
184
|
+
usage: 'amxts upgrade [--to <version>] [--no-server] [--server-only] [--dry-run]',
|
|
185
|
+
flags: {
|
|
186
|
+
to: { type: 'string', value: 'version', description: 'The version to move to (the latest on the registry by default)' },
|
|
187
|
+
server: { type: 'boolean', description: 'Update the server in AMXTS_SERVER: its module, from the release (--no-server to leave it)' },
|
|
188
|
+
serverOnly: { type: 'boolean', description: 'Only update the server - once it is stopped, when the upgrade found it running' },
|
|
189
|
+
dryRun: { type: 'boolean', description: 'Say what would change, and change nothing' },
|
|
190
|
+
},
|
|
191
|
+
examples: ['amxts upgrade', 'amxts upgrade --dry-run', 'amxts upgrade --to 0.2.0 --no-server', 'amxts upgrade --server-only'],
|
|
192
|
+
async run({ values }) {
|
|
193
|
+
const { upgrade } = await import('./upgrade.mjs');
|
|
194
|
+
await upgrade({ to: values.to, server: values.server !== false, serverOnly: values.serverOnly, dryRun: values.dryRun });
|
|
195
|
+
},
|
|
196
|
+
},
|
|
171
197
|
info: {
|
|
172
198
|
description: 'Versions and settings, for a bug report',
|
|
173
199
|
usage: 'amxts info',
|
package/src/module.mjs
CHANGED
|
@@ -16,7 +16,8 @@ import { configEntries, installTarget, loadCatalog, resolveModule, taglineOf, wi
|
|
|
16
16
|
import { addToConfig, CONFIG_FILE, newConfig, readConfigModules } from './config.mjs';
|
|
17
17
|
import { FROM_SOURCE, installedCore, needLocalCore, needProject, readJson } from './core.mjs';
|
|
18
18
|
import { prepare } from './includes.mjs';
|
|
19
|
-
import { addArgs, commandLine, detectPackageManager, execAmxts,
|
|
19
|
+
import { addArgs, commandLine, detectPackageManager, execAmxts, installLinked, linkedFolders, versionOf } from './pm.mjs';
|
|
20
|
+
import { npmRegistry, versionsFor } from './registry.mjs';
|
|
20
21
|
import { c, CliError, log } from './ui.mjs';
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -46,9 +47,12 @@ export async function moduleAdd(names, options = {}) {
|
|
|
46
47
|
|
|
47
48
|
if (!options.skipInstall) {
|
|
48
49
|
if (!versionOf(pm)) throw new CliError(`${pm} is not installed, and this project uses it`, `Install ${pm}, or add the package by hand and run amxts module add ${names.join(' ')} --skip-install.`);
|
|
49
|
-
|
|
50
|
+
// A module from the registry at its newest version that works with the project's core.
|
|
51
|
+
const fromRegistry = core ? toInstall.filter(module => !module.dir && !/^@?[^@]+@/.test(module.name)).map(module => module.name) : [];
|
|
52
|
+
const { versions } = fromRegistry.length ? await versionsFor(npmRegistry(dir), core.version, fromRegistry) : { versions: {} };
|
|
53
|
+
const argv = addArgs(pm, toInstall.map(module => (versions[module.name] ? `${module.name}@^${versions[module.name]}` : installTarget(module, dir, pm))));
|
|
50
54
|
log.step(`${commandLine(argv)}`);
|
|
51
|
-
const result = await
|
|
55
|
+
const result = await installLinked(argv, linkedFolders(dir, toInstall.map(module => module.dir).filter(Boolean)), { cwd: dir });
|
|
52
56
|
if (result.code !== 0) throw new CliError(`${commandLine(argv)} failed`, 'The package manager says why, above.');
|
|
53
57
|
}
|
|
54
58
|
|
package/src/pm.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The package manager a project uses - npm, pnpm, yarn or bun - and the
|
|
2
2
|
// commands each one spells its own way.
|
|
3
3
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
|
|
@@ -75,9 +75,64 @@ export function execAmxts(pm) {
|
|
|
75
75
|
return { npm: 'npx amxts', pnpm: 'pnpm amxts', yarn: 'yarn amxts', bun: 'bunx amxts' }[pm] ?? 'npx amxts';
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* The folders a project's package.json takes packages from - `link:` and
|
|
80
|
+
* `file:` specs: the core and the modules of this machine (--local).
|
|
81
|
+
* @param {string} projectDir
|
|
82
|
+
* @param {string[]} [more] folders about to be added
|
|
83
|
+
* @returns {string[]} the folders
|
|
84
|
+
*/
|
|
85
|
+
export function linkedFolders(projectDir, more = []) {
|
|
86
|
+
let json = {};
|
|
87
|
+
try {
|
|
88
|
+
json = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8'));
|
|
89
|
+
} catch {}
|
|
90
|
+
const specs = Object.values({ ...json.dependencies, ...json.devDependencies });
|
|
91
|
+
const linked = specs.filter(spec => /^(?:link|file):(?:[./]|[a-z]:)/i.test(spec)).map(spec => resolve(projectDir, spec.replace(/^\w+:/, '')));
|
|
92
|
+
return [...new Set([...linked, ...more])];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Runs an install and leaves the `node_modules/.bin` of each linked folder
|
|
97
|
+
* as it was. yarn 1 installs a `link:` folder's dependencies into the
|
|
98
|
+
* project and points that folder's own `.bin` at them: the core's `tsc`,
|
|
99
|
+
* `amxts` and `bun` would start the project's copies.
|
|
100
|
+
* @param {string[]} argv
|
|
101
|
+
* @param {string[]} linked the linked folders, `linkedFolders`
|
|
102
|
+
* @param {Parameters<typeof run>[1]} [options]
|
|
103
|
+
*/
|
|
104
|
+
export async function installLinked(argv, linked, options) {
|
|
105
|
+
const bins = linked.map(dir => join(dir, 'node_modules', '.bin'));
|
|
106
|
+
const saved = bins.map(binsOf);
|
|
107
|
+
const result = await run(argv, options);
|
|
108
|
+
bins.forEach((dir, i) => putBack(dir, saved[i]));
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A `.bin` folder's entries: a link's target, or a file's bytes. Null when there is none. */
|
|
113
|
+
function binsOf(dir) {
|
|
114
|
+
if (!existsSync(dir)) return null;
|
|
115
|
+
return new Map(readdirSync(dir).map((name) => {
|
|
116
|
+
const path = join(dir, name);
|
|
117
|
+
const stat = lstatSync(path);
|
|
118
|
+
return [name, stat.isSymbolicLink() ? { link: readlinkSync(path) } : { data: readFileSync(path), mode: stat.mode }];
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Writes a `.bin` folder back as `binsOf` read it; what the install added goes. */
|
|
123
|
+
function putBack(dir, saved) {
|
|
124
|
+
if (!saved) return;
|
|
125
|
+
mkdirSync(dir, { recursive: true });
|
|
126
|
+
for (const name of readdirSync(dir)) rmSync(join(dir, name), { force: true });
|
|
127
|
+
for (const [name, entry] of saved) {
|
|
128
|
+
if (entry.link) symlinkSync(entry.link, join(dir, name));
|
|
129
|
+
else writeFileSync(join(dir, name), entry.data, { mode: entry.mode });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A command line as it is shown, and as Windows' shell runs it: arguments with spaces, or with what that shell reads (`^0.2.0`), quoted. */
|
|
79
134
|
export function commandLine(argv) {
|
|
80
|
-
return argv.map(arg => (
|
|
135
|
+
return argv.map(arg => (/[\s^&|<>]/.test(arg) ? `"${arg}"` : arg)).join(' ');
|
|
81
136
|
}
|
|
82
137
|
|
|
83
138
|
/**
|
|
@@ -85,7 +140,7 @@ export function commandLine(argv) {
|
|
|
85
140
|
* .cmd files; `quiet` collects the output instead of showing it, for a
|
|
86
141
|
* spinner, and hands it back for an error.
|
|
87
142
|
*
|
|
88
|
-
* @returns {Promise<{ code: number, output: string }>} its exit code, and its output when quiet
|
|
143
|
+
* @returns {Promise<{ code: number, output: string, stdout: string }>} its exit code, and its output when quiet - all of it, and what went to stdout alone
|
|
89
144
|
*/
|
|
90
145
|
export function run(argv, { cwd = process.cwd(), quiet = false, env } = {}) {
|
|
91
146
|
return new Promise((done) => {
|
|
@@ -99,13 +154,15 @@ export function run(argv, { cwd = process.cwd(), quiet = false, env } = {}) {
|
|
|
99
154
|
windowsHide: true,
|
|
100
155
|
});
|
|
101
156
|
let output = '';
|
|
157
|
+
let stdout = '';
|
|
102
158
|
child.stdout?.on('data', (chunk) => {
|
|
103
159
|
output += chunk;
|
|
160
|
+
stdout += chunk;
|
|
104
161
|
});
|
|
105
162
|
child.stderr?.on('data', (chunk) => {
|
|
106
163
|
output += chunk;
|
|
107
164
|
});
|
|
108
|
-
child.on('error', error => done({ code: 127, output: `${output}${error.message}
|
|
109
|
-
child.on('close', code => done({ code: code ?? 1, output }));
|
|
165
|
+
child.on('error', error => done({ code: 127, output: `${output}${error.message}`, stdout }));
|
|
166
|
+
child.on('close', code => done({ code: code ?? 1, output, stdout }));
|
|
110
167
|
});
|
|
111
168
|
}
|
package/src/registry.mjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// The versions of the @amxts packages that go together. Every package has a
|
|
2
|
+
// version of its own: a module versions by its own API and names the cores it
|
|
3
|
+
// works with as a range (`peerDependencies`: `"@amxts/core": "^0.2.0"`); the
|
|
4
|
+
// core names the command and wamrc it comes with the same way. So for a core,
|
|
5
|
+
// each package's newest version that works with it - what `amxts upgrade`
|
|
6
|
+
// moves a project to, and what a new project and `amxts module add` install.
|
|
7
|
+
import { compareVersions, satisfies } from './core.mjs';
|
|
8
|
+
import { run } from './pm.mjs';
|
|
9
|
+
import { CliError } from './ui.mjs';
|
|
10
|
+
|
|
11
|
+
export const CORE = '@amxts/core';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} Release a version of a package, as the registry has it
|
|
15
|
+
* @property {string} version its version
|
|
16
|
+
* @property {Record<string, string>} [dependencies] what it depends on, by range
|
|
17
|
+
* @property {Record<string, string>} [peerDependencies] what it needs beside it: a module's core and modules
|
|
18
|
+
* @property {Record<string, string>} [optionalDependencies] what it takes when there is one for the system: the core's wamrc
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} Registry
|
|
23
|
+
* @property {(name: string, spec?: string) => Promise<Release[]>} versions a package's versions of `spec` - `*`, a version, a tag - with what they depend on; none when it has none
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The registry npm is set to - NPM_CONFIG_REGISTRY, .npmrc - through npm itself.
|
|
28
|
+
* @param {string} dir the folder whose .npmrc npm reads
|
|
29
|
+
* @returns {Registry} the registry
|
|
30
|
+
*/
|
|
31
|
+
export function npmRegistry(dir) {
|
|
32
|
+
return {
|
|
33
|
+
versions: async (name, spec = '*') => {
|
|
34
|
+
const result = await run(['npm', 'view', `${name}@${spec}`, 'version', 'dependencies', 'peerDependencies', 'optionalDependencies', '--json'], { cwd: dir, quiet: true });
|
|
35
|
+
if (result.code !== 0) {
|
|
36
|
+
if (/E404|404 Not Found|No match found/.test(result.output)) return [];
|
|
37
|
+
throw new CliError(`npm view ${name}@${spec} failed`, result.output.trim().split('\n').slice(-5).join('\n'));
|
|
38
|
+
}
|
|
39
|
+
// One version is an object, more a list; a version that depends on nothing, its version alone.
|
|
40
|
+
return [JSON.parse(result.stdout.trim() || '[]')].flat().map(each => (typeof each === 'string' ? { version: each } : each));
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The newest of a package's releases that works with a core: one whose own
|
|
47
|
+
* range for the core takes the core's version; for a package that names no
|
|
48
|
+
* core, one the core's own range for it takes (the command, wamrc); else the
|
|
49
|
+
* newest.
|
|
50
|
+
* @param {Release[]} releases
|
|
51
|
+
* @param {string} name
|
|
52
|
+
* @param {Release} core
|
|
53
|
+
* @returns {string | null} the version; null when none works with the core
|
|
54
|
+
*/
|
|
55
|
+
export function newestFor(releases, name, core) {
|
|
56
|
+
const asked = core.dependencies?.[name] ?? core.optionalDependencies?.[name];
|
|
57
|
+
const fits = (release) => {
|
|
58
|
+
const range = release.peerDependencies?.[CORE] ?? release.dependencies?.[CORE];
|
|
59
|
+
return range ? satisfies(core.version, range) : !asked || satisfies(release.version, asked);
|
|
60
|
+
};
|
|
61
|
+
return releases.filter(fits).map(each => each.version).sort(compareVersions).at(-1) ?? null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A core and, for it, the newest version of each package that works with it,
|
|
66
|
+
* asked of the registry all at once. The core is a version, a tag (`latest`),
|
|
67
|
+
* or a range, whose newest version it is. Throws, saying which, when the core
|
|
68
|
+
* or a package has no such version.
|
|
69
|
+
* @param {Registry} registry
|
|
70
|
+
* @param {string} core
|
|
71
|
+
* @param {string[]} names
|
|
72
|
+
* @returns {Promise<{ core: string, versions: Record<string, string> }>} the core's version, and each package's
|
|
73
|
+
*/
|
|
74
|
+
export async function versionsFor(registry, core, names) {
|
|
75
|
+
const range = !/^(?:\d+\.\d+\.\d+(?:-[\w.-]+)?|[a-z][\w-]*)$/i.test(core);
|
|
76
|
+
const [cores, ...lists] = await Promise.all([registry.versions(CORE, range ? '*' : core), ...names.map(name => registry.versions(name))]);
|
|
77
|
+
const found = cores.filter(each => !range || satisfies(each.version, core)).sort((a, b) => compareVersions(a.version, b.version)).at(-1);
|
|
78
|
+
if (!found) throw new CliError(`${CORE} ${core} is not on the registry`, 'npm config get registry says which one npm asks.');
|
|
79
|
+
const versions = names.map((name, i) => newestFor(lists[i], name, found));
|
|
80
|
+
const missing = names.map((name, i) => !versions[i] && (lists[i].length ? `${name} has no version for ${CORE} ${found.version}` : `${name} is not on the registry`)).filter(Boolean);
|
|
81
|
+
if (missing.length) throw new CliError(missing.join('\n'), `Each package takes its newest version that works with the core; a module that has none is not released for ${CORE} ${found.version} yet.`);
|
|
82
|
+
return { core: found.version, versions: Object.fromEntries(names.map((name, i) => [name, versions[i]])) };
|
|
83
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// The server step of `amxts upgrade`: the server in AMXTS_SERVER gets the
|
|
2
|
+
// module of the project's core - and the compiler for `.ts` written on the
|
|
3
|
+
// server, where it has one - from that version's GitHub Release.
|
|
4
|
+
//
|
|
5
|
+
// The release's manifest (amxts-<system>.json) names each file with its
|
|
6
|
+
// sha256. Nothing on the server changes until every file it needs is
|
|
7
|
+
// downloaded and checked, and none of the files it replaces is in use: a
|
|
8
|
+
// running Windows server holds its module, which cannot be written then.
|
|
9
|
+
// The file it replaces stays beside it, with its version
|
|
10
|
+
// (amxts_amxx.dll.0.1.0). AMX Mod X's plugins.ini loses an
|
|
11
|
+
// `amxts_host.amxx` line: the module loads its host plugin itself.
|
|
12
|
+
//
|
|
13
|
+
// Also the check `amxts dev` and `build` start with: the release the
|
|
14
|
+
// server's module is of, read from the module file (the core's
|
|
15
|
+
// moduleVersion()), against the project's core.
|
|
16
|
+
import { Buffer } from 'node:buffer';
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
import { closeSync, existsSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { dirname, join, resolve } from 'node:path';
|
|
20
|
+
import { setting } from './core.mjs';
|
|
21
|
+
import { serverFolderOrNull } from './server.mjs';
|
|
22
|
+
import { c, CliError, log } from './ui.mjs';
|
|
23
|
+
|
|
24
|
+
/** A path as the messages write it. */
|
|
25
|
+
const slashes = path => path.replace(/\\/g, '/');
|
|
26
|
+
|
|
27
|
+
const sha256 = data => createHash('sha256').update(data).digest('hex');
|
|
28
|
+
|
|
29
|
+
/** A file of a release: a URL's, or a folder's (AMXTS_RELEASE_URL may name one). */
|
|
30
|
+
export async function releaseFile(base, name) {
|
|
31
|
+
if (!/^https?:\/\//.test(base)) return readFileSync(join(base, name));
|
|
32
|
+
const url = new URL(name, base.endsWith('/') ? base : `${base}/`);
|
|
33
|
+
const response = await fetch(url);
|
|
34
|
+
if (!response.ok) throw new CliError(`${url} answered ${response.status}`, 'The release may not be published yet, or the network is down.');
|
|
35
|
+
return Buffer.from(await response.arrayBuffer());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Whether a file can be written: false while a process holds it - a module a running server loaded. */
|
|
39
|
+
export function writable(path) {
|
|
40
|
+
try {
|
|
41
|
+
closeSync(openSync(path, 'r+'));
|
|
42
|
+
return true;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// EBUSY: a module Windows has loaded; ETXTBSY: a tool Linux is running.
|
|
45
|
+
if (['EBUSY', 'ETXTBSY', 'EPERM', 'EACCES'].includes(error.code)) return false;
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** AMX Mod X's plugins.ini with its `amxts_host.amxx` lines left out; null when it has none. */
|
|
51
|
+
export function withoutHost(text) {
|
|
52
|
+
const lines = text.split(/(?<=\n)/);
|
|
53
|
+
const kept = lines.filter(line => !/^\s*amxts_host\.amxx\b/i.test(line));
|
|
54
|
+
return kept.length === lines.length ? null : kept.join('');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The settings the core's serverSystem() reads, as the build sees them: the environment, else .env. */
|
|
58
|
+
function serverEnv(dir) {
|
|
59
|
+
return { AMXTS_SERVER: setting(dir, 'AMXTS_SERVER'), AMXTS_SERVER_OS: setting(dir, 'AMXTS_SERVER_OS') };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The server's game folder and the files of `api`'s release for its system, or null when .env names no server that is there.
|
|
64
|
+
* @param {typeof import('@amxts/core/cli-api')} api
|
|
65
|
+
* @param {string} dir the project
|
|
66
|
+
* @param {string[]} [argv] --os, when the command was given one
|
|
67
|
+
*/
|
|
68
|
+
function serverOf(api, dir, argv = []) {
|
|
69
|
+
const amxts = serverFolderOrNull(setting(dir, 'AMXTS_SERVER'));
|
|
70
|
+
if (!amxts || !existsSync(amxts)) return null;
|
|
71
|
+
const { system } = api.serverSystem(argv, serverEnv(dir));
|
|
72
|
+
return { amxts, game: resolve(amxts, '..', '..'), system, release: api.release(system) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The line `amxts dev` and `build` start with when the server's module is of
|
|
77
|
+
* another release than the project's core; null when it is the same, or
|
|
78
|
+
* there is no module to read.
|
|
79
|
+
* @param {import('./core.mjs').Core} core
|
|
80
|
+
* @param {string} dir the project
|
|
81
|
+
* @param {string[]} [argv] --os, when the command was given one
|
|
82
|
+
*/
|
|
83
|
+
export function serverMismatch(core, dir, argv = []) {
|
|
84
|
+
const server = serverOf(core.api, dir, argv);
|
|
85
|
+
const running = server && core.api.moduleVersion(join(server.game, server.release.files[0].path));
|
|
86
|
+
return running && running !== core.version ? `the server runs amxts ${running}, this project ${core.version} - run amxts upgrade` : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @typedef {object} ServerOutcome
|
|
91
|
+
* @property {boolean} ok whether the server is as it should be, or nothing was asked of it
|
|
92
|
+
* @property {string} line what happened, for the summary
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The server step.
|
|
97
|
+
* @param {typeof import('@amxts/core/cli-api')} api the project's core, of the version the server moves to
|
|
98
|
+
* @param {string} dir the project
|
|
99
|
+
* @param {{ dryRun?: boolean, canWrite?: typeof writable }} [options] `canWrite`: what the tests replace
|
|
100
|
+
* @returns {Promise<ServerOutcome>} what happened
|
|
101
|
+
*/
|
|
102
|
+
export async function updateServer(api, dir, { dryRun = false, canWrite = writable } = {}) {
|
|
103
|
+
const named = setting(dir, 'AMXTS_SERVER');
|
|
104
|
+
const version = api.version;
|
|
105
|
+
if (!named) {
|
|
106
|
+
const image = api.release('linux').image;
|
|
107
|
+
log.step('Server: no AMXTS_SERVER in .env');
|
|
108
|
+
return { ok: true, line: `none in .env; a server in Docker: docker pull ${image}` };
|
|
109
|
+
}
|
|
110
|
+
const server = serverOf(api, dir);
|
|
111
|
+
if (!server) {
|
|
112
|
+
log.step(`Server: ${slashes(named)} is not there`);
|
|
113
|
+
return { ok: false, line: `${slashes(named)} is not there: nothing was updated` };
|
|
114
|
+
}
|
|
115
|
+
const { game, system, release } = server;
|
|
116
|
+
const [module] = release.files;
|
|
117
|
+
const modulePath = join(game, module.path);
|
|
118
|
+
log.step(`Server: ${slashes(game)} ${c.dim(`(${system})`)}`);
|
|
119
|
+
if (!existsSync(modulePath)) {
|
|
120
|
+
return { ok: false, line: `there is no ${module.asset} in ${slashes(dirname(modulePath))}: install the server kit of amxts ${version}` };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const before = api.moduleVersion(modulePath) ?? 'old';
|
|
124
|
+
const manifest = JSON.parse((await releaseFile(release.url, release.manifest)).toString('utf8'));
|
|
125
|
+
if (manifest.version !== version) throw new CliError(`${release.manifest} is of amxts ${manifest.version}, not ${version}`, `It was read from ${release.url}.`);
|
|
126
|
+
const files = release.files
|
|
127
|
+
.filter(file => !file.tool || existsSync(join(game, file.path)))
|
|
128
|
+
.map((file) => {
|
|
129
|
+
const entry = manifest.files.find(one => one.name === file.asset);
|
|
130
|
+
if (!entry) throw new CliError(`${release.manifest} lists no ${file.asset}`, `It was read from ${release.url}.`);
|
|
131
|
+
const path = join(game, file.path);
|
|
132
|
+
return { ...file, path, sha256: entry.sha256, same: existsSync(path) && sha256(readFileSync(path)) === entry.sha256 };
|
|
133
|
+
});
|
|
134
|
+
const changing = files.filter(file => !file.same);
|
|
135
|
+
const pluginsIni = join(game, 'addons', 'amxmodx', 'configs', 'plugins.ini');
|
|
136
|
+
const cleaned = existsSync(pluginsIni) ? withoutHost(readFileSync(pluginsIni, 'utf8')) : null;
|
|
137
|
+
const names = changing.map(file => file.path.split(/[\\/]/).pop()).join(', ');
|
|
138
|
+
|
|
139
|
+
if (dryRun) {
|
|
140
|
+
for (const file of changing) log.info(`would put ${file.asset} of ${version} at ${slashes(file.path)}`);
|
|
141
|
+
if (cleaned !== null) log.info(`would take amxts_host.amxx out of ${slashes(pluginsIni)}`);
|
|
142
|
+
return { ok: true, line: changing.length ? `${names} would go from amxts ${before} to ${version}` : `already amxts ${version}` };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const busy = changing.filter(file => existsSync(file.path) && !canWrite(file.path));
|
|
146
|
+
if (busy.length) {
|
|
147
|
+
log.warn(`${busy.map(file => slashes(file.path)).join(', ')} is in use: the server is running`);
|
|
148
|
+
log.hint(`Stop the server, then run: amxts upgrade --server-only`);
|
|
149
|
+
return { ok: false, line: `in use - stop the server and run amxts upgrade --server-only; nothing on it was changed` };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Every file downloaded and checked before the first is replaced.
|
|
153
|
+
const downloads = await Promise.all(changing.map(async (file) => {
|
|
154
|
+
const data = await releaseFile(release.url, file.asset);
|
|
155
|
+
if (sha256(data) !== file.sha256) throw new CliError(`${file.asset} from ${release.url} is not the one ${release.manifest} lists (sha256)`, 'Nothing on the server was changed. Run it again; if it says the same, the download is broken.');
|
|
156
|
+
return { ...file, data };
|
|
157
|
+
}));
|
|
158
|
+
for (const file of downloads) {
|
|
159
|
+
const old = existsSync(file.path) ? statSync(file.path) : null;
|
|
160
|
+
if (old) {
|
|
161
|
+
rmSync(`${file.path}.${before}`, { force: true });
|
|
162
|
+
renameSync(file.path, `${file.path}.${before}`);
|
|
163
|
+
}
|
|
164
|
+
writeFileSync(file.path, file.data, { mode: old?.mode ?? 0o755 });
|
|
165
|
+
log.success(`${file.path.split(/[\\/]/).pop()} ${c.dim(`${before} →`)} ${version}${old ? c.dim(` (the old one: ${file.path.split(/[\\/]/).pop()}.${before})`) : ''}`);
|
|
166
|
+
}
|
|
167
|
+
if (cleaned !== null) {
|
|
168
|
+
writeFileSync(pluginsIni, cleaned);
|
|
169
|
+
log.success(`Took amxts_host.amxx out of ${slashes(pluginsIni)}: the module loads it itself`);
|
|
170
|
+
}
|
|
171
|
+
if (!changing.length) return { ok: true, line: `already amxts ${version}${cleaned !== null ? ', plugins.ini cleaned' : ''}` };
|
|
172
|
+
return { ok: true, line: `${names} ${before} → ${version} - restart the server to load it` };
|
|
173
|
+
}
|
package/src/server.mjs
CHANGED
|
@@ -3,12 +3,48 @@
|
|
|
3
3
|
// ask the same when it is not set - in a terminal. Elsewhere (CI, a pipe)
|
|
4
4
|
// nothing is asked, and the build says it is missing.
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
6
|
+
import { basename, join, resolve } from 'node:path';
|
|
7
7
|
import process from 'node:process';
|
|
8
8
|
import * as p from '@clack/prompts';
|
|
9
9
|
import { setting } from './core.mjs';
|
|
10
10
|
import { runScript } from './pm.mjs';
|
|
11
|
-
import { c } from './ui.mjs';
|
|
11
|
+
import { c, CliError } from './ui.mjs';
|
|
12
|
+
|
|
13
|
+
// The folders a server path may name above its addons/amxts, by what is below
|
|
14
|
+
// them: hlds's own (cstrike/addons), the game's (addons).
|
|
15
|
+
const ABOVE_ADDONS = [join('cstrike', 'addons'), 'addons'];
|
|
16
|
+
|
|
17
|
+
/** A path as .env and the messages write it: with forward slashes. */
|
|
18
|
+
const slashes = path => path.replace(/\\/g, '/');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The server's addons/amxts folder from a path a person gave: that folder
|
|
22
|
+
* itself, or the hlds folder, cstrike or cstrike/addons above it - as the
|
|
23
|
+
* core's build reads AMXTS_SERVER. A path that is not there yet is taken as
|
|
24
|
+
* it is, and so is a folder named amxts or holding the module's plugins.ini.
|
|
25
|
+
* @param {string} path
|
|
26
|
+
* @returns {string} the folder; '' for ''
|
|
27
|
+
* @throws {CliError} for a folder that is there and none of them, naming where it looked
|
|
28
|
+
*/
|
|
29
|
+
export function serverFolder(path) {
|
|
30
|
+
if (!path) return '';
|
|
31
|
+
const dir = resolve(path);
|
|
32
|
+
if (!existsSync(dir) || basename(dir).toLowerCase() === 'amxts' || existsSync(join(dir, 'plugins.ini'))) return path;
|
|
33
|
+
if (basename(dir).toLowerCase() === 'addons') return join(dir, 'amxts');
|
|
34
|
+
const addons = ABOVE_ADDONS.map(below => join(dir, below)).find(folder => existsSync(folder));
|
|
35
|
+
if (addons) return join(addons, 'amxts');
|
|
36
|
+
const looked = [...ABOVE_ADDONS, 'plugins.ini'].map(below => slashes(join(dir, below)));
|
|
37
|
+
throw new CliError(`${slashes(path)} is not a server: there is no ${looked.join(', ')}`, 'Give the server\'s folder (where hlds is), its cstrike, or cstrike/addons/amxts.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The server's addons/amxts from a path, or null when the path is no server: for what only reads it. */
|
|
41
|
+
export function serverFolderOrNull(path) {
|
|
42
|
+
try {
|
|
43
|
+
return serverFolder(path);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
12
48
|
|
|
13
49
|
/** Whether a command may ask: a terminal, and not CI. */
|
|
14
50
|
export function canAsk() {
|
|
@@ -16,18 +52,28 @@ export function canAsk() {
|
|
|
16
52
|
}
|
|
17
53
|
|
|
18
54
|
/**
|
|
19
|
-
* Asks for the server
|
|
20
|
-
* not taken. The
|
|
55
|
+
* Asks for the server: its folder, its cstrike or its addons/amxts.
|
|
56
|
+
* `required`: an empty answer is not taken. The server's addons/amxts
|
|
57
|
+
* (serverFolder), '' for none, or clack's cancel symbol on Ctrl+C.
|
|
21
58
|
* @param {string} pm the project's package manager, for the words
|
|
22
59
|
* @param {{ required?: boolean }} [options]
|
|
23
60
|
*/
|
|
24
|
-
export function askServer(pm, { required = false } = {}) {
|
|
25
|
-
|
|
26
|
-
message: `Where is the server? ${c.dim(`its addons/amxts
|
|
27
|
-
placeholder: process.platform === 'win32' ? 'D:/hlds
|
|
61
|
+
export async function askServer(pm, { required = false } = {}) {
|
|
62
|
+
const answer = await p.text({
|
|
63
|
+
message: `Where is the server? ${c.dim(`its folder or its addons/amxts, for ${runScript(pm, 'dev')}${required ? '' : ' - empty to skip'}`)}`,
|
|
64
|
+
placeholder: process.platform === 'win32' ? 'D:/hlds' : '/srv/hlds',
|
|
28
65
|
defaultValue: '',
|
|
29
|
-
validate:
|
|
66
|
+
validate: (value) => {
|
|
67
|
+
if (!value?.trim()) return required ? 'The plugins are deployed there: the server\'s folder' : undefined;
|
|
68
|
+
try {
|
|
69
|
+
serverFolder(value.trim());
|
|
70
|
+
return undefined;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return `${error.message}. ${error.hint}`;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
30
75
|
});
|
|
76
|
+
return p.isCancel(answer) ? answer : serverFolder(answer.trim());
|
|
31
77
|
}
|
|
32
78
|
|
|
33
79
|
/** The warning for a server folder that is not there. */
|
|
@@ -67,6 +113,6 @@ export async function ensureServer(root, pm) {
|
|
|
67
113
|
}
|
|
68
114
|
saveServer(root, server);
|
|
69
115
|
process.env.AMXTS_SERVER = server;
|
|
70
|
-
p.log.success(`Wrote AMXTS_SERVER=${server
|
|
116
|
+
p.log.success(`Wrote AMXTS_SERVER=${slashes(server)} to .env`);
|
|
71
117
|
if (!existsSync(server)) p.log.warn(missingServer(server));
|
|
72
118
|
}
|
package/src/upgrade.mjs
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// `amxts upgrade`: a project moved to another amxts release in one command.
|
|
2
|
+
//
|
|
3
|
+
// amxts upgrade [--to <version>] [--no-server] [--server-only] [--dry-run]
|
|
4
|
+
//
|
|
5
|
+
// 1. The packages: the core to the latest version on the registry npm is set
|
|
6
|
+
// to, or to --to; every other @amxts/ package in package.json - the
|
|
7
|
+
// official modules, the command - to its own newest version that works
|
|
8
|
+
// with that core (registry.mjs), in the style the project writes its
|
|
9
|
+
// ranges (`^0.1.0` → `^0.2.0`); then the project's own package manager
|
|
10
|
+
// installs.
|
|
11
|
+
// 2. The code: the new core's rewrites (its `upgrade` task), each change listed.
|
|
12
|
+
// 3. The build: `amxts build`.
|
|
13
|
+
// 4. The server in AMXTS_SERVER: its module, and its compiler where it has
|
|
14
|
+
// one, from the release of the new version (server-update.mjs).
|
|
15
|
+
// 5. A summary: the versions, the files rewritten, what to check by hand and
|
|
16
|
+
// the server's result.
|
|
17
|
+
//
|
|
18
|
+
// A command of one release drives cores of that release only, so once the
|
|
19
|
+
// packages are installed a command of another version hands the rest to the
|
|
20
|
+
// project's new one: `amxts upgrade --to <version>` again, with the versions
|
|
21
|
+
// it started from in AMXTS_UPGRADE_FROM.
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
import { tmpdir } from 'node:os';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import process from 'node:process';
|
|
28
|
+
import { coreDirFrom, loadCore, needProject, readJson, runTask, satisfies, VERSION } from './core.mjs';
|
|
29
|
+
import { prepare } from './includes.mjs';
|
|
30
|
+
import { commandLine, detectPackageManager, installArgs, installLinked, linkedFolders } from './pm.mjs';
|
|
31
|
+
import { CORE, npmRegistry, versionsFor } from './registry.mjs';
|
|
32
|
+
import { updateServer } from './server-update.mjs';
|
|
33
|
+
import { banner, c, CliError, log } from './ui.mjs';
|
|
34
|
+
|
|
35
|
+
const VERSION_TEXT = /^v?(\d+\.\d+\.\d+(?:-[\d.a-z-]+)?)$/i;
|
|
36
|
+
|
|
37
|
+
/** A spec upgrade moves: a version, with `^`, `~`, `=` or `>=` before it. */
|
|
38
|
+
const MOVABLE = /^([~^]|>?=)?\d+\.\d+\.\d+(?:-[\d.a-z-]+)?$/i;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A spec at another version, in the style it was written: `^0.1.0` →
|
|
42
|
+
* `^0.2.0`, `~0.1.0` → `~0.2.0`, `0.1.0` → `0.2.0`. Null for a spec upgrade
|
|
43
|
+
* leaves as it is: a folder (`file:`, `link:`), a tag, a range of its own.
|
|
44
|
+
* @param {string} spec
|
|
45
|
+
* @param {string} version
|
|
46
|
+
*/
|
|
47
|
+
export function bumpSpec(spec, version) {
|
|
48
|
+
const match = MOVABLE.exec(spec.trim());
|
|
49
|
+
return match ? `${match[1] ?? ''}${version}` : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {object} Bump
|
|
54
|
+
* @property {string} name the package
|
|
55
|
+
* @property {string} spec as package.json has it
|
|
56
|
+
* @property {string | null} version the version it goes to; null when it is left as it is
|
|
57
|
+
* @property {string | null} to the spec at that version; null when it is left as it is
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/** The @amxts/ packages of a project's package.json, with their specs. */
|
|
61
|
+
function amxtsPackages(pkg) {
|
|
62
|
+
return Object.entries({ ...pkg.dependencies, ...pkg.devDependencies }).filter(([name]) => name.startsWith('@amxts/')).map(([name, spec]) => ({ name, spec: String(spec) }));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The @amxts/ packages of a project's package.json, and their specs at the versions they go to.
|
|
67
|
+
* @param {Record<string, any>} pkg
|
|
68
|
+
* @param {Record<string, string | null>} versions each package's new version, by name
|
|
69
|
+
* @returns {Bump[]} the packages
|
|
70
|
+
*/
|
|
71
|
+
export function packageBumps(pkg, versions) {
|
|
72
|
+
return amxtsPackages(pkg).map(({ name, spec }) => {
|
|
73
|
+
const to = versions[name] ? bumpSpec(spec, versions[name]) : null;
|
|
74
|
+
return { name, spec, version: to && versions[name], to };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** package.json's text with the specs moved, everything else as it was written. */
|
|
79
|
+
export function writeBumps(text, bumps) {
|
|
80
|
+
return bumps.filter(bump => bump.to && bump.to !== bump.spec).reduce((out, bump) => {
|
|
81
|
+
const escape = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
82
|
+
return out.replace(new RegExp(`("${escape(bump.name)}"\\s*:\\s*")${escape(bump.spec)}"`, 'g'), `$1${bump.to}"`);
|
|
83
|
+
}, text);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The version of a package the project has installed, or null. */
|
|
87
|
+
function installedVersion(dir, name) {
|
|
88
|
+
return readJson(join(dir, 'node_modules', name, 'package.json'))?.version ?? null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Runs the project's install, quietly, with a line before and after; its output when it fails. */
|
|
92
|
+
async function installProject(dir, pm) {
|
|
93
|
+
const argv = installArgs(pm);
|
|
94
|
+
log.step(`Installing with ${pm}`);
|
|
95
|
+
const result = await installLinked(argv, linkedFolders(dir), { cwd: dir, quiet: true });
|
|
96
|
+
if (result.code !== 0) {
|
|
97
|
+
throw new CliError(`${commandLine(argv)} failed`, result.output.trim().split('\n').slice(-15).join('\n'));
|
|
98
|
+
}
|
|
99
|
+
log.success(`Installed with ${pm}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The command a project's core runs - `npx amxts` - by its folder, or null. */
|
|
103
|
+
function projectCommand(coreDir) {
|
|
104
|
+
try {
|
|
105
|
+
return join(createRequire(join(coreDir, 'package.json')).resolve('@amxts/cli/package.json'), '..');
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The code step: the core's rewrites, listed as they are made; with
|
|
113
|
+
* `dryRun`, listed only. What they changed and left, from the task's report.
|
|
114
|
+
*/
|
|
115
|
+
function rewriteCode(core, dryRun) {
|
|
116
|
+
const dir = mkdtempSync(join(tmpdir(), 'amxts-upgrade-'));
|
|
117
|
+
try {
|
|
118
|
+
const report = join(dir, 'report.json');
|
|
119
|
+
if (runTask(core, 'upgrade', ['--report', report, ...(dryRun ? ['--dry-run'] : [])]) !== 0) {
|
|
120
|
+
throw new CliError('The code was not upgraded', 'The error is above.');
|
|
121
|
+
}
|
|
122
|
+
return JSON.parse(readFileSync(report, 'utf8'));
|
|
123
|
+
} finally {
|
|
124
|
+
rmSync(dir, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @typedef {object} Outcome
|
|
130
|
+
* @property {string} version the release the project moves to: the core's version
|
|
131
|
+
* @property {{ name: string, from: string | null, version: string | null, to: string | null, spec: string }[]} packages each @amxts/ package, and the version and spec it goes to; null when left as it is
|
|
132
|
+
* @property {{ files: string[], left: { file: string, line: number, why: string }[] } | null} code what the rewrites changed and left; null when they did not run
|
|
133
|
+
* @property {'built' | 'failed' | null} build how the build went; null when it did not run
|
|
134
|
+
* @property {import('./server-update.mjs').ServerOutcome | null} server the server step's result; null with --no-server
|
|
135
|
+
* @property {boolean} dryRun whether nothing was changed
|
|
136
|
+
*/
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* What `amxts upgrade` prints last.
|
|
140
|
+
* @param {Outcome} outcome
|
|
141
|
+
* @returns {string[]} the lines
|
|
142
|
+
*/
|
|
143
|
+
export function summary(outcome) {
|
|
144
|
+
const lines = [];
|
|
145
|
+
const would = outcome.dryRun ? 'would be ' : '';
|
|
146
|
+
const moved = outcome.packages.filter(each => each.to);
|
|
147
|
+
const width = Math.max(0, ...moved.map(each => each.name.length));
|
|
148
|
+
lines.push(c.bold(outcome.dryRun ? `Dry run: amxts ${outcome.version}, nothing was changed` : `Upgraded to amxts ${outcome.version}`));
|
|
149
|
+
for (const each of moved) lines.push(` ${each.name.padEnd(width)} ${each.from === each.version ? `${each.version} ${c.dim('(already)')}` : `${each.from ?? each.spec} ${c.dim('→')} ${each.version}`}`);
|
|
150
|
+
for (const each of outcome.packages.filter(one => !one.to)) lines.push(` ${each.name.padEnd(width)} ${each.spec} ${c.dim('(left as it is)')}`);
|
|
151
|
+
if (outcome.code) {
|
|
152
|
+
lines.push(outcome.code.files.length ? ` ${c.green('✔')} ${would}rewritten: ${outcome.code.files.join(', ')}` : ` ${c.green('✔')} the code already uses this API`);
|
|
153
|
+
if (outcome.code.left.length) {
|
|
154
|
+
lines.push(` ${c.yellow('▲')} to check by hand:`);
|
|
155
|
+
for (const each of outcome.code.left) lines.push(` ${c.dim(`${each.file}:${each.line}`)} ${each.why}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (outcome.build) lines.push(outcome.build === 'built' ? ` ${c.green('✔')} built` : ` ${c.red('✖')} the build failed: see above`);
|
|
159
|
+
if (outcome.server) lines.push(` ${outcome.server.ok ? c.green('✔') : c.yellow('▲')} server: ${outcome.server.line}`);
|
|
160
|
+
return lines;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* `amxts upgrade`.
|
|
165
|
+
* @param {{ to?: string, server?: boolean, serverOnly?: boolean, dryRun?: boolean }} options
|
|
166
|
+
* @param {{ dir?: string, registry?: import('./registry.mjs').Registry, install?: typeof installProject }} [deps] what the tests replace
|
|
167
|
+
* @returns {Promise<Outcome | null>} what was done; null when it was handed to the project's command
|
|
168
|
+
*/
|
|
169
|
+
export async function upgrade({ to, server = true, serverOnly = false, dryRun = false }, { dir = needProject(), registry = npmRegistry(dir), install = installProject } = {}) {
|
|
170
|
+
const handedFrom = process.env.AMXTS_UPGRADE_FROM ? JSON.parse(process.env.AMXTS_UPGRADE_FROM) : null;
|
|
171
|
+
if (!handedFrom) banner(VERSION, dryRun ? 'upgrade --dry-run' : 'upgrade');
|
|
172
|
+
if (to !== undefined && !VERSION_TEXT.test(to)) throw new CliError(`--to ${to}: a version, such as 0.2.0`);
|
|
173
|
+
|
|
174
|
+
if (serverOnly) {
|
|
175
|
+
const core = await loadCore(coreDirFrom(dir) ?? needCore());
|
|
176
|
+
/** @type {Outcome} */
|
|
177
|
+
const outcome = { version: core.version, packages: [], code: null, build: null, server: await updateServer(core.api, dir, { dryRun }), dryRun };
|
|
178
|
+
console.log(['', ...summary(outcome)].join('\n'));
|
|
179
|
+
return outcome;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const pkg = readJson(join(dir, 'package.json')) ?? {};
|
|
183
|
+
const { version, versions } = handedFrom ? installed(dir, pkg, VERSION_TEXT.exec(to)[1]) : await newest(registry, dir, pkg, to ? VERSION_TEXT.exec(to)[1] : 'latest');
|
|
184
|
+
const bumps = packageBumps(pkg, versions);
|
|
185
|
+
const from = handedFrom ?? Object.fromEntries(bumps.map(bump => [bump.name, installedVersion(dir, bump.name)]));
|
|
186
|
+
const moving = bumps.filter(bump => bump.to && (bump.to !== bump.spec || from[bump.name] !== bump.version));
|
|
187
|
+
|
|
188
|
+
if (!handedFrom) {
|
|
189
|
+
log.step(`Packages: amxts ${version}${to ? '' : c.dim(' (latest)')}`);
|
|
190
|
+
for (const bump of bumps) log.info(`${bump.name} ${bump.spec} ${!bump.to ? c.dim('(left as it is)') : moving.includes(bump) ? `${c.dim('→')} ${bump.to}` : c.dim('(already)')}`);
|
|
191
|
+
if (!dryRun && moving.length) {
|
|
192
|
+
const path = join(dir, 'package.json');
|
|
193
|
+
writeFileSync(path, writeBumps(readFileSync(path, 'utf8'), bumps));
|
|
194
|
+
await install(dir, detectPackageManager(dir));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// A core this command cannot drive is handed, with the rest, to the command it came with.
|
|
199
|
+
const coreDir = coreDirFrom(dir);
|
|
200
|
+
const coreVersion = coreDir ? readJson(join(coreDir, 'package.json'))?.version : null;
|
|
201
|
+
const command = !dryRun && coreVersion && !satisfies(coreVersion) ? projectCommand(coreDir) : null;
|
|
202
|
+
const commandVersion = command ? readJson(join(command, 'package.json'))?.version : null;
|
|
203
|
+
if (command && commandVersion && commandVersion !== VERSION) {
|
|
204
|
+
log.step(`Going on with the project's amxts ${commandVersion}`);
|
|
205
|
+
const args = [join(command, 'bin', 'amxts.mjs'), 'upgrade', '--to', version, ...(server ? [] : ['--no-server'])];
|
|
206
|
+
const result = spawnSync(process.execPath, args, { cwd: dir, stdio: 'inherit', env: { ...process.env, AMXTS_UPGRADE_FROM: JSON.stringify(from) } });
|
|
207
|
+
process.exitCode = result.status ?? 1;
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// With the new core installed - or, in a dry run, the installed one when it is already that version.
|
|
212
|
+
const core = coreDir && (!dryRun || from[CORE] === version) ? await loadCore(coreDir) : null;
|
|
213
|
+
/** @type {Outcome} */
|
|
214
|
+
const outcome = {
|
|
215
|
+
version,
|
|
216
|
+
packages: bumps.map(bump => ({ name: bump.name, spec: bump.spec, from: from[bump.name], version: bump.version, to: bump.to })),
|
|
217
|
+
code: null,
|
|
218
|
+
build: null,
|
|
219
|
+
server: null,
|
|
220
|
+
dryRun,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
if (core) {
|
|
224
|
+
log.step(dryRun ? 'Code: what the rewrites would change' : 'Code');
|
|
225
|
+
const report = rewriteCode(core, dryRun);
|
|
226
|
+
outcome.code = { files: [...new Set(report.changes.map(change => change.file))], left: report.left };
|
|
227
|
+
} else {
|
|
228
|
+
log.step(`Code: rewritten by @amxts/core ${version} once it is installed`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (core && !dryRun) {
|
|
232
|
+
log.step('Build');
|
|
233
|
+
await prepare(core, { quiet: true });
|
|
234
|
+
outcome.build = runTask(core, 'build') === 0 ? 'built' : 'failed';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (server) {
|
|
238
|
+
outcome.server = core
|
|
239
|
+
? await updateServer(core.api, dir, { dryRun })
|
|
240
|
+
: { ok: true, line: `the module of amxts ${version} goes in once @amxts/core ${version} is installed` };
|
|
241
|
+
}
|
|
242
|
+
console.log(['', ...summary(outcome)].join('\n'));
|
|
243
|
+
return outcome;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The core of `core` - a version, or `latest` - and the version each @amxts/
|
|
248
|
+
* package of the project goes to: the core's own, the others' newest that
|
|
249
|
+
* works with it. Only a package whose spec moves is asked for, and nothing
|
|
250
|
+
* when the project has that core already and nothing else moves.
|
|
251
|
+
*/
|
|
252
|
+
async function newest(registry, dir, pkg, core) {
|
|
253
|
+
const names = amxtsPackages(pkg).filter(each => each.name !== CORE && MOVABLE.test(each.spec.trim())).map(each => each.name);
|
|
254
|
+
if (!names.length && installedVersion(dir, CORE) === core) return { version: core, versions: { [CORE]: core } };
|
|
255
|
+
const found = await versionsFor(registry, core, names);
|
|
256
|
+
return { version: found.core, versions: { ...found.versions, [CORE]: found.core } };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The versions the command that handed the upgrade on installed: the core's, and each package's. */
|
|
260
|
+
function installed(dir, pkg, version) {
|
|
261
|
+
return { version, versions: Object.fromEntries(amxtsPackages(pkg).map(each => [each.name, installedVersion(dir, each.name)])) };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function needCore() {
|
|
265
|
+
throw new CliError('@amxts/core is not installed in this project', 'Run amxts upgrade without --server-only first.');
|
|
266
|
+
}
|
|
@@ -47,7 +47,7 @@ export default defineConfig({
|
|
|
47
47
|
A plugin of a project that lists the module uses it as `{{camel}}`, without an import:
|
|
48
48
|
|
|
49
49
|
```ts
|
|
50
|
-
server.addCommand("/hello", {{camel}}.greet);
|
|
50
|
+
server.addCommand("/hello", ({ player }) => {{camel}}.greet(player));
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
| Function | What it does |
|
|
@@ -47,7 +47,7 @@ export default defineConfig({
|
|
|
47
47
|
Плагин проекта, в котором модуль указан, пользуется им как `{{camel}}`, без импорта:
|
|
48
48
|
|
|
49
49
|
```ts
|
|
50
|
-
server.addCommand("/hello", {{camel}}.greet);
|
|
50
|
+
server.addCommand("/hello", ({ player }) => {{camel}}.greet(player));
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
| Функция | Что делает |
|
|
@@ -15,7 +15,8 @@ Counter-Strike 1.6 plugins in TypeScript, built with [amxts](https://amxts.githu
|
|
|
15
15
|
- `{{amxts}} module add <name>` - installs a module and lists it in `amxts.config.ts`
|
|
16
16
|
|
|
17
17
|
`AMXTS_SERVER` in `.env` is the `addons/amxts` folder of the server `dev`
|
|
18
|
-
deploys to:
|
|
18
|
+
deploys to. The server's own folder or its `cstrike` will do too: the build
|
|
19
|
+
finds `cstrike/addons/amxts` under it.
|
|
19
20
|
|
|
20
21
|
```sh
|
|
21
22
|
AMXTS_SERVER=D:/hlds/cstrike/addons/amxts
|
|
@@ -3,37 +3,49 @@ plugin({ name: "Hello", version: "1.0.0", author: "{{author}}", description: "Th
|
|
|
3
3
|
// #if config-core
|
|
4
4
|
// configs/hello.yaml (or hello.json) says what a player is greeted with -
|
|
5
5
|
// "greeting: Hi" - and "Welcome" is what it is when the file does not say.
|
|
6
|
-
|
|
6
|
+
interface HelloConfig {
|
|
7
|
+
greeting: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { greeting } = configs.load<HelloConfig>("hello", { greeting: "Welcome" });
|
|
7
11
|
// #endif
|
|
8
12
|
// #if !config-core
|
|
9
13
|
const greeting = "Welcome";
|
|
10
14
|
// #endif
|
|
11
15
|
|
|
12
|
-
server.addCommand("/hp", sayHp);
|
|
13
|
-
server.addEventListener("
|
|
16
|
+
server.addCommand("/hp", ({ player }) => sayHp(player));
|
|
17
|
+
server.addEventListener("putInServer", event => print(0, `${greeting}, ${event.player.name}!`));
|
|
14
18
|
// #if menu-core
|
|
15
19
|
|
|
16
|
-
// A menu made in code: its title and items can be functions of the player.
|
|
17
|
-
const hello = menus.create("HELLO", { title: player => `Hello, ${player.name}` });
|
|
18
|
-
hello.addItem("Wave",
|
|
19
|
-
hello.addItem(
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// A menu made in code: its title and items can be functions of the player it is shown to.
|
|
21
|
+
const hello = menus.create("HELLO", { title: ({ player }) => `Hello, ${player.name}` });
|
|
22
|
+
hello.addItem({ title: "Wave", onSelect: ({ player }) => print(0, `${player.name} waves`) });
|
|
23
|
+
hello.addItem({
|
|
24
|
+
title: ({ player }) => `Heal (${player.health} HP)`,
|
|
25
|
+
visible: ({ player }) => player.health < 100,
|
|
26
|
+
onSelect: ({ player }) => {
|
|
27
|
+
player.health = 100;
|
|
28
|
+
},
|
|
22
29
|
});
|
|
23
30
|
|
|
24
|
-
server.addCommand("/menu", player => hello.show(player));
|
|
31
|
+
server.addCommand("/menu", ({ player }) => hello.show(player));
|
|
25
32
|
// #endif
|
|
33
|
+
// #if !menu-core
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
35
|
+
// A menu: its title and items can be functions of the player it is shown to.
|
|
36
|
+
const hello = new Menu(({ player }) => `Hello, ${player.name}`);
|
|
37
|
+
hello.addItem({ title: "Wave", onSelect: ({ player }) => print(0, `${player.name} waves`) });
|
|
38
|
+
hello.addItem({
|
|
39
|
+
title: ({ player }) => `Heal (${player.health} HP)`,
|
|
40
|
+
visible: ({ player }) => player.health < 100,
|
|
41
|
+
onSelect: ({ player }) => {
|
|
42
|
+
player.health = 100;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
46
|
+
server.addCommand("/menu", ({ player }) => hello.show(player));
|
|
47
|
+
// #endif
|
|
35
48
|
|
|
36
|
-
function
|
|
37
|
-
player.
|
|
49
|
+
function sayHp(player: Player) {
|
|
50
|
+
print(player, `${player.name}, your HP: ${player.health}`);
|
|
38
51
|
}
|
|
39
|
-
// #endif
|