@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/package.json
CHANGED
package/src/core.mjs
CHANGED
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Creating a project or a module needs no core: they run on their own.
|
|
11
11
|
// `--local` takes the core from this machine instead of npm (localCore()).
|
|
12
|
-
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
13
13
|
import { existsSync, readFileSync } from 'node:fs';
|
|
14
14
|
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
18
|
import { addArgs, commandLine, detectPackageManager, execAmxts, versionOf } from './pm.mjs';
|
|
19
|
-
import { CliError, debug } from './ui.mjs';
|
|
19
|
+
import { CliError, debug, report } from './ui.mjs';
|
|
20
20
|
|
|
21
21
|
/** The command's own package folder: packages/cli in its repository. */
|
|
22
22
|
export const CLI_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -24,7 +24,7 @@ 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.3.0';
|
|
28
28
|
/** The version of `@amxts/core/cli-api` this command speaks. */
|
|
29
29
|
export const CLI_API = 5;
|
|
30
30
|
|
|
@@ -275,19 +275,42 @@ export function needBun(core, what = 'builds plugins') {
|
|
|
275
275
|
* @param {string[]} [args]
|
|
276
276
|
*/
|
|
277
277
|
export function runTask(core, name, args = []) {
|
|
278
|
+
const result = spawnSync(...taskSpawn(core, name, args, { stdio: 'inherit' }));
|
|
279
|
+
if (result.error) throw new CliError(`Bun did not start: ${result.error.message}`, BUN_HINT);
|
|
280
|
+
return result.status ?? 1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** A task as spawn takes it: the file, its arguments (none when it goes through the shell) and the options. */
|
|
284
|
+
function taskSpawn(core, name, args, options) {
|
|
278
285
|
const task = core.api.task(name, args);
|
|
279
|
-
const env = { ...process.env, ...(debug() ? { AMXTS_DEBUG: '1' } : {}) };
|
|
280
|
-
if (task.runtime === 'node') {
|
|
281
|
-
return spawnSync(process.execPath, task.args, { stdio: 'inherit', env }).status ?? 1;
|
|
282
|
-
}
|
|
286
|
+
const env = { ...process.env, ...(debug() ? { AMXTS_DEBUG: '1' } : {}), ...options.env };
|
|
287
|
+
if (task.runtime === 'node') return [process.execPath, task.args, { ...options, env }];
|
|
283
288
|
const binary = needBun(core);
|
|
284
289
|
// A Bun on PATH goes through the shell on Windows, where it may be
|
|
285
290
|
// bun.cmd; a path with a space is quoted for it.
|
|
286
|
-
|
|
287
|
-
?
|
|
288
|
-
:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
+
return binary === 'bun' && process.platform === 'win32'
|
|
292
|
+
? [commandLine(['bun', ...task.args]), { ...options, shell: true, env }]
|
|
293
|
+
: [binary, task.args, { ...options, env }];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Starts one of the core's tasks and leaves it running: `dev`'s build, which
|
|
298
|
+
* watches. `pipe` takes its output instead of the terminal, with its colours.
|
|
299
|
+
* @param {Core} core
|
|
300
|
+
* @param {string} name
|
|
301
|
+
* @param {string[]} args
|
|
302
|
+
* @param {{ pipe?: boolean }} [options]
|
|
303
|
+
* @returns {import('node:child_process').ChildProcess} the task
|
|
304
|
+
*/
|
|
305
|
+
export function startTask(core, name, args, { pipe = false } = {}) {
|
|
306
|
+
const child = pipe
|
|
307
|
+
? spawn(...taskSpawn(core, name, args, { stdio: ['ignore', 'pipe', 'pipe'], env: { FORCE_COLOR: process.env.NO_COLOR === undefined ? '1' : '0' } }))
|
|
308
|
+
: spawn(...taskSpawn(core, name, args, { stdio: 'inherit' }));
|
|
309
|
+
child.on('error', (error) => {
|
|
310
|
+
report(new CliError(`Bun did not start: ${error.message}`, BUN_HINT));
|
|
311
|
+
process.exit(1);
|
|
312
|
+
});
|
|
313
|
+
return child;
|
|
291
314
|
}
|
|
292
315
|
|
|
293
316
|
/** Runs a task and stops with its exit code when it fails. */
|
package/src/dev-feed.mjs
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// What the dev panel knows of the build: read off the lines the core's build
|
|
2
|
+
// task prints, the one place that reads them. The words are the core's; a
|
|
3
|
+
// structured feed from the core would replace this file, and the panel would
|
|
4
|
+
// stay as it is.
|
|
5
|
+
//
|
|
6
|
+
// Plugins 3 in plugins/ the header
|
|
7
|
+
// ◇ compiling hello 1/2 a compile starts (no TTY)
|
|
8
|
+
// ✔ hello · 1.2s one is compiled
|
|
9
|
+
// ✖ hello does not compile: ... an error, its lines under it
|
|
10
|
+
// ✔ 12:01:33 plugins/hello.ts changed: deployed, the server reloaded hello (2.1s)
|
|
11
|
+
// ◇ watching plugins - save a plugin and ... the first build is done
|
|
12
|
+
|
|
13
|
+
/** A line without its colours. */
|
|
14
|
+
export function plain(line) {
|
|
15
|
+
// eslint-disable-next-line no-control-regex
|
|
16
|
+
return line.replace(/\x1B\[[\d;?]*[a-z]|\x1B\]8;[^\x1B]*\x1B\\/gi, '').replace(/\r/g, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The marks a line of the build starts with: a line without one belongs to the error above it. */
|
|
20
|
+
const MARK = /^[✔◇▲✖i] /;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} Rebuild
|
|
24
|
+
* @property {string[]} files the saved files that started it; none for a start
|
|
25
|
+
* @property {string[]} plugins what it built
|
|
26
|
+
* @property {number | undefined} compile seconds of compiling, the longest one (they compile side by side); undefined when nothing compiled
|
|
27
|
+
* @property {number} total seconds, all of it
|
|
28
|
+
* @property {string} outcome what the deploy did, as the core says it
|
|
29
|
+
* @property {boolean} reloaded whether the server reloaded them all
|
|
30
|
+
* @property {string} at the time it ended, as printed
|
|
31
|
+
* @property {number | undefined} boot seconds before the core counts, for a build the panel started
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** Each plugin a line names as not compiling is failed. */
|
|
35
|
+
function markFailed(feed, text) {
|
|
36
|
+
for (const [, name] of text.matchAll(/(\S+) does not compile:/g)) feed.plugins.set(name, 'failed');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The lines the feed reads: what each is, and what it changes. */
|
|
40
|
+
const RULES = [
|
|
41
|
+
[/^ {2}(Core|Modules|Server|Plugins) +(\S.*)$/, 'header', (feed, [, row, value]) => {
|
|
42
|
+
feed.header[row] = value;
|
|
43
|
+
if (row === 'Plugins') feed.count = Number.parseInt(value, 10) || 0;
|
|
44
|
+
}],
|
|
45
|
+
[/^◇ compiling (.+?) (\d+)\/(\d+)$/, 'compiling', (feed, [, what, , total]) => {
|
|
46
|
+
feed.building ??= { what, done: 0, total: 0, since: Date.now() };
|
|
47
|
+
feed.building.what = what;
|
|
48
|
+
feed.building.total = Number(total);
|
|
49
|
+
}],
|
|
50
|
+
[/^✔ (\S+) · (?:(\d+(?:\.\d+)?)s|prebuilt|unchanged)$/, 'compiled', (feed, [, name, time]) => {
|
|
51
|
+
if (!time) return;
|
|
52
|
+
feed.times.set(name, Number(time));
|
|
53
|
+
feed.expected = Number(time);
|
|
54
|
+
if (feed.building) feed.building.done++;
|
|
55
|
+
}],
|
|
56
|
+
[/^✖ (.*)$/, 'error', (feed, [, text]) => {
|
|
57
|
+
feed.error = [text];
|
|
58
|
+
feed.inError = true;
|
|
59
|
+
feed.broken = true;
|
|
60
|
+
feed.building = null;
|
|
61
|
+
markFailed(feed, text);
|
|
62
|
+
}],
|
|
63
|
+
[/^✔ (?:(\d{1,2}:\d{2}:\d{2}(?: ?[AP]M)?) )?(?:(.+?) changed: )?(.+) \((\d+(?:\.\d+)?)s\)$/, 'rebuilt', (feed, [, at = '', files = '', outcome, total]) => {
|
|
64
|
+
const named = /reloaded (.+)$/.exec(outcome)?.[1]
|
|
65
|
+
?? /^(?:deployed|built|rebuilt) (.+?)(?: into .*| - .*)?$/.exec(outcome)?.[1]
|
|
66
|
+
?? /^(.+?) not deployed/.exec(outcome)?.[1] ?? '';
|
|
67
|
+
const plugins = named.split(', ').filter(Boolean);
|
|
68
|
+
for (const name of plugins) feed.plugins.set(name, 'built');
|
|
69
|
+
const times = [...feed.times.values()];
|
|
70
|
+
feed.last = {
|
|
71
|
+
files: files ? files.split(', ') : [],
|
|
72
|
+
plugins,
|
|
73
|
+
compile: times.length ? Math.max(...times) : undefined,
|
|
74
|
+
total: Number(total),
|
|
75
|
+
outcome,
|
|
76
|
+
reloaded: outcome.startsWith('deployed, the server reloaded '),
|
|
77
|
+
at,
|
|
78
|
+
// A build the panel started: the time before the core's own count - Bun, the project, the server asked.
|
|
79
|
+
boot: feed.started === null ? undefined : Math.max(0, (Date.now() - feed.started) / 1000 - Number(total)),
|
|
80
|
+
};
|
|
81
|
+
feed.started = null;
|
|
82
|
+
feed.times.clear();
|
|
83
|
+
feed.broken = false;
|
|
84
|
+
feed.building = null;
|
|
85
|
+
}],
|
|
86
|
+
[/^◇ watching /, 'watching', (feed) => {
|
|
87
|
+
feed.watching = true;
|
|
88
|
+
}],
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
/** The parser's state, which the panel shows. */
|
|
92
|
+
export function createFeed() {
|
|
93
|
+
const feed = {
|
|
94
|
+
/** @type {Record<string, string>} the header's rows: Core, Modules, Server, Plugins */
|
|
95
|
+
header: {},
|
|
96
|
+
/** How many plugins the project has, from the header. */
|
|
97
|
+
count: 0,
|
|
98
|
+
/** @type {Map<string, 'built' | 'failed'>} each plugin a build has named */
|
|
99
|
+
plugins: new Map(),
|
|
100
|
+
/** @type {{ what: string, done: number, total: number, since: number } | null} the build that compiles now: what started last, how many of how many are done, since when (ms) */
|
|
101
|
+
building: null,
|
|
102
|
+
/** Seconds the last plugin took to compile: what the next one is expected to take. */
|
|
103
|
+
expected: 10,
|
|
104
|
+
/** @type {number | null} when the panel started the build that runs (ms), till it ends; null for one a save started */
|
|
105
|
+
started: null,
|
|
106
|
+
/** @type {Rebuild | null} */
|
|
107
|
+
last: null,
|
|
108
|
+
/** @type {string[]} the last error's lines */
|
|
109
|
+
error: [],
|
|
110
|
+
watching: false,
|
|
111
|
+
/** @type {Map<string, number>} seconds of each plugin compiled since the last summary */
|
|
112
|
+
times: new Map(),
|
|
113
|
+
/** Whether the lines that follow belong to the last error. */
|
|
114
|
+
inError: false,
|
|
115
|
+
/** Whether the last build failed: the server keeps the one before. */
|
|
116
|
+
broken: false,
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Takes one line of the build's output.
|
|
120
|
+
* @returns {string | null} what it was - `header`, `compiling`, `compiled`, `error`, `rebuilt`, `watching` - or null for anything else
|
|
121
|
+
*/
|
|
122
|
+
push(raw) {
|
|
123
|
+
const line = plain(raw);
|
|
124
|
+
if (feed.inError && line && !MARK.test(line)) {
|
|
125
|
+
feed.error.push(line);
|
|
126
|
+
markFailed(feed, line);
|
|
127
|
+
return 'error';
|
|
128
|
+
}
|
|
129
|
+
feed.inError = false;
|
|
130
|
+
for (const [pattern, kind, take] of RULES) {
|
|
131
|
+
const match = pattern.exec(line);
|
|
132
|
+
if (match) {
|
|
133
|
+
take(feed, match);
|
|
134
|
+
return kind;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
/** How many plugins are built and how many failed. */
|
|
141
|
+
counts() {
|
|
142
|
+
const states = [...feed.plugins.values()];
|
|
143
|
+
return { built: states.filter(each => each === 'built').length, failed: states.filter(each => each === 'failed').length };
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
return feed;
|
|
147
|
+
}
|