@amxts/cli 0.1.0 → 0.2.1
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 +8 -6
- package/src/init.mjs +39 -9
- package/src/main.mjs +39 -6
- package/src/module.mjs +7 -3
- package/src/pm.mjs +74 -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/module/test/tsconfig.json +1 -1
- package/templates/module/test/{{name}}.test.ts +5 -0
- package/templates/project/README.md +2 -1
- package/templates/project/plugins/hello.ts +32 -20
- package/templates/project/test/hello.test.ts +10 -0
- package/templates/project/test/tsconfig.json +1 -1
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,14 +10,14 @@
|
|
|
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';
|
|
17
17
|
import { shortName, specFor } from './catalog.mjs';
|
|
18
18
|
import { CLI_DIR, CORE_RANGE, FROM_SOURCE, needLocalCore } from './core.mjs';
|
|
19
19
|
import { LINT_DEPENDENCIES, LINT_SCRIPTS } from './lint.mjs';
|
|
20
|
-
import { detectPackageManager, execAmxts, installArgs, runScript } from './pm.mjs';
|
|
20
|
+
import { detectPackageManager, execAmxts, installArgs, runScript, testRunner } from './pm.mjs';
|
|
21
21
|
import { copyTemplate, gitAuthor, writeFile } from './template.mjs';
|
|
22
22
|
import { c, CliError, log } from './ui.mjs';
|
|
23
23
|
|
|
@@ -57,9 +57,11 @@ export async function initModule(options) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const pm = options.pm ?? detectPackageManager();
|
|
60
|
+
const runner = testRunner(pm);
|
|
61
|
+
values['test.types'] = runner.types;
|
|
60
62
|
const localCore = (options.local ?? FROM_SOURCE) ? await needLocalCore() : null;
|
|
61
63
|
|
|
62
|
-
const written = copyTemplate(join(CLI_DIR, 'templates', 'module'), dir, values, { natives }, path => path === 'src/natives.ts' && !natives);
|
|
64
|
+
const written = copyTemplate(join(CLI_DIR, 'templates', 'module'), dir, values, { natives, bun: pm === 'bun' }, path => path === 'src/natives.ts' && !natives);
|
|
63
65
|
|
|
64
66
|
const amxts = { module: 'src/index.ts' };
|
|
65
67
|
if (natives) Object.assign(amxts, { natives: 'src/natives.ts', include: `include/${values.include}.inc` });
|
|
@@ -78,15 +80,15 @@ export async function initModule(options) {
|
|
|
78
80
|
scripts: {
|
|
79
81
|
build: 'amxts build',
|
|
80
82
|
check: 'amxts check',
|
|
81
|
-
test:
|
|
83
|
+
test: runner.script,
|
|
82
84
|
...LINT_SCRIPTS,
|
|
83
85
|
},
|
|
84
86
|
peerDependencies: { '@amxts/core': CORE_RANGE },
|
|
85
87
|
devDependencies: Object.fromEntries(Object.entries({
|
|
86
88
|
...(localCore ? { '@amxts/core': specFor({ name: '@amxts/core', dir: localCore.dir }, dir, pm) } : {}),
|
|
87
89
|
...LINT_DEPENDENCIES,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
...runner.devDependencies,
|
|
91
|
+
typescript: '^6.0.3',
|
|
90
92
|
}).sort(([a], [b]) => a.localeCompare(b))),
|
|
91
93
|
}, null, '\t')}\n`);
|
|
92
94
|
written.push('package.json');
|
package/src/init.mjs
CHANGED
|
@@ -18,15 +18,21 @@ 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, testRunner, 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
|
+
knip: '^6.39.0',
|
|
30
36
|
};
|
|
31
37
|
|
|
32
38
|
const DEFAULT_DIR = 'my-server';
|
|
@@ -144,7 +150,8 @@ export async function initProject(options) {
|
|
|
144
150
|
const git = hasGit && (options.git ?? (interactive
|
|
145
151
|
? answer(await p.confirm({ message: 'Initialize a git repository?', initialValue: true }))
|
|
146
152
|
: true));
|
|
147
|
-
|
|
153
|
+
// Its folder, cstrike or addons/amxts: .env keeps the addons/amxts.
|
|
154
|
+
const server = options.server === undefined ? (interactive ? answer(await askServer(pm)) : '') : serverFolder(options.server);
|
|
148
155
|
|
|
149
156
|
// 7. Which server the project is for: its includes say, when it has them
|
|
150
157
|
// (addons/amxmodx/scripting/include beside AMXTS_SERVER); else asked.
|
|
@@ -179,6 +186,7 @@ export async function initProject(options) {
|
|
|
179
186
|
// The files.
|
|
180
187
|
const amxts = execAmxts(pm);
|
|
181
188
|
const install = installArgs(pm).join(' ');
|
|
189
|
+
const runner = testRunner(pm);
|
|
182
190
|
const values = {
|
|
183
191
|
'name': packageName(root),
|
|
184
192
|
'author': gitAuthor(),
|
|
@@ -191,15 +199,18 @@ export async function initProject(options) {
|
|
|
191
199
|
'run.typecheck': runScript(pm, 'typecheck'),
|
|
192
200
|
'run.test': runScript(pm, 'test'),
|
|
193
201
|
'run.lint': runScript(pm, 'lint'),
|
|
202
|
+
'test.types': runner.types,
|
|
194
203
|
};
|
|
195
|
-
const flags = { lint, ...Object.fromEntries(installed.map(module => [shortName(module.name), true])) };
|
|
204
|
+
const flags = { lint, bun: pm === 'bun', ...Object.fromEntries(installed.map(module => [shortName(module.name), true])) };
|
|
196
205
|
const written = copyTemplate(join(CLI_DIR, 'templates', 'project'), root, values, flags, path => LINT_FILES.includes(path) && !lint);
|
|
197
206
|
|
|
198
207
|
const core = { name: '@amxts/core', dir: localCore?.dir ?? null };
|
|
208
|
+
const versions = await moduleVersions(installed);
|
|
199
209
|
const devDependencies = {
|
|
200
210
|
[core.name]: specFor(core, root, pm, CORE_RANGE),
|
|
201
|
-
...Object.fromEntries(installed.map(module => [module.name, specFor(module, root, pm)])),
|
|
211
|
+
...Object.fromEntries(installed.map(module => [module.name, specFor(module, root, pm, versions[module.name] ? `^${versions[module.name]}` : 'latest')])),
|
|
202
212
|
...TOOLS,
|
|
213
|
+
...runner.devDependencies,
|
|
203
214
|
...(lint ? LINT_DEPENDENCIES : {}),
|
|
204
215
|
};
|
|
205
216
|
writeFile(join(root, 'package.json'), `${JSON.stringify({
|
|
@@ -212,7 +223,7 @@ export async function initProject(options) {
|
|
|
212
223
|
dev: 'amxts dev',
|
|
213
224
|
build: 'amxts build',
|
|
214
225
|
typecheck: 'amxts typecheck',
|
|
215
|
-
test:
|
|
226
|
+
test: runner.script,
|
|
216
227
|
...(lint ? LINT_SCRIPTS : {}),
|
|
217
228
|
},
|
|
218
229
|
devDependencies: Object.fromEntries(Object.entries(devDependencies).sort(([a], [b]) => a.localeCompare(b))),
|
|
@@ -245,7 +256,7 @@ export async function initProject(options) {
|
|
|
245
256
|
// A spinner in a terminal; in a log (CI, a pipe) one line before and one after.
|
|
246
257
|
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
258
|
spinner.start(`Installing dependencies with ${pm}`);
|
|
248
|
-
const result = await
|
|
259
|
+
const result = await installLinked(installArgs(pm), linkedFolders(root), { cwd: root, quiet: true });
|
|
249
260
|
if (result.code === 0) {
|
|
250
261
|
pinLatest(root);
|
|
251
262
|
installedOk = true;
|
|
@@ -301,6 +312,25 @@ export async function initProject(options) {
|
|
|
301
312
|
if (options.install !== false && pmVersion && !installedOk) process.exitCode = 1;
|
|
302
313
|
}
|
|
303
314
|
|
|
315
|
+
/**
|
|
316
|
+
* The modules from the registry, each at its newest version that works with
|
|
317
|
+
* the newest core of CORE_RANGE - the core the project gets; a module from a
|
|
318
|
+
* folder, or a package named with its version, is left to its spec. When the
|
|
319
|
+
* registry does not answer, none: `latest`, and the install says why.
|
|
320
|
+
* @param {import('./catalog.mjs').ModuleRef[]} modules
|
|
321
|
+
* @returns {Promise<Record<string, string>>} the versions, by name
|
|
322
|
+
*/
|
|
323
|
+
async function moduleVersions(modules) {
|
|
324
|
+
const names = modules.filter(module => !module.dir && !/^@?[^@]+@/.test(module.name)).map(module => module.name);
|
|
325
|
+
if (!names.length) return {};
|
|
326
|
+
try {
|
|
327
|
+
return (await versionsFor(npmRegistry(process.cwd()), CORE_RANGE, names)).versions;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
if (error instanceof CliError && !/npm view/.test(error.message)) throw error;
|
|
330
|
+
return {};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
304
334
|
/** `"latest"` in package.json, after the install, as the version it installed: `^1.6.2`. */
|
|
305
335
|
function pinLatest(root) {
|
|
306
336
|
const path = join(root, 'package.json');
|
package/src/main.mjs
CHANGED
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
// It runs on Node. Commands that make or change a project (init, module,
|
|
7
7
|
// info) run here; the ones that compile (build, dev, typecheck, check,
|
|
8
8
|
// prepare) are tasks of the project's core, which runs them on Bun
|
|
9
|
-
// (core.mjs); test runs bun test.
|
|
9
|
+
// (core.mjs); test runs the project's Vitest, or bun test.
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
10
12
|
import process from 'node:process';
|
|
11
13
|
import { GLOBAL_FLAGS, parseArgs } from './args.mjs';
|
|
12
14
|
import { needBun, needProject, projectCore, runTask, runTaskOrExit, VERSION } from './core.mjs';
|
|
13
15
|
import { prepare, TARGETS } from './includes.mjs';
|
|
14
16
|
import { detectPackageManager, PACKAGE_MANAGERS, run } from './pm.mjs';
|
|
17
|
+
import { serverMismatch } from './server-update.mjs';
|
|
15
18
|
import { ensureServer } from './server.mjs';
|
|
16
19
|
import { SYSTEMS } from './system.mjs';
|
|
17
20
|
import { banner, c, CliError, closest, log, report } from './ui.mjs';
|
|
@@ -19,6 +22,12 @@ import { banner, c, CliError, closest, log, report } from './ui.mjs';
|
|
|
19
22
|
/** --os, for the commands that compile: the plugins are built for that system. */
|
|
20
23
|
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
24
|
|
|
25
|
+
/** The line for a server whose module is of another release than the project's core. */
|
|
26
|
+
function warnServer(core, os) {
|
|
27
|
+
const line = serverMismatch(core, needProject(), os);
|
|
28
|
+
if (line) log.warn(line);
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
/** --os as the build task takes it. */
|
|
23
32
|
function osArgs(values) {
|
|
24
33
|
if (values.os === undefined) return [];
|
|
@@ -31,7 +40,7 @@ const INIT_FLAGS = {
|
|
|
31
40
|
modules: { type: 'string', value: 'list', description: 'Modules to add, comma-separated: menu-core,config-core ("" for none)' },
|
|
32
41
|
lint: { type: 'boolean', description: 'oxlint and oxfmt, with @antfu/eslint-config\'s rules (--no-lint to leave them out)' },
|
|
33
42
|
git: { type: 'boolean', description: 'git init (--no-git to skip)' },
|
|
34
|
-
server: { type: 'string', value: 'path', description: 'The server
|
|
43
|
+
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
44
|
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
45
|
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
46
|
install: { type: 'boolean', description: 'Install the dependencies (--no-install to skip)' },
|
|
@@ -87,7 +96,10 @@ export const COMMANDS = {
|
|
|
87
96
|
const core = await projectCore();
|
|
88
97
|
banner(core.version, 'dev');
|
|
89
98
|
// The container reads dist/ where it is: nothing to deploy, and AMXTS_SERVER is not asked.
|
|
90
|
-
if (!values.docker)
|
|
99
|
+
if (!values.docker) {
|
|
100
|
+
await ensureServer(needProject(), detectPackageManager());
|
|
101
|
+
warnServer(core, osArgs(values));
|
|
102
|
+
}
|
|
91
103
|
await prepare(core, { quiet: true });
|
|
92
104
|
if (values.docker) runTaskOrExit(core, 'build', ['--watch', '--docker', '--os', 'linux']);
|
|
93
105
|
else runTaskOrExit(core, 'build', ['--deploy', '--watch', ...osArgs(values)]);
|
|
@@ -106,6 +118,7 @@ export const COMMANDS = {
|
|
|
106
118
|
const os = osArgs(values);
|
|
107
119
|
const core = await projectCore();
|
|
108
120
|
banner(core.version, 'build');
|
|
121
|
+
warnServer(core, os);
|
|
109
122
|
if (values.deploy) await ensureServer(needProject(), detectPackageManager());
|
|
110
123
|
await prepare(core, { quiet: true });
|
|
111
124
|
runTaskOrExit(core, 'build', [...(values.deploy ? ['--deploy'] : []), ...(values.watch ? ['--watch'] : []), ...os]);
|
|
@@ -126,12 +139,17 @@ export const COMMANDS = {
|
|
|
126
139
|
},
|
|
127
140
|
},
|
|
128
141
|
test: {
|
|
129
|
-
description: 'Run the project\'s tests on a fake server (bun test)',
|
|
130
|
-
usage: 'amxts test [
|
|
142
|
+
description: 'Run the project\'s tests on a fake server (Vitest when the project has it, else bun test)',
|
|
143
|
+
usage: 'amxts test [test runner options]',
|
|
131
144
|
passThrough: true,
|
|
132
145
|
examples: ['amxts test', 'amxts test hello', 'amxts test --watch'],
|
|
133
146
|
async run({ raw }) {
|
|
134
|
-
const
|
|
147
|
+
const core = await projectCore();
|
|
148
|
+
// A project with Vitest tests on Node; any other on bun test.
|
|
149
|
+
const vitest = join(needProject(), 'node_modules', 'vitest', 'vitest.mjs');
|
|
150
|
+
const result = await run(existsSync(vitest)
|
|
151
|
+
? [process.execPath, vitest, ...(raw.includes('--watch') ? [] : ['run']), ...raw]
|
|
152
|
+
: [needBun(core, 'runs tests'), 'test', ...raw]);
|
|
135
153
|
process.exitCode = result.code;
|
|
136
154
|
},
|
|
137
155
|
},
|
|
@@ -168,6 +186,21 @@ export const COMMANDS = {
|
|
|
168
186
|
await prepare(await projectCore());
|
|
169
187
|
},
|
|
170
188
|
},
|
|
189
|
+
upgrade: {
|
|
190
|
+
description: 'Move the project to the latest amxts: its packages, its code, the build and the server',
|
|
191
|
+
usage: 'amxts upgrade [--to <version>] [--no-server] [--server-only] [--dry-run]',
|
|
192
|
+
flags: {
|
|
193
|
+
to: { type: 'string', value: 'version', description: 'The version to move to (the latest on the registry by default)' },
|
|
194
|
+
server: { type: 'boolean', description: 'Update the server in AMXTS_SERVER: its module, from the release (--no-server to leave it)' },
|
|
195
|
+
serverOnly: { type: 'boolean', description: 'Only update the server - once it is stopped, when the upgrade found it running' },
|
|
196
|
+
dryRun: { type: 'boolean', description: 'Say what would change, and change nothing' },
|
|
197
|
+
},
|
|
198
|
+
examples: ['amxts upgrade', 'amxts upgrade --dry-run', 'amxts upgrade --to 0.2.0 --no-server', 'amxts upgrade --server-only'],
|
|
199
|
+
async run({ values }) {
|
|
200
|
+
const { upgrade } = await import('./upgrade.mjs');
|
|
201
|
+
await upgrade({ to: values.to, server: values.server !== false, serverOnly: values.serverOnly, dryRun: values.dryRun });
|
|
202
|
+
},
|
|
203
|
+
},
|
|
171
204
|
info: {
|
|
172
205
|
description: 'Versions and settings, for a bug report',
|
|
173
206
|
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,75 @@ 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
|
+
* What a new project's or module's tests run on: `bun test` with Bun as the
|
|
80
|
+
* package manager, Vitest on Node with any other. `types` is the test
|
|
81
|
+
* folder's tsconfig `types`.
|
|
82
|
+
*/
|
|
83
|
+
export function testRunner(pm) {
|
|
84
|
+
return pm === 'bun'
|
|
85
|
+
? { script: 'amxts test', types: 'bun', devDependencies: { '@types/bun': '^1.4.2' } }
|
|
86
|
+
: { script: 'vitest run', types: 'node', devDependencies: { '@types/node': '^24.0.0', 'vitest': '^5.0.3' } };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The folders a project's package.json takes packages from - `link:` and
|
|
91
|
+
* `file:` specs: the core and the modules of this machine (--local).
|
|
92
|
+
* @param {string} projectDir
|
|
93
|
+
* @param {string[]} [more] folders about to be added
|
|
94
|
+
* @returns {string[]} the folders
|
|
95
|
+
*/
|
|
96
|
+
export function linkedFolders(projectDir, more = []) {
|
|
97
|
+
let json = {};
|
|
98
|
+
try {
|
|
99
|
+
json = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8'));
|
|
100
|
+
} catch {}
|
|
101
|
+
const specs = Object.values({ ...json.dependencies, ...json.devDependencies });
|
|
102
|
+
const linked = specs.filter(spec => /^(?:link|file):(?:[./]|[a-z]:)/i.test(spec)).map(spec => resolve(projectDir, spec.replace(/^\w+:/, '')));
|
|
103
|
+
return [...new Set([...linked, ...more])];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Runs an install and leaves the `node_modules/.bin` of each linked folder
|
|
108
|
+
* as it was. yarn 1 installs a `link:` folder's dependencies into the
|
|
109
|
+
* project and points that folder's own `.bin` at them: the core's `tsc`,
|
|
110
|
+
* `amxts` and `bun` would start the project's copies.
|
|
111
|
+
* @param {string[]} argv
|
|
112
|
+
* @param {string[]} linked the linked folders, `linkedFolders`
|
|
113
|
+
* @param {Parameters<typeof run>[1]} [options]
|
|
114
|
+
*/
|
|
115
|
+
export async function installLinked(argv, linked, options) {
|
|
116
|
+
const bins = linked.map(dir => join(dir, 'node_modules', '.bin'));
|
|
117
|
+
const saved = bins.map(binsOf);
|
|
118
|
+
const result = await run(argv, options);
|
|
119
|
+
bins.forEach((dir, i) => putBack(dir, saved[i]));
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A `.bin` folder's entries: a link's target, or a file's bytes. Null when there is none. */
|
|
124
|
+
function binsOf(dir) {
|
|
125
|
+
if (!existsSync(dir)) return null;
|
|
126
|
+
return new Map(readdirSync(dir).map((name) => {
|
|
127
|
+
const path = join(dir, name);
|
|
128
|
+
const stat = lstatSync(path);
|
|
129
|
+
return [name, stat.isSymbolicLink() ? { link: readlinkSync(path) } : { data: readFileSync(path), mode: stat.mode }];
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Writes a `.bin` folder back as `binsOf` read it; what the install added goes. */
|
|
134
|
+
function putBack(dir, saved) {
|
|
135
|
+
if (!saved) return;
|
|
136
|
+
mkdirSync(dir, { recursive: true });
|
|
137
|
+
for (const name of readdirSync(dir)) rmSync(join(dir, name), { force: true });
|
|
138
|
+
for (const [name, entry] of saved) {
|
|
139
|
+
if (entry.link) symlinkSync(entry.link, join(dir, name));
|
|
140
|
+
else writeFileSync(join(dir, name), entry.data, { mode: entry.mode });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 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
145
|
export function commandLine(argv) {
|
|
80
|
-
return argv.map(arg => (
|
|
146
|
+
return argv.map(arg => (/[\s^&|<>]/.test(arg) ? `"${arg}"` : arg)).join(' ');
|
|
81
147
|
}
|
|
82
148
|
|
|
83
149
|
/**
|
|
@@ -85,7 +151,7 @@ export function commandLine(argv) {
|
|
|
85
151
|
* .cmd files; `quiet` collects the output instead of showing it, for a
|
|
86
152
|
* spinner, and hands it back for an error.
|
|
87
153
|
*
|
|
88
|
-
* @returns {Promise<{ code: number, output: string }>} its exit code, and its output when quiet
|
|
154
|
+
* @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
155
|
*/
|
|
90
156
|
export function run(argv, { cwd = process.cwd(), quiet = false, env } = {}) {
|
|
91
157
|
return new Promise((done) => {
|
|
@@ -99,13 +165,15 @@ export function run(argv, { cwd = process.cwd(), quiet = false, env } = {}) {
|
|
|
99
165
|
windowsHide: true,
|
|
100
166
|
});
|
|
101
167
|
let output = '';
|
|
168
|
+
let stdout = '';
|
|
102
169
|
child.stdout?.on('data', (chunk) => {
|
|
103
170
|
output += chunk;
|
|
171
|
+
stdout += chunk;
|
|
104
172
|
});
|
|
105
173
|
child.stderr?.on('data', (chunk) => {
|
|
106
174
|
output += chunk;
|
|
107
175
|
});
|
|
108
|
-
child.on('error', error => done({ code: 127, output: `${output}${error.message}
|
|
109
|
-
child.on('close', code => done({ code: code ?? 1, output }));
|
|
176
|
+
child.on('error', error => done({ code: 127, output: `${output}${error.message}`, stdout }));
|
|
177
|
+
child.on('close', code => done({ code: code ?? 1, output, stdout }));
|
|
110
178
|
});
|
|
111
179
|
}
|
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
|
+
}
|