@amxts/cli 0.2.0 → 0.3.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/package.json +1 -1
- package/src/core.mjs +35 -12
- package/src/dev-feed.mjs +147 -0
- package/src/dev-panel.mjs +686 -0
- package/src/init-module.mjs +7 -5
- package/src/init.mjs +10 -7
- package/src/lock.mjs +94 -0
- package/src/main.mjs +88 -12
- package/src/modules.json +15 -0
- package/src/pm.mjs +11 -0
- package/src/rcon.mjs +134 -0
- package/src/server-update.mjs +13 -8
- package/src/ui.mjs +134 -3
- package/src/update-check.mjs +64 -0
- package/src/upgrade.mjs +2 -0
- package/templates/module/_oxlintrc.json +5 -26
- package/templates/module/src/index.ts +1 -1
- package/templates/module/test/tsconfig.json +1 -1
- package/templates/module/test/{{name}}.test.ts +5 -0
- package/templates/project/_oxlintrc.json +5 -26
- package/templates/project/plugins/hello.ts +14 -4
- package/templates/project/test/hello.test.ts +21 -0
- package/templates/project/test/tsconfig.json +1 -1
package/src/ui.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// per step, and an error as one line with a hint under it. The stack of an
|
|
3
3
|
// error is printed only with --debug (or AMXTS_DEBUG=1).
|
|
4
4
|
import process from 'node:process';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
5
6
|
|
|
6
7
|
/** Whether the output takes colors: a terminal, and NO_COLOR not set; FORCE_COLOR wins. */
|
|
7
8
|
function colorful(stream = process.stdout) {
|
|
@@ -66,9 +67,139 @@ export function report(error) {
|
|
|
66
67
|
if (debug() && error instanceof Error && error.stack) process.stderr.write(`${c.gray(error.stack)}\n`);
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
/**
|
|
70
|
-
export function
|
|
71
|
-
|
|
70
|
+
/** Whether a run is CI's: `CI` set to anything but `false`. */
|
|
71
|
+
export function isCI(env = process.env) {
|
|
72
|
+
return Boolean(env.CI) && env.CI !== 'false' && env.CI !== '0';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* What characters the output can draw: `box` - any (the panel's spinner and
|
|
77
|
+
* bar); `bullet` - the logo's `{•}` but not the box characters, for Windows'
|
|
78
|
+
* own console, whose fonts may miss them; `ascii` - none of them, where the
|
|
79
|
+
* text is not UTF-8: the logo is `{*}`.
|
|
80
|
+
*/
|
|
81
|
+
export function logoGlyphs(env = process.env, platform = process.platform) {
|
|
82
|
+
if (platform === 'win32') {
|
|
83
|
+
const modern = env.WT_SESSION || env.TERM_PROGRAM || env.ConEmuTask || env.TERMINAL_EMULATOR === 'JetBrains-JediTerm' || /^(?:xterm|alacritty)/.test(env.TERM ?? '');
|
|
84
|
+
return modern ? 'box' : 'bullet';
|
|
85
|
+
}
|
|
86
|
+
if (!/utf-?8/i.test(env.LC_ALL || env.LC_CTYPE || env.LANG || 'UTF-8')) return 'ascii';
|
|
87
|
+
return env.TERM === 'linux' ? 'bullet' : 'box';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The logo's colour, amxts yellow (#F2C94C): in 24 bits where the terminal
|
|
92
|
+
* has them, plain yellow in one of 16 colours, none without a terminal, in CI
|
|
93
|
+
* or with NO_COLOR - FORCE_COLOR wins, as for every colour here.
|
|
94
|
+
* @param {{ tty?: boolean, env?: Record<string, string | undefined>, depth?: number }} [where] the output: a terminal, its environment and its colour depth in bits
|
|
95
|
+
* @returns {(text: string) => string} what paints a text
|
|
96
|
+
*/
|
|
97
|
+
export function logoPaint(where) {
|
|
98
|
+
return [text => text, text => `\x1B[33m${text}\x1B[39m`, text => `\x1B[38;2;242;201;76m${text}\x1B[39m`][brandDepth(where)];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** How amxts yellow is drawn (logoPaint): 0 - not at all, 1 - plain yellow, 2 - in 24 bits. */
|
|
102
|
+
function brandDepth({ tty = Boolean(process.stdout.isTTY), env = process.env, depth = process.stdout.getColorDepth?.(env) ?? 4 } = {}) {
|
|
103
|
+
const forced = env.FORCE_COLOR && env.FORCE_COLOR !== '0';
|
|
104
|
+
if (!forced && (!tty || isCI(env) || env.NO_COLOR !== undefined || env.TERM === 'dumb')) return 0;
|
|
105
|
+
return depth >= 24 || env.COLORTERM === 'truecolor' || env.COLORTERM === '24bit' ? 2 : 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The 16 colours as amxts draws them in 24 bits: green, red, yellow (amxts
|
|
109
|
+
// yellow), blue, magenta, cyan - lit, where a terminal's own may be pale.
|
|
110
|
+
const VIVID = { 31: '248;113;113', 32: '74;222;128', 33: '242;201;76', 34: '96;165;250', 35: '232;121;249', 36: '34;211;238' };
|
|
111
|
+
|
|
112
|
+
/** A line with its 16 colours drawn vivid, where the terminal has 24 bits; as it is elsewhere. */
|
|
113
|
+
export function vivid(line, where) {
|
|
114
|
+
if (brandDepth(where) !== 2) return line;
|
|
115
|
+
// eslint-disable-next-line no-control-regex
|
|
116
|
+
return line.replace(/\x1B\[(3[1-6])m/g, (_, code) => `\x1B[38;2;${VIVID[code]}m`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A text the terminal makes a link of (OSC 8), where colours are on; the text
|
|
121
|
+
* alone elsewhere.
|
|
122
|
+
*/
|
|
123
|
+
export function link(text, url, where) {
|
|
124
|
+
return brandDepth(where) ? `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\` : text;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The link that opens a file at a line and column in the editor: Orca's own
|
|
129
|
+
* in Orca's terminal, else VS Code's, or Cursor's where the terminal or EDITOR
|
|
130
|
+
* is Cursor's. A `file://` link elsewhere would open a `.ts` in whatever the
|
|
131
|
+
* system opens it with - a video player, on some.
|
|
132
|
+
*/
|
|
133
|
+
export function editorLink(file, line, col, env = process.env) {
|
|
134
|
+
// Orca opens a file:// link in its own editor, the line and column after it.
|
|
135
|
+
if (env.TERM_PROGRAM === 'Orca') return `${pathToFileURL(file).href}:${line}:${col}`;
|
|
136
|
+
const scheme = /cursor/i.test(`${env.TERM_PROGRAM ?? ''} ${env.EDITOR ?? ''} ${env.VISUAL ?? ''}`) ? 'cursor' : 'vscode';
|
|
137
|
+
// `vscode://file/D:/x.ts:3:26`, `vscode://file/home/me/x.ts:3:26`: the path absolute.
|
|
138
|
+
const path = file.replace(/\\/g, '/');
|
|
139
|
+
return `${scheme}://file${path.startsWith('/') ? '' : '/'}${encodeURI(path)}:${line}:${col}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A state as a word on a ground of its colour - ` READY ` on amxts yellow,
|
|
144
|
+
* ` BUILDING ` on cyan, ` ERROR ` on red; `[READY]` without colours.
|
|
145
|
+
* @param {string} text
|
|
146
|
+
* @param {'ready' | 'busy' | 'error'} tone
|
|
147
|
+
*/
|
|
148
|
+
export function badge(text, tone, where) {
|
|
149
|
+
const depth = brandDepth(where);
|
|
150
|
+
if (!depth) return `[${text}]`;
|
|
151
|
+
const ground = {
|
|
152
|
+
ready: depth === 2 ? '48;2;242;201;76;38;2;21;24;29' : '43;30',
|
|
153
|
+
busy: depth === 2 ? '48;2;34;211;238;38;2;21;24;29' : '46;30',
|
|
154
|
+
error: depth === 2 ? '48;2;239;68;68;97' : '41;97',
|
|
155
|
+
}[tone];
|
|
156
|
+
return `\x1B[1;${ground}m ${text} \x1B[0m`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// The logo in a line: the braces and the dot.
|
|
160
|
+
const MARK = { box: '{•}', bullet: '{•}', ascii: '{*}' };
|
|
161
|
+
|
|
162
|
+
// The logo - the crosshair: its arms, the braces and the dot - in braille
|
|
163
|
+
// dots, four lines of them, the logo's SVG rasterised to 16 by 16.
|
|
164
|
+
export const LOGO = ['⠀⢀⡀⢸⡇⢀⡀⠀', '⣀⡿⠁⣀⣀⠈⢿⣀', '⠉⣷⡀⠉⠉⢀⣾⠉', '⠀⠈⠁⢸⡇⠈⠁⠀'];
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The logo's four lines, painted. `wave`: a light that runs across it - its
|
|
168
|
+
* place, in columns - while something runs, in 24 bits; the plain colour
|
|
169
|
+
* without it. Where the font may have no braille, the mark in a line instead
|
|
170
|
+
* (`{•}` on the second).
|
|
171
|
+
* @param {number | null} [wave]
|
|
172
|
+
* @param {{ glyphs?: string, where?: object }} [options] how the output draws (logoGlyphs, and the output as logoPaint takes it)
|
|
173
|
+
* @returns {string[]} four lines, each as wide
|
|
174
|
+
*/
|
|
175
|
+
export function logoLines(wave = null, { glyphs = logoGlyphs(), where } = {}) {
|
|
176
|
+
if (glyphs !== 'box') return [' ', logoPaint(where)(MARK[glyphs]), ' ', ' '];
|
|
177
|
+
const depth = brandDepth(where);
|
|
178
|
+
if (depth < 2 || wave === null) return LOGO.map(line => logoPaint(where)(line));
|
|
179
|
+
// From a darker gold to a pale one, amxts yellow between.
|
|
180
|
+
// One spot of light, gone a few columns away from its place.
|
|
181
|
+
const shade = column => Math.exp(-((column - wave) ** 2) / 4);
|
|
182
|
+
const mix = light => [[150, 110, 30], [255, 236, 150]].reduce((low, high) => low.map((each, index) => Math.round(each + (high[index] - each) * light)));
|
|
183
|
+
return LOGO.map(line => `${[...line].map((dot, column) => `\x1B[38;2;${mix(shade(column)).join(';')}m${dot}`).join('')}\x1B[39m`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The line a command starts with: the logo, `{•} amxts 0.2.4 · build`. With
|
|
188
|
+
* `logo` (dev, a new project) the logo in its four lines, the words beside the
|
|
189
|
+
* second.
|
|
190
|
+
* @param {string} version
|
|
191
|
+
* @param {string} [what]
|
|
192
|
+
* @param {{ logo?: boolean, glyphs?: string, where?: object }} [options] how the output draws (logoGlyphs, and the output as logoPaint takes it)
|
|
193
|
+
*/
|
|
194
|
+
export function bannerText(version, what, { logo = false, glyphs = logoGlyphs(), where } = {}) {
|
|
195
|
+
const words = `${c.bold('amxts')} ${c.dim(version)}${what ? c.dim(` · ${what}`) : ''}`;
|
|
196
|
+
if (!logo || glyphs !== 'box') return `${logoPaint(where)(MARK[glyphs] ?? MARK.bullet)} ${words}`;
|
|
197
|
+
return logoLines(null, { glyphs, where }).map((line, index) => (index === 1 ? `${line} ${words}` : line)).join('\n');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Prints the line a command starts with (bannerText). */
|
|
201
|
+
export function banner(version, what, options) {
|
|
202
|
+
console.log(bannerText(version, what, options));
|
|
72
203
|
}
|
|
73
204
|
|
|
74
205
|
/** The closest of `known` to `given`, when it is close enough to be a typo. */
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The line that says a newer amxts is out: `amxts 0.2.5 is out: npx amxts
|
|
2
|
+
// upgrade`. Read from a file the last check wrote, so it costs a command
|
|
3
|
+
// nothing; once a day a check runs in a process of its own, left behind, and
|
|
4
|
+
// writes it again. Only in a terminal, not in CI; AMXTS_IGNORE_UPDATE_CHECK=1
|
|
5
|
+
// turns it off.
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import process from 'node:process';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { compareVersions } from './core.mjs';
|
|
13
|
+
import { execAmxts } from './pm.mjs';
|
|
14
|
+
import { isCI } from './ui.mjs';
|
|
15
|
+
|
|
16
|
+
/** Where the last check is kept: `{ checked, latest }`. */
|
|
17
|
+
export const UPDATE_FILE = join(homedir(), '.cache', 'amxts', 'update.json');
|
|
18
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
function save(file, data) {
|
|
21
|
+
try {
|
|
22
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
23
|
+
writeFileSync(file, `${JSON.stringify(data)}\n`);
|
|
24
|
+
} catch {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The line about a newer core than the project's, or null; starts the day's
|
|
29
|
+
* check when the last one is older.
|
|
30
|
+
* @param {string} version the project's core
|
|
31
|
+
* @param {string} pm its package manager, for the words
|
|
32
|
+
* @param {{ env?: Record<string, string | undefined>, tty?: boolean, now?: number, file?: string, check?: (file: string) => void }} [options]
|
|
33
|
+
*/
|
|
34
|
+
export function updateLine(version, pm, { env = process.env, tty = Boolean(process.stdout.isTTY), now = Date.now(), file = UPDATE_FILE, check = startCheck } = {}) {
|
|
35
|
+
if (env.AMXTS_IGNORE_UPDATE_CHECK === '1' || !tty || isCI(env)) return null;
|
|
36
|
+
let last = {};
|
|
37
|
+
try {
|
|
38
|
+
last = JSON.parse(readFileSync(file, 'utf8'));
|
|
39
|
+
} catch {}
|
|
40
|
+
if (!(now - (last.checked ?? 0) < DAY)) {
|
|
41
|
+
// Written first: a second command in the same minute does not check again.
|
|
42
|
+
save(file, { ...last, checked: now });
|
|
43
|
+
check(file);
|
|
44
|
+
}
|
|
45
|
+
return last.latest && compareVersions(last.latest, version) > 0 ? `amxts ${last.latest} is out: ${execAmxts(pm)} upgrade` : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Starts the check in a process of its own, which the command does not wait for. */
|
|
49
|
+
function startCheck(file) {
|
|
50
|
+
try {
|
|
51
|
+
spawn(process.execPath, [fileURLToPath(import.meta.url), file], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The check itself: the latest @amxts/core on the registry npm is set to.
|
|
56
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
57
|
+
const file = process.argv[2];
|
|
58
|
+
const registry = (process.env.npm_config_registry || 'https://registry.npmjs.org/').replace(/\/?$/, '/');
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(`${registry}@amxts/core/latest`, { signal: AbortSignal.timeout(10_000) });
|
|
61
|
+
const { version } = await response.json();
|
|
62
|
+
if (typeof version === 'string') save(file, { checked: Date.now(), latest: version });
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
package/src/upgrade.mjs
CHANGED
|
@@ -240,6 +240,8 @@ export async function upgrade({ to, server = true, serverOnly = false, dryRun =
|
|
|
240
240
|
: { ok: true, line: `the module of amxts ${version} goes in once @amxts/core ${version} is installed` };
|
|
241
241
|
}
|
|
242
242
|
console.log(['', ...summary(outcome)].join('\n'));
|
|
243
|
+
// A script that upgrades and goes on hears that the build failed.
|
|
244
|
+
if (outcome.build === 'failed') process.exitCode = 1;
|
|
243
245
|
return outcome;
|
|
244
246
|
}
|
|
245
247
|
|
|
@@ -18,41 +18,20 @@
|
|
|
18
18
|
"**/node_modules/",
|
|
19
19
|
"**/*.tgz",
|
|
20
20
|
"**/bun.lock",
|
|
21
|
+
"**/bun.lockb",
|
|
21
22
|
"**/package-lock.json",
|
|
22
23
|
"**/pnpm-lock.yaml",
|
|
23
24
|
"**/yarn.lock",
|
|
25
|
+
"**/.yarn",
|
|
24
26
|
"**/.amxts/",
|
|
25
27
|
"**/dist/",
|
|
26
|
-
"**/node_modules",
|
|
27
|
-
"**/dist",
|
|
28
|
-
"**/bun.lockb",
|
|
29
|
-
"**/output",
|
|
30
28
|
"**/coverage",
|
|
31
|
-
"**/temp",
|
|
32
|
-
"**/.temp",
|
|
33
|
-
"**/tmp",
|
|
34
|
-
"**/.tmp",
|
|
35
|
-
"**/.history",
|
|
36
|
-
"**/.vitepress/cache",
|
|
37
|
-
"**/.nuxt",
|
|
38
|
-
"**/.next",
|
|
39
|
-
"**/.svelte-kit",
|
|
40
|
-
"**/.vercel",
|
|
41
|
-
"**/.changeset",
|
|
42
|
-
"**/.idea",
|
|
43
29
|
"**/.cache",
|
|
44
|
-
"
|
|
45
|
-
"**/.vite-inspect",
|
|
46
|
-
"**/.yarn",
|
|
30
|
+
"**/__snapshots__",
|
|
47
31
|
"**/CHANGELOG*.md",
|
|
48
32
|
"**/LICENSE*",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"**/vite.config.*.timestamp-*",
|
|
52
|
-
"**/auto-import.d.ts",
|
|
53
|
-
"**/auto-imports.d.ts",
|
|
54
|
-
"**/components.d.ts",
|
|
55
|
-
"**/.context",
|
|
33
|
+
"**/.history",
|
|
34
|
+
"**/.idea",
|
|
56
35
|
"**/.claude",
|
|
57
36
|
"**/.agents",
|
|
58
37
|
"**/.*/skills"
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
// #if bun
|
|
1
2
|
import { describe, expect, test } from "bun:test";
|
|
3
|
+
// #endif
|
|
4
|
+
// #if !bun
|
|
5
|
+
import { describe, expect, test } from "vitest";
|
|
6
|
+
// #endif
|
|
2
7
|
import { setup } from "@amxts/core/test-utils";
|
|
3
8
|
|
|
4
9
|
// The first test compiles the plugins; the next runs take them from the cache.
|
|
@@ -18,41 +18,20 @@
|
|
|
18
18
|
"**/node_modules/",
|
|
19
19
|
"**/*.tgz",
|
|
20
20
|
"**/bun.lock",
|
|
21
|
+
"**/bun.lockb",
|
|
21
22
|
"**/package-lock.json",
|
|
22
23
|
"**/pnpm-lock.yaml",
|
|
23
24
|
"**/yarn.lock",
|
|
25
|
+
"**/.yarn",
|
|
24
26
|
"**/.amxts/",
|
|
25
27
|
"**/dist/",
|
|
26
|
-
"**/node_modules",
|
|
27
|
-
"**/dist",
|
|
28
|
-
"**/bun.lockb",
|
|
29
|
-
"**/output",
|
|
30
28
|
"**/coverage",
|
|
31
|
-
"**/temp",
|
|
32
|
-
"**/.temp",
|
|
33
|
-
"**/tmp",
|
|
34
|
-
"**/.tmp",
|
|
35
|
-
"**/.history",
|
|
36
|
-
"**/.vitepress/cache",
|
|
37
|
-
"**/.nuxt",
|
|
38
|
-
"**/.next",
|
|
39
|
-
"**/.svelte-kit",
|
|
40
|
-
"**/.vercel",
|
|
41
|
-
"**/.changeset",
|
|
42
|
-
"**/.idea",
|
|
43
29
|
"**/.cache",
|
|
44
|
-
"
|
|
45
|
-
"**/.vite-inspect",
|
|
46
|
-
"**/.yarn",
|
|
30
|
+
"**/__snapshots__",
|
|
47
31
|
"**/CHANGELOG*.md",
|
|
48
32
|
"**/LICENSE*",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"**/vite.config.*.timestamp-*",
|
|
52
|
-
"**/auto-import.d.ts",
|
|
53
|
-
"**/auto-imports.d.ts",
|
|
54
|
-
"**/components.d.ts",
|
|
55
|
-
"**/.context",
|
|
33
|
+
"**/.history",
|
|
34
|
+
"**/.idea",
|
|
56
35
|
"**/.claude",
|
|
57
36
|
"**/.agents",
|
|
58
37
|
"**/.*/skills"
|
|
@@ -14,12 +14,17 @@ const greeting = "Welcome";
|
|
|
14
14
|
// #endif
|
|
15
15
|
|
|
16
16
|
server.addCommand("/hp", ({ player }) => sayHp(player));
|
|
17
|
-
server.addEventListener("putInServer", event => print(
|
|
17
|
+
server.addEventListener("putInServer", event => event.player.print(`${greeting}, ${event.player.name}!`));
|
|
18
18
|
// #if menu-core
|
|
19
19
|
|
|
20
20
|
// A menu made in code: its title and items can be functions of the player it is shown to.
|
|
21
21
|
const hello = menus.create("HELLO", { title: ({ player }) => `Hello, ${player.name}` });
|
|
22
|
-
hello.addItem({
|
|
22
|
+
hello.addItem({
|
|
23
|
+
title: "Wave",
|
|
24
|
+
onSelect: ({ player }) => {
|
|
25
|
+
server.print(`${player.name} waves`);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
23
28
|
hello.addItem({
|
|
24
29
|
title: ({ player }) => `Heal (${player.health} HP)`,
|
|
25
30
|
visible: ({ player }) => player.health < 100,
|
|
@@ -34,7 +39,12 @@ server.addCommand("/menu", ({ player }) => hello.show(player));
|
|
|
34
39
|
|
|
35
40
|
// A menu: its title and items can be functions of the player it is shown to.
|
|
36
41
|
const hello = new Menu(({ player }) => `Hello, ${player.name}`);
|
|
37
|
-
hello.addItem({
|
|
42
|
+
hello.addItem({
|
|
43
|
+
title: "Wave",
|
|
44
|
+
onSelect: ({ player }) => {
|
|
45
|
+
server.print(`${player.name} waves`);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
38
48
|
hello.addItem({
|
|
39
49
|
title: ({ player }) => `Heal (${player.health} HP)`,
|
|
40
50
|
visible: ({ player }) => player.health < 100,
|
|
@@ -47,5 +57,5 @@ server.addCommand("/menu", ({ player }) => hello.show(player));
|
|
|
47
57
|
// #endif
|
|
48
58
|
|
|
49
59
|
function sayHp(player: Player) {
|
|
50
|
-
print(
|
|
60
|
+
player.print(`${player.name}, your HP: ${player.health}`);
|
|
51
61
|
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
// #if bun
|
|
1
2
|
import { expect, setDefaultTimeout, test } from "bun:test";
|
|
3
|
+
// #endif
|
|
4
|
+
// #if !bun
|
|
5
|
+
import { expect, test, vi } from "vitest";
|
|
6
|
+
// #endif
|
|
2
7
|
import { setup } from "@amxts/core/test-utils";
|
|
3
8
|
// #if menu-core
|
|
4
9
|
import { menusOf } from "@amxts/menu-core/testing";
|
|
5
10
|
// #endif
|
|
6
11
|
|
|
7
12
|
// The first run compiles the plugins; the next ones take them from the cache.
|
|
13
|
+
// #if bun
|
|
8
14
|
setDefaultTimeout(120_000);
|
|
15
|
+
// #endif
|
|
16
|
+
// #if !bun
|
|
17
|
+
vi.setConfig({ testTimeout: 120_000 });
|
|
18
|
+
// #endif
|
|
9
19
|
|
|
10
20
|
test("a player who joins is greeted", async () => {
|
|
11
21
|
const server = await setup();
|
|
@@ -34,4 +44,15 @@ test("/menu heals a hurt player", async () => {
|
|
|
34
44
|
menus.press(alice, 2);
|
|
35
45
|
expect(alice.health).toBe(100);
|
|
36
46
|
});
|
|
47
|
+
|
|
48
|
+
test("Wave in /menu tells everyone", async () => {
|
|
49
|
+
const server = await setup();
|
|
50
|
+
const menus = menusOf(server);
|
|
51
|
+
const alice = server.join("Alice");
|
|
52
|
+
const bob = server.join("Bob");
|
|
53
|
+
|
|
54
|
+
alice.say("/menu");
|
|
55
|
+
menus.press(alice, 1);
|
|
56
|
+
expect(bob.chat).toContain("Alice waves");
|
|
57
|
+
});
|
|
37
58
|
// #endif
|