@amxts/cli 0.1.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/LICENSE +21 -0
- package/README.md +20 -0
- package/bin/amxts.mjs +8 -0
- package/package.json +45 -0
- package/src/args.mjs +62 -0
- package/src/catalog.mjs +196 -0
- package/src/config.mjs +191 -0
- package/src/core.mjs +272 -0
- package/src/includes.mjs +181 -0
- package/src/info.mjs +85 -0
- package/src/init-module.mjs +117 -0
- package/src/init.mjs +316 -0
- package/src/lint.mjs +26 -0
- package/src/main.mjs +273 -0
- package/src/module.mjs +150 -0
- package/src/modules.json +47 -0
- package/src/pm.mjs +111 -0
- package/src/server.mjs +72 -0
- package/src/system.mjs +32 -0
- package/src/template.mjs +74 -0
- package/src/ui.mjs +101 -0
- package/templates/module/LICENSE +21 -0
- package/templates/module/README.md +81 -0
- package/templates/module/README.ru.md +81 -0
- package/templates/module/_gitattributes +5 -0
- package/templates/module/_gitignore +11 -0
- package/templates/module/_oxfmtrc.json +18 -0
- package/templates/module/_oxlintrc.json +784 -0
- package/templates/module/assets/logo.svg +8 -0
- package/templates/module/playground/amxts.config.ts +4 -0
- package/templates/module/playground/plugins/welcome.ts +3 -0
- package/templates/module/playground/tsconfig.json +3 -0
- package/templates/module/src/index.ts +25 -0
- package/templates/module/src/natives.ts +13 -0
- package/templates/module/src/types.ts +11 -0
- package/templates/module/test/tsconfig.json +13 -0
- package/templates/module/test/{{name}}.test.ts +25 -0
- package/templates/module/tsconfig.json +10 -0
- package/templates/project/README.md +28 -0
- package/templates/project/_gitattributes +7 -0
- package/templates/project/_gitignore +7 -0
- package/templates/project/_oxfmtrc.json +18 -0
- package/templates/project/_oxlintrc.json +784 -0
- package/templates/project/_vscode/extensions.json +3 -0
- package/templates/project/_vscode/settings.json +13 -0
- package/templates/project/amxts.config.ts +24 -0
- package/templates/project/knip.json +4 -0
- package/templates/project/plugins/hello.ts +39 -0
- package/templates/project/test/hello.test.ts +37 -0
- package/templates/project/test/tsconfig.json +13 -0
- package/templates/project/tsconfig.json +3 -0
package/src/module.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// `amxts module add` installs a module and lists it in amxts.config.ts in one
|
|
2
|
+
// step; `amxts module list` shows what the config lists and what is installed:
|
|
3
|
+
//
|
|
4
|
+
// amxts module add menu-core npm install -D @amxts/menu-core @amxts/config-core,
|
|
5
|
+
// both into modules in amxts.config.ts - config-core
|
|
6
|
+
// after menu-core, "// needed by menu-core" -
|
|
7
|
+
// then amxts prepare, which names them for the editor
|
|
8
|
+
// amxts module add @you/greeter any package from npm - with a warning
|
|
9
|
+
// when the amxts catalog does not list it
|
|
10
|
+
// amxts module add ../greeter a module from its folder
|
|
11
|
+
// amxts module list what the config lists, what is installed,
|
|
12
|
+
// what else the catalog has
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { configEntries, installTarget, loadCatalog, resolveModule, taglineOf, withRequired } from './catalog.mjs';
|
|
16
|
+
import { addToConfig, CONFIG_FILE, newConfig, readConfigModules } from './config.mjs';
|
|
17
|
+
import { FROM_SOURCE, installedCore, needLocalCore, needProject, readJson } from './core.mjs';
|
|
18
|
+
import { prepare } from './includes.mjs';
|
|
19
|
+
import { addArgs, commandLine, detectPackageManager, execAmxts, run, versionOf } from './pm.mjs';
|
|
20
|
+
import { c, CliError, log } from './ui.mjs';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string[]} names
|
|
24
|
+
* @param {{ local?: boolean, skipInstall?: boolean, skipConfig?: boolean }} options
|
|
25
|
+
*/
|
|
26
|
+
export async function moduleAdd(names, options = {}) {
|
|
27
|
+
if (names.length === 0) throw new CliError('Which module?', 'amxts module add menu-core (or a package name, or a folder)');
|
|
28
|
+
const dir = needProject();
|
|
29
|
+
const pm = detectPackageManager(dir);
|
|
30
|
+
|
|
31
|
+
// --local, the default when the project's core is a checkout: the official
|
|
32
|
+
// modules from the folders that core takes them from.
|
|
33
|
+
const core = await installedCore(dir);
|
|
34
|
+
const local = options.local ?? (core ? core.fromSource : FROM_SOURCE);
|
|
35
|
+
const [source, catalog] = await Promise.all([local && !core?.fromSource ? needLocalCore() : core, loadCatalog()]);
|
|
36
|
+
const localModules = local ? source.api.localModules() : {};
|
|
37
|
+
const from = { catalog, local, localModules };
|
|
38
|
+
const wanted = names.map(name => resolveModule(name, from));
|
|
39
|
+
for (const module of wanted.filter(each => !each.dir && !each.listed)) log.warn(`${module.name} is not in the amxts catalog: it is installed from npm as it is`);
|
|
40
|
+
const project = readJson(join(dir, 'package.json'));
|
|
41
|
+
const deps = { ...project?.dependencies, ...project?.devDependencies };
|
|
42
|
+
// What a module needs comes along, unless the project has it: yarn does
|
|
43
|
+
// not install peer dependencies by itself.
|
|
44
|
+
const all = withRequired(wanted, from);
|
|
45
|
+
const toInstall = all.filter(module => wanted.includes(module) || !deps[module.name]);
|
|
46
|
+
|
|
47
|
+
if (!options.skipInstall) {
|
|
48
|
+
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
|
+
const argv = addArgs(pm, toInstall.map(module => installTarget(module, dir, pm)));
|
|
50
|
+
log.step(`${commandLine(argv)}`);
|
|
51
|
+
const result = await run(argv, { cwd: dir });
|
|
52
|
+
if (result.code !== 0) throw new CliError(`${commandLine(argv)} failed`, 'The package manager says why, above.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Only a package that is a module goes into the config, with what it
|
|
56
|
+
// requires right after it.
|
|
57
|
+
const notModules = wanted.filter((module) => {
|
|
58
|
+
const json = readJson(join(dir, 'node_modules', module.name, 'package.json')) ?? (module.dir ? readJson(join(module.dir, 'package.json')) : null);
|
|
59
|
+
return json && !json.amxts?.module;
|
|
60
|
+
});
|
|
61
|
+
for (const module of notModules) log.warn(`${module.name} is not an amxts module (its package.json has no "amxts" field): installed, not added to ${CONFIG_FILE}`);
|
|
62
|
+
const entries = configEntries(all.filter(module => !notModules.includes(module)), wanted);
|
|
63
|
+
if (options.skipConfig || entries.length === 0) return;
|
|
64
|
+
if (!await addModules(dir, entries)) return;
|
|
65
|
+
|
|
66
|
+
// The editor config names the modules the config lists: prepared again
|
|
67
|
+
// now that it lists these - the install's own prepare ran before.
|
|
68
|
+
const projectCore = await installedCore(dir);
|
|
69
|
+
if (projectCore) await prepare(projectCore);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Lists modules in amxts.config.ts, a new one when there is none. Whether it changed.
|
|
74
|
+
* @param {string} dir
|
|
75
|
+
* @param {import('./catalog.mjs').ConfigEntry[]} entries
|
|
76
|
+
*/
|
|
77
|
+
async function addModules(dir, entries) {
|
|
78
|
+
const path = join(dir, CONFIG_FILE);
|
|
79
|
+
const names = entries.map(each => each.name).join(', ');
|
|
80
|
+
if (!existsSync(path)) {
|
|
81
|
+
writeFileSync(path, newConfig(entries));
|
|
82
|
+
log.success(`Created ${CONFIG_FILE} with ${names}`);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
const { text, added } = await addToConfig(readFileSync(path, 'utf8'), entries);
|
|
86
|
+
if (added.length === 0) {
|
|
87
|
+
log.info(`${CONFIG_FILE} lists ${names} already`);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
writeFileSync(path, text);
|
|
91
|
+
log.success(`Added ${added.map(name => c.bold(name)).join(', ')} to ${CONFIG_FILE}`);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Every installed package with an "amxts" field: node_modules and its scopes. */
|
|
96
|
+
function installedModules(dir) {
|
|
97
|
+
const found = new Map();
|
|
98
|
+
const add = (packageDir) => {
|
|
99
|
+
const json = readJson(join(packageDir, 'package.json'));
|
|
100
|
+
if (json?.name && json.amxts?.module && !found.has(json.name)) {
|
|
101
|
+
const requires = Object.keys(json.peerDependencies ?? {}).filter(name => name !== '@amxts/core');
|
|
102
|
+
found.set(json.name, { name: json.name, version: json.version ?? '', dir: packageDir, requires });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const modulesDir = join(dir, 'node_modules');
|
|
106
|
+
if (existsSync(modulesDir)) {
|
|
107
|
+
for (const name of readdirSync(modulesDir)) {
|
|
108
|
+
if (name.startsWith('.')) continue;
|
|
109
|
+
if (name.startsWith('@')) {
|
|
110
|
+
for (const inner of readdirSync(join(modulesDir, name))) add(join(modulesDir, name, inner));
|
|
111
|
+
} else {
|
|
112
|
+
add(join(modulesDir, name));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return found;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function moduleList() {
|
|
120
|
+
const dir = needProject();
|
|
121
|
+
const [listed, catalog] = await Promise.all([readConfigModules(dir), loadCatalog()]);
|
|
122
|
+
const names = listed ?? [];
|
|
123
|
+
const installed = installedModules(dir);
|
|
124
|
+
const available = catalog.filter(module => !installed.has(module.npm));
|
|
125
|
+
const add = `${execAmxts(detectPackageManager(dir))} module add`;
|
|
126
|
+
const pad = Math.max(10, ...[...names, ...installed.keys(), ...available.map(module => module.name)].map(name => name.length));
|
|
127
|
+
const row = (mark, name, rest) => console.log(` ${mark} ${name.padEnd(pad)} ${rest}`);
|
|
128
|
+
|
|
129
|
+
console.log(c.bold(listed === null ? `No ${CONFIG_FILE} here` : `In ${CONFIG_FILE}`));
|
|
130
|
+
if (listed && listed.length === 0) console.log(c.dim(' no modules yet'));
|
|
131
|
+
for (const name of names) {
|
|
132
|
+
const module = installed.get(name);
|
|
133
|
+
if (module) row(c.green('✔'), name, `${c.dim(module.version)} ${taglineOf(module.dir)}`);
|
|
134
|
+
else row(c.red('✖'), name, c.red(`not installed - ${add} ${name}`));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const others = [...installed.values()].filter(module => !names.includes(module.name));
|
|
138
|
+
if (others.length) {
|
|
139
|
+
console.log(c.bold('\nInstalled, not in the config'));
|
|
140
|
+
for (const module of others) {
|
|
141
|
+
const by = names.filter(name => installed.get(name)?.requires.includes(module.name));
|
|
142
|
+
row(c.yellow('•'), module.name, `${c.dim(module.version)} ${c.dim(by.length ? `comes along: ${by.join(', ')} needs it` : `not used - add it to modules to use it`)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (available.length) {
|
|
147
|
+
console.log(c.bold('\nIn the amxts catalog'));
|
|
148
|
+
for (const module of available) row(c.dim('○'), module.name, c.dim(`${module.description}${module.type === 'community' ? ' (community)' : ''} - ${add} ${module.name}`));
|
|
149
|
+
}
|
|
150
|
+
}
|
package/src/modules.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "config-core",
|
|
4
|
+
"npm": "@amxts/config-core",
|
|
5
|
+
"repo": "amxts/config-core",
|
|
6
|
+
"description": "Configs in INI, YAML or JSON, read into typed objects and written back",
|
|
7
|
+
"category": "config",
|
|
8
|
+
"type": "official",
|
|
9
|
+
"logo": "https://raw.githubusercontent.com/amxts/config-core/HEAD/assets/logo.svg",
|
|
10
|
+
"maintainers": [
|
|
11
|
+
{
|
|
12
|
+
"github": "kukson777"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"requires": []
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "menu-core",
|
|
19
|
+
"npm": "@amxts/menu-core",
|
|
20
|
+
"repo": "amxts/menu-core",
|
|
21
|
+
"description": "Menus from INI, YAML or JSON files, with conditions and placeholders",
|
|
22
|
+
"category": "ui",
|
|
23
|
+
"type": "official",
|
|
24
|
+
"logo": "https://raw.githubusercontent.com/amxts/menu-core/HEAD/assets/logo.svg",
|
|
25
|
+
"maintainers": [
|
|
26
|
+
{
|
|
27
|
+
"github": "kukson777"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"requires": ["@amxts/config-core"]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "resemiclip",
|
|
34
|
+
"npm": "@amxts/resemiclip",
|
|
35
|
+
"repo": "amxts/resemiclip",
|
|
36
|
+
"description": "Semiclip over the ReSemiclip module - who walks through whom, as a rule over two players",
|
|
37
|
+
"category": "gameplay",
|
|
38
|
+
"type": "official",
|
|
39
|
+
"logo": "https://raw.githubusercontent.com/amxts/resemiclip/HEAD/assets/logo.svg",
|
|
40
|
+
"maintainers": [
|
|
41
|
+
{
|
|
42
|
+
"github": "kukson777"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"requires": []
|
|
46
|
+
}
|
|
47
|
+
]
|
package/src/pm.mjs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// The package manager a project uses - npm, pnpm, yarn or bun - and the
|
|
2
|
+
// commands each one spells its own way.
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
|
|
8
|
+
export const PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun'];
|
|
9
|
+
|
|
10
|
+
const LOCKFILES = [
|
|
11
|
+
['bun.lock', 'bun'],
|
|
12
|
+
['bun.lockb', 'bun'],
|
|
13
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
14
|
+
['yarn.lock', 'yarn'],
|
|
15
|
+
['package-lock.json', 'npm'],
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/** The package manager that started this process - `npm create`, `pnpm dlx`, `bunx` - from its user agent. */
|
|
19
|
+
export function packageManagerOfAgent(agent = process.env.npm_config_user_agent ?? '') {
|
|
20
|
+
const name = agent.split('/')[0];
|
|
21
|
+
return PACKAGE_MANAGERS.includes(name) ? name : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The project's package manager: its lockfile, its package.json's
|
|
26
|
+
* "packageManager", the one running this command, else npm.
|
|
27
|
+
*/
|
|
28
|
+
export function detectPackageManager(dir = process.cwd()) {
|
|
29
|
+
for (let at = resolve(dir); ; at = dirname(at)) {
|
|
30
|
+
for (const [file, pm] of LOCKFILES) {
|
|
31
|
+
if (existsSync(join(at, file))) return pm;
|
|
32
|
+
}
|
|
33
|
+
const pkg = join(at, 'package.json');
|
|
34
|
+
if (existsSync(pkg)) {
|
|
35
|
+
try {
|
|
36
|
+
const field = JSON.parse(readFileSync(pkg, 'utf8')).packageManager;
|
|
37
|
+
const name = typeof field === 'string' ? field.split('@')[0] : '';
|
|
38
|
+
if (PACKAGE_MANAGERS.includes(name)) return name;
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
if (dirname(at) === at) break;
|
|
42
|
+
}
|
|
43
|
+
return packageManagerOfAgent() ?? 'npm';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The version of an installed package manager (or bun, git), or null when it is not there. */
|
|
47
|
+
export function versionOf(tool) {
|
|
48
|
+
// Through the shell on Windows, where npm and bun are .cmd files.
|
|
49
|
+
const run = process.platform === 'win32'
|
|
50
|
+
? spawnSync(`${tool} --version`, { encoding: 'utf8', shell: true, windowsHide: true })
|
|
51
|
+
: spawnSync(tool, ['--version'], { encoding: 'utf8' });
|
|
52
|
+
if (run.status !== 0 || !run.stdout) return null;
|
|
53
|
+
return run.stdout.trim().split('\n')[0].replace(/^v/, '');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `npm install`, `pnpm install`... - what installs a project's dependencies. */
|
|
57
|
+
export function installArgs(pm) {
|
|
58
|
+
return [pm, 'install'];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** What adds packages as dev dependencies. */
|
|
62
|
+
export function addArgs(pm, packages) {
|
|
63
|
+
if (pm === 'npm') return ['npm', 'install', '-D', ...packages];
|
|
64
|
+
if (pm === 'bun') return ['bun', 'add', '-d', ...packages];
|
|
65
|
+
return [pm, 'add', '-D', ...packages];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** How a package.json script is run: `npm run dev`, `pnpm dev`. */
|
|
69
|
+
export function runScript(pm, script) {
|
|
70
|
+
return pm === 'npm' || pm === 'bun' ? `${pm} run ${script}` : `${pm} ${script}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** How the project's amxts command is run: `npx amxts`, `pnpm amxts`. */
|
|
74
|
+
export function execAmxts(pm) {
|
|
75
|
+
return { npm: 'npx amxts', pnpm: 'pnpm amxts', yarn: 'yarn amxts', bun: 'bunx amxts' }[pm] ?? 'npx amxts';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A command line as it is shown: arguments with spaces quoted. */
|
|
79
|
+
export function commandLine(argv) {
|
|
80
|
+
return argv.map(arg => (/\s/.test(arg) ? `"${arg}"` : arg)).join(' ');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Runs a command. Through the shell on Windows, where npm and bun may be
|
|
85
|
+
* .cmd files; `quiet` collects the output instead of showing it, for a
|
|
86
|
+
* spinner, and hands it back for an error.
|
|
87
|
+
*
|
|
88
|
+
* @returns {Promise<{ code: number, output: string }>} its exit code, and its output when quiet
|
|
89
|
+
*/
|
|
90
|
+
export function run(argv, { cwd = process.cwd(), quiet = false, env } = {}) {
|
|
91
|
+
return new Promise((done) => {
|
|
92
|
+
const windows = process.platform === 'win32';
|
|
93
|
+
const [command, ...args] = windows ? [commandLine(argv)] : argv;
|
|
94
|
+
const child = spawn(command, args, {
|
|
95
|
+
cwd,
|
|
96
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
97
|
+
shell: windows,
|
|
98
|
+
stdio: quiet ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
99
|
+
windowsHide: true,
|
|
100
|
+
});
|
|
101
|
+
let output = '';
|
|
102
|
+
child.stdout?.on('data', (chunk) => {
|
|
103
|
+
output += chunk;
|
|
104
|
+
});
|
|
105
|
+
child.stderr?.on('data', (chunk) => {
|
|
106
|
+
output += chunk;
|
|
107
|
+
});
|
|
108
|
+
child.on('error', error => done({ code: 127, output: `${output}${error.message}` }));
|
|
109
|
+
child.on('close', code => done({ code: code ?? 1, output }));
|
|
110
|
+
});
|
|
111
|
+
}
|
package/src/server.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// The server a project deploys to: its addons/amxts folder, AMXTS_SERVER in
|
|
2
|
+
// the project's .env. `amxts init` asks for it; `dev` and `build --deploy`
|
|
3
|
+
// ask the same when it is not set - in a terminal. Elsewhere (CI, a pipe)
|
|
4
|
+
// nothing is asked, and the build says it is missing.
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
import * as p from '@clack/prompts';
|
|
9
|
+
import { setting } from './core.mjs';
|
|
10
|
+
import { runScript } from './pm.mjs';
|
|
11
|
+
import { c } from './ui.mjs';
|
|
12
|
+
|
|
13
|
+
/** Whether a command may ask: a terminal, and not CI. */
|
|
14
|
+
export function canAsk() {
|
|
15
|
+
return Boolean(process.stdin.isTTY) && !p.isCI();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Asks for the server's addons/amxts folder. `required`: an empty answer is
|
|
20
|
+
* not taken. The answer, or clack's cancel symbol on Ctrl+C.
|
|
21
|
+
* @param {string} pm the project's package manager, for the words
|
|
22
|
+
* @param {{ required?: boolean }} [options]
|
|
23
|
+
*/
|
|
24
|
+
export function askServer(pm, { required = false } = {}) {
|
|
25
|
+
return p.text({
|
|
26
|
+
message: `Where is the server? ${c.dim(`its addons/amxts folder, for ${runScript(pm, 'dev')}${required ? '' : ' - empty to skip'}`)}`,
|
|
27
|
+
placeholder: process.platform === 'win32' ? 'D:/hlds/cstrike/addons/amxts' : '/srv/hlds/cstrike/addons/amxts',
|
|
28
|
+
defaultValue: '',
|
|
29
|
+
validate: value => (required && !value?.trim() ? 'The plugins are deployed there: the server\'s addons/amxts folder' : undefined),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The warning for a server folder that is not there. */
|
|
34
|
+
export function missingServer(server) {
|
|
35
|
+
return `${server} is not there yet: dev builds, and deploys once it is.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `.env`'s lines for the server. */
|
|
39
|
+
export function serverEnv(server) {
|
|
40
|
+
const hint = server ? '' : '# Left empty, amxts dev asks for it - e.g. D:/hlds/cstrike/addons/amxts.\n';
|
|
41
|
+
return `# Where amxts dev deploys: the addons/amxts folder of the server.\n${hint}AMXTS_SERVER=${server.replace(/\\/g, '/')}\n`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Sets AMXTS_SERVER in the project's .env: on its line when it has one, else after what is there. */
|
|
45
|
+
export function saveServer(root, server) {
|
|
46
|
+
const path = join(root, '.env');
|
|
47
|
+
const text = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
48
|
+
const line = /^[ \t]*AMXTS_SERVER[ \t]*=.*$/m;
|
|
49
|
+
writeFileSync(path, line.test(text)
|
|
50
|
+
? text.replace(line, `AMXTS_SERVER=${server.replace(/\\/g, '/')}`)
|
|
51
|
+
: `${text}${text && !text.endsWith('\n') ? '\n' : ''}${serverEnv(server)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* AMXTS_SERVER for a command that deploys. When it is not set and the
|
|
56
|
+
* command may ask, it is asked, written to .env and set for the build.
|
|
57
|
+
* @param {string} root the project's folder
|
|
58
|
+
* @param {string} pm its package manager
|
|
59
|
+
*/
|
|
60
|
+
export async function ensureServer(root, pm) {
|
|
61
|
+
if (setting(root, 'AMXTS_SERVER') || !canAsk()) return;
|
|
62
|
+
p.log.warn('AMXTS_SERVER is not set: where should the plugins go?');
|
|
63
|
+
const server = await askServer(pm, { required: true });
|
|
64
|
+
if (p.isCancel(server)) {
|
|
65
|
+
p.cancel('Nothing was built.');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
saveServer(root, server);
|
|
69
|
+
process.env.AMXTS_SERVER = server;
|
|
70
|
+
p.log.success(`Wrote AMXTS_SERVER=${server.replace(/\\/g, '/')} to .env`);
|
|
71
|
+
if (!existsSync(server)) p.log.warn(missingServer(server));
|
|
72
|
+
}
|
package/src/system.mjs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Which system a project's server runs, Windows or Linux: the build compiles
|
|
2
|
+
// each plugin in that system's object format, and a plugin built for one does
|
|
3
|
+
// not load on the other.
|
|
4
|
+
//
|
|
5
|
+
// The build decides it (the core's `serverSystem()`, through cli-api): --os,
|
|
6
|
+
// AMXTS_SERVER_OS in .env, what the AMXTS_SERVER folder holds, this machine.
|
|
7
|
+
// `amxts init` needs it before a core is installed, so it reads the server's
|
|
8
|
+
// folder the same way here - hlds_linux or hlds.exe beside the game - and
|
|
9
|
+
// asks when there is nothing to read.
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { join, resolve } from 'node:path';
|
|
12
|
+
import process from 'node:process';
|
|
13
|
+
|
|
14
|
+
/** The systems, as `--os` and AMXTS_SERVER_OS name them. */
|
|
15
|
+
export const SYSTEMS = { windows: 'Windows', linux: 'Linux' };
|
|
16
|
+
|
|
17
|
+
/** This machine's system. */
|
|
18
|
+
export const HOST_SYSTEM = process.platform === 'win32' ? 'windows' : 'linux';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* What the server's files say it runs, or null: `server` is its addons/amxts,
|
|
22
|
+
* so hlds is three folders up.
|
|
23
|
+
* @param {string} server
|
|
24
|
+
* @returns {'windows' | 'linux' | null} the system, or null
|
|
25
|
+
*/
|
|
26
|
+
export function serverSystemOf(server) {
|
|
27
|
+
if (!server) return null;
|
|
28
|
+
const hlds = resolve(server, '..', '..', '..');
|
|
29
|
+
if (existsSync(join(hlds, 'hlds_linux'))) return 'linux';
|
|
30
|
+
if (existsSync(join(hlds, 'hlds.exe'))) return 'windows';
|
|
31
|
+
return null;
|
|
32
|
+
}
|
package/src/template.mjs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The starters in templates/ are written as the files they become, with
|
|
2
|
+
// {{placeholders}} and conditional blocks:
|
|
3
|
+
//
|
|
4
|
+
// // #if menu-core kept only when the flag is on
|
|
5
|
+
// // #if !lint kept only when it is off
|
|
6
|
+
// // #endif
|
|
7
|
+
//
|
|
8
|
+
// (`<!-- #if x -->` in Markdown, `# #if x` in .gitignore). A file named `_x`
|
|
9
|
+
// is written as `.x`: npm leaves a .gitignore out of a published package.
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
/** The {{author}} a starter is written with: git's user.name, else "you". */
|
|
15
|
+
export function gitAuthor() {
|
|
16
|
+
return spawnSync('git', ['config', 'user.name'], { encoding: 'utf8' }).stdout?.trim() || 'you';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const IF = /^\s*(?:\/\/|<!--|#)\s*#if\s+(!?)([\w-]+)/;
|
|
20
|
+
const ENDIF = /^\s*(?:\/\/|<!--|#)\s*#endif\b/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} text
|
|
24
|
+
* @param {Record<string, string>} values
|
|
25
|
+
* @param {Record<string, boolean>} flags
|
|
26
|
+
*/
|
|
27
|
+
export function fill(text, values, flags = {}) {
|
|
28
|
+
const lines = [];
|
|
29
|
+
let keep = true;
|
|
30
|
+
for (const line of text.split('\n')) {
|
|
31
|
+
const open = line.match(IF);
|
|
32
|
+
if (open) {
|
|
33
|
+
keep = open[1] === '!' ? !flags[open[2]] : Boolean(flags[open[2]]);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (ENDIF.test(line)) {
|
|
37
|
+
keep = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (keep) lines.push(line);
|
|
41
|
+
}
|
|
42
|
+
return lines.join('\n').replace(/\{\{([\w.-]+)\}\}/g, (whole, key) => values[key] ?? whole);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Writes a template folder into `to`. `skip` leaves a file out by its path in
|
|
47
|
+
* the template; a file that fills to nothing but blank lines is left out too.
|
|
48
|
+
* Returns the paths written, relative to `to`.
|
|
49
|
+
*/
|
|
50
|
+
export function copyTemplate(from, to, values, flags = {}, skip = () => false, base = '') {
|
|
51
|
+
const written = [];
|
|
52
|
+
for (const entry of readdirSync(from)) {
|
|
53
|
+
const source = join(from, entry);
|
|
54
|
+
const path = base ? `${base}/${entry}` : entry;
|
|
55
|
+
if (skip(path)) continue;
|
|
56
|
+
const name = fill(entry.replace(/^_/, '.'), values, flags);
|
|
57
|
+
if (statSync(source).isDirectory()) {
|
|
58
|
+
written.push(...copyTemplate(source, join(to, name), values, flags, skip, path).map(each => `${name}/${each}`));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const text = fill(readFileSync(source, 'utf8'), values, flags);
|
|
62
|
+
if (!/\S/.test(text)) continue;
|
|
63
|
+
mkdirSync(to, { recursive: true });
|
|
64
|
+
writeFileSync(join(to, name), text);
|
|
65
|
+
written.push(name);
|
|
66
|
+
}
|
|
67
|
+
return written;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Writes one file, making its folder. */
|
|
71
|
+
export function writeFile(path, text) {
|
|
72
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
73
|
+
writeFileSync(path, text);
|
|
74
|
+
}
|
package/src/ui.mjs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// How the amxts command talks: colors when the terminal has them, one line
|
|
2
|
+
// per step, and an error as one line with a hint under it. The stack of an
|
|
3
|
+
// error is printed only with --debug (or AMXTS_DEBUG=1).
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
|
|
6
|
+
/** Whether the output takes colors: a terminal, and NO_COLOR not set; FORCE_COLOR wins. */
|
|
7
|
+
function colorful(stream = process.stdout) {
|
|
8
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== '0') return true;
|
|
9
|
+
if (process.env.NO_COLOR !== undefined || process.env.TERM === 'dumb') return false;
|
|
10
|
+
return Boolean(stream.isTTY);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const enabled = colorful();
|
|
14
|
+
|
|
15
|
+
function paint(open, close) {
|
|
16
|
+
return text => (enabled ? `\x1B[${open}m${text}\x1B[${close}m` : String(text));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const c = {
|
|
20
|
+
bold: paint(1, 22),
|
|
21
|
+
dim: paint(2, 22),
|
|
22
|
+
red: paint(31, 39),
|
|
23
|
+
green: paint(32, 39),
|
|
24
|
+
yellow: paint(33, 39),
|
|
25
|
+
blue: paint(34, 39),
|
|
26
|
+
magenta: paint(35, 39),
|
|
27
|
+
cyan: paint(36, 39),
|
|
28
|
+
gray: paint(90, 39),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** An error the user can do something about: the message, and what to do. */
|
|
32
|
+
export class CliError extends Error {
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} message one line: what went wrong
|
|
35
|
+
* @param {string} [hint] one or more lines: what to do about it
|
|
36
|
+
*/
|
|
37
|
+
constructor(message, hint) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'CliError';
|
|
40
|
+
this.hint = hint;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const debug = () => process.argv.includes('--debug') || process.env.AMXTS_DEBUG === '1';
|
|
45
|
+
|
|
46
|
+
export const log = {
|
|
47
|
+
info: text => console.log(`${c.cyan('i')} ${text}`),
|
|
48
|
+
success: text => console.log(`${c.green('✔')} ${text}`),
|
|
49
|
+
warn: text => console.log(`${c.yellow('▲')} ${text}`),
|
|
50
|
+
step: text => console.log(`${c.cyan('◇')} ${text}`),
|
|
51
|
+
error: text => process.stderr.write(`${c.red('✖')} ${text}\n`),
|
|
52
|
+
hint: text => process.stderr.write(`${String(text).split('\n').map(line => ` ${c.dim(line)}`).join('\n')}\n`),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** An error, printed as the user should see it: one line and its hint; the stack only with --debug. */
|
|
56
|
+
export function report(error) {
|
|
57
|
+
if (error instanceof CliError) {
|
|
58
|
+
log.error(error.message);
|
|
59
|
+
if (error.hint) log.hint(error.hint);
|
|
60
|
+
} else {
|
|
61
|
+
const [first, ...rest] = (error instanceof Error ? error.message : String(error)).trim().split('\n');
|
|
62
|
+
log.error(first);
|
|
63
|
+
if (rest.length) process.stderr.write(`${rest.map(line => ` ${line}`).join('\n')}\n`);
|
|
64
|
+
if (!debug()) log.hint('Run it again with --debug to see where it happened.');
|
|
65
|
+
}
|
|
66
|
+
if (debug() && error instanceof Error && error.stack) process.stderr.write(`${c.gray(error.stack)}\n`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The line a command starts with: `amxts 0.1.0 · build`. */
|
|
70
|
+
export function banner(version, what) {
|
|
71
|
+
console.log(`${c.bold(c.cyan('amxts'))} ${c.dim(version)}${what ? c.dim(` · ${what}`) : ''}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The closest of `known` to `given`, when it is close enough to be a typo. */
|
|
75
|
+
export function closest(given, known) {
|
|
76
|
+
let best = null;
|
|
77
|
+
let distance = Infinity;
|
|
78
|
+
for (const each of known) {
|
|
79
|
+
const d = levenshtein(given.toLowerCase(), each.toLowerCase());
|
|
80
|
+
if (d < distance) {
|
|
81
|
+
best = each;
|
|
82
|
+
distance = d;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const prefix = given.length >= 2 ? known.find(each => each.startsWith(given)) : undefined;
|
|
86
|
+
if (prefix) return prefix;
|
|
87
|
+
return distance <= (given.length <= 3 ? 1 : 2) ? best : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Edits between two words, a swap of neighbours counted as one: `buidl` is one from `build`. */
|
|
91
|
+
function levenshtein(a, b) {
|
|
92
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array.from({ length: b.length }, (_, j) => (i === 0 ? j + 1 : 0))]);
|
|
93
|
+
for (let i = 1; i <= a.length; i++) {
|
|
94
|
+
for (let j = 1; j <= b.length; j++) {
|
|
95
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
96
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
97
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return d[a.length][b.length];
|
|
101
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) {{year}} {{author}}
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|