xp-command 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/bin/index.js +34 -10
- package/package.json +1 -1
- package/src/api.js +24 -14
- package/src/config.js +32 -6
- package/src/logger.js +80 -0
package/README.md
CHANGED
|
@@ -146,9 +146,11 @@ The first time you run xp-command in a new aircraft, it automatically creates a
|
|
|
146
146
|
- **macOS/Linux:** `~/.xp-command/<Aircraft Name>.yml`
|
|
147
147
|
- **Windows:** `%USERPROFILE%\.xp-command\<Aircraft Name>.yml`
|
|
148
148
|
|
|
149
|
+
You can run the command `config` to open the aircraft configuration file in your system’s default YAML editor.
|
|
150
|
+
|
|
149
151
|
### Customizing commands
|
|
150
152
|
|
|
151
|
-
You can edit these YAML files to add aircraft-specific commands or modify existing ones.
|
|
153
|
+
You can edit these YAML files to add aircraft-specific commands or modify existing ones.
|
|
152
154
|
|
|
153
155
|
**Example**: Add a command to set heading with 10-degree increments:
|
|
154
156
|
|
package/bin/index.js
CHANGED
|
@@ -11,16 +11,23 @@ import {
|
|
|
11
11
|
import chalk from "chalk";
|
|
12
12
|
import { program } from "commander";
|
|
13
13
|
import ora from "ora";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { join } from "path";
|
|
14
16
|
|
|
17
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
15
18
|
import { getDatarefValues, initAPI, setDatarefValues } from "../src/api.js";
|
|
16
19
|
import { copyToClipboard } from "../src/clipboard.js";
|
|
17
|
-
import { getConfig } from "../src/config.js";
|
|
20
|
+
import { editConfig, getConfig } from "../src/config.js";
|
|
18
21
|
import { clearLine, hideCursor, showCursor } from "../src/console.js";
|
|
19
22
|
import { isAPIError, isEconnRefused } from "../src/error.js";
|
|
20
23
|
import history from "../src/history.js";
|
|
24
|
+
import { Logger } from "../src/logger.js";
|
|
21
25
|
import { sleep } from "../src/sleep.js";
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
const osHomedir = homedir();
|
|
28
|
+
const logger = new Logger(join(osHomedir, ".xp-command", "log.txt"), {
|
|
29
|
+
clearOnStart: true,
|
|
30
|
+
});
|
|
24
31
|
|
|
25
32
|
const PREFIX = "🛩 ";
|
|
26
33
|
|
|
@@ -28,7 +35,7 @@ program
|
|
|
28
35
|
.version(packageJson.version)
|
|
29
36
|
.description(`${PREFIX} ${packageJson.name}\n${packageJson.description}`)
|
|
30
37
|
.option("-p, --port <number>", "server port number")
|
|
31
|
-
.helpOption(
|
|
38
|
+
.helpOption("-h, --help", "display this help text");
|
|
32
39
|
|
|
33
40
|
/**
|
|
34
41
|
* @param {string} command
|
|
@@ -42,6 +49,7 @@ const processCommand = async (command) => {
|
|
|
42
49
|
try {
|
|
43
50
|
config = await getConfig();
|
|
44
51
|
} catch (error) {
|
|
52
|
+
logger.error(error);
|
|
45
53
|
if (isEconnRefused(error) || isAPIError(error)) {
|
|
46
54
|
spinner.fail(chalk.red(`${PREFIX} No connection - in aircraft?`));
|
|
47
55
|
hideCursor();
|
|
@@ -51,9 +59,19 @@ const processCommand = async (command) => {
|
|
|
51
59
|
}
|
|
52
60
|
}
|
|
53
61
|
|
|
62
|
+
if (command?.toLowerCase() === "config") {
|
|
63
|
+
await editConfig();
|
|
64
|
+
spinner.succeed(chalk.green(`${PREFIX} config`));
|
|
65
|
+
hideCursor();
|
|
66
|
+
await sleep(1500);
|
|
67
|
+
clearLine();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
/**
|
|
55
|
-
* @param {number|string} value
|
|
72
|
+
* @param {number|string|Array<number|string>} value
|
|
56
73
|
* @param {import('../src/config.js').Transform} transform
|
|
74
|
+
* @return {number|string|Array<number|string>}
|
|
57
75
|
*/
|
|
58
76
|
const getTransformedValue = (value, transform) => {
|
|
59
77
|
if (Array.isArray(value)) return value.slice();
|
|
@@ -84,15 +102,20 @@ const processCommand = async (command) => {
|
|
|
84
102
|
return [
|
|
85
103
|
new RegExp(c.pattern),
|
|
86
104
|
async () => {
|
|
105
|
+
/** @type {number|string|Array<number|string>}*/
|
|
87
106
|
let value = await getDatarefValues(c.dataref);
|
|
88
107
|
c.transform?.forEach((t) => {
|
|
89
108
|
value = getTransformedValue(value, t);
|
|
90
109
|
});
|
|
91
|
-
const asString = String(value)
|
|
110
|
+
const asString = String(value);
|
|
92
111
|
await copyToClipboard(asString);
|
|
93
|
-
const lines = asString.split(
|
|
94
|
-
const firstLine = lines[0]
|
|
95
|
-
spinner.succeed(
|
|
112
|
+
const lines = asString.split("\n");
|
|
113
|
+
const firstLine = lines[0];
|
|
114
|
+
spinner.succeed(
|
|
115
|
+
chalk.green(
|
|
116
|
+
`${PREFIX} ${lines.length > 1 ? firstLine + "..." : firstLine}`,
|
|
117
|
+
),
|
|
118
|
+
);
|
|
96
119
|
hideCursor();
|
|
97
120
|
await sleep(1500);
|
|
98
121
|
clearLine();
|
|
@@ -103,9 +126,9 @@ const processCommand = async (command) => {
|
|
|
103
126
|
new RegExp(c.pattern),
|
|
104
127
|
async (regExpResult) => {
|
|
105
128
|
let value = String(regExpResult[1]);
|
|
106
|
-
|
|
129
|
+
|
|
107
130
|
if (isNaN(Number(value))) {
|
|
108
|
-
const base64 = Buffer.from(value,
|
|
131
|
+
const base64 = Buffer.from(value, "utf-8").toString("base64");
|
|
109
132
|
await setDatarefValues(c.dataref, base64);
|
|
110
133
|
} else {
|
|
111
134
|
c.transform?.forEach((t) => {
|
|
@@ -133,6 +156,7 @@ const processCommand = async (command) => {
|
|
|
133
156
|
|
|
134
157
|
showCursor();
|
|
135
158
|
} catch (error) {
|
|
159
|
+
logger.error(error);
|
|
136
160
|
spinner.fail();
|
|
137
161
|
hideCursor();
|
|
138
162
|
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -112,13 +112,18 @@ export const getDatarefValue = async (datarefNameWithOptionalIndex) => {
|
|
|
112
112
|
*/
|
|
113
113
|
export const getDatarefValues = async (datarefNamesWithOptionalIndex) => {
|
|
114
114
|
if (Array.isArray(datarefNamesWithOptionalIndex)) {
|
|
115
|
-
return Promise.all(
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
return Promise.all(
|
|
116
|
+
datarefNamesWithOptionalIndex.map((dataref) => getDatarefValue(dataref)),
|
|
117
|
+
).then((results) => {
|
|
118
|
+
return results
|
|
119
|
+
.map((v) => String(v))
|
|
120
|
+
.join("\n")
|
|
121
|
+
.trim();
|
|
122
|
+
});
|
|
118
123
|
}
|
|
119
|
-
|
|
120
|
-
return getDatarefValue(datarefNamesWithOptionalIndex)
|
|
121
|
-
}
|
|
124
|
+
|
|
125
|
+
return getDatarefValue(datarefNamesWithOptionalIndex);
|
|
126
|
+
};
|
|
122
127
|
|
|
123
128
|
/**
|
|
124
129
|
* @param {string} datarefNameWithOptionalIndex
|
|
@@ -154,14 +159,19 @@ export const setDatarefValue = async (datarefNameWithOptionalIndex, value) => {
|
|
|
154
159
|
/**
|
|
155
160
|
* @param {string|Array<string>} datarefNamesWithOptionalIndex
|
|
156
161
|
* @param {number|string} value
|
|
157
|
-
* @return {Promise<
|
|
162
|
+
* @return {Promise<void>}
|
|
158
163
|
*/
|
|
159
|
-
export const setDatarefValues = async (
|
|
164
|
+
export const setDatarefValues = async (
|
|
165
|
+
datarefNamesWithOptionalIndex,
|
|
166
|
+
value,
|
|
167
|
+
) => {
|
|
160
168
|
if (Array.isArray(datarefNamesWithOptionalIndex)) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
await Promise.all(
|
|
170
|
+
datarefNamesWithOptionalIndex.map((dataref) =>
|
|
171
|
+
setDatarefValue(dataref, value),
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
} else {
|
|
175
|
+
await setDatarefValue(datarefNamesWithOptionalIndex, value);
|
|
164
176
|
}
|
|
165
|
-
|
|
166
|
-
return setDatarefValue(datarefNamesWithOptionalIndex, value)
|
|
167
|
-
}
|
|
177
|
+
};
|
package/src/config.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { exec as rawExec } from "node:child_process";
|
|
1
2
|
import { copyFile, mkdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { platform } from "node:process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
2
5
|
|
|
3
6
|
import yaml from "js-yaml";
|
|
4
7
|
import { homedir } from "os";
|
|
@@ -8,6 +11,8 @@ import { fileURLToPath } from "url";
|
|
|
8
11
|
|
|
9
12
|
import { getDatarefValue } from "./api.js";
|
|
10
13
|
|
|
14
|
+
const exec = promisify(rawExec);
|
|
15
|
+
|
|
11
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
17
|
const __dirname = dirname(__filename);
|
|
13
18
|
|
|
@@ -35,6 +40,15 @@ const __dirname = dirname(__filename);
|
|
|
35
40
|
|
|
36
41
|
const osHomedir = homedir();
|
|
37
42
|
|
|
43
|
+
const getAircraftConfigPath = async () => {
|
|
44
|
+
const aircraft = /** @type {string} */ (
|
|
45
|
+
await getDatarefValue("sim/aircraft/view/acf_ui_name")
|
|
46
|
+
)
|
|
47
|
+
.replaceAll(/[./\\]/g, " ")
|
|
48
|
+
.replace(/"/g, "");
|
|
49
|
+
return join(osHomedir, ".xp-command", `${aircraft}.yml`);
|
|
50
|
+
};
|
|
51
|
+
|
|
38
52
|
/**
|
|
39
53
|
* @return {Promise<CommandConfig>}
|
|
40
54
|
*/
|
|
@@ -43,12 +57,7 @@ export const getConfig = async () => {
|
|
|
43
57
|
recursive: true,
|
|
44
58
|
});
|
|
45
59
|
|
|
46
|
-
|
|
47
|
-
let aircraft;
|
|
48
|
-
aircraft = /** @type {string} */ (
|
|
49
|
-
await getDatarefValue("sim/aircraft/view/acf_ui_name")
|
|
50
|
-
);
|
|
51
|
-
const aircraftConfigPath = join(osHomedir, ".xp-command", `${aircraft}.yml`);
|
|
60
|
+
const aircraftConfigPath = await getAircraftConfigPath();
|
|
52
61
|
|
|
53
62
|
let config;
|
|
54
63
|
try {
|
|
@@ -67,3 +76,20 @@ export const getConfig = async () => {
|
|
|
67
76
|
|
|
68
77
|
return /** @type {CommandConfig} */ (config);
|
|
69
78
|
};
|
|
79
|
+
|
|
80
|
+
export const editConfig = async () => {
|
|
81
|
+
const aircraftConfigPath = await getAircraftConfigPath();
|
|
82
|
+
|
|
83
|
+
const command =
|
|
84
|
+
platform === "win32"
|
|
85
|
+
? `start "" "${aircraftConfigPath}"`
|
|
86
|
+
: platform === "darwin"
|
|
87
|
+
? `open "${aircraftConfigPath}"`
|
|
88
|
+
: `xdg-open "${aircraftConfigPath}"`;
|
|
89
|
+
|
|
90
|
+
const { stderr } = await exec(command);
|
|
91
|
+
|
|
92
|
+
if (stderr) {
|
|
93
|
+
throw new Error(stderr);
|
|
94
|
+
}
|
|
95
|
+
};
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// logger.js
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {"debug" | "info" | "warn" | "error"} LogLevel
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} LoggerOptions
|
|
11
|
+
* @property {boolean} [clearOnStart]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export class Logger {
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} filePath
|
|
17
|
+
* @param {LoggerOptions} [options]
|
|
18
|
+
*/
|
|
19
|
+
constructor(filePath, options = {}) {
|
|
20
|
+
/** @private */
|
|
21
|
+
this.filePath = filePath;
|
|
22
|
+
|
|
23
|
+
/** @private */
|
|
24
|
+
this.options = options;
|
|
25
|
+
|
|
26
|
+
const dir = path.dirname(filePath);
|
|
27
|
+
if (!fs.existsSync(dir)) {
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (options.clearOnStart && fs.existsSync(filePath)) {
|
|
32
|
+
// clear file on app start
|
|
33
|
+
fs.truncateSync(filePath, 0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @private
|
|
38
|
+
* @type {fs.WriteStream}
|
|
39
|
+
*/
|
|
40
|
+
this.stream = fs.createWriteStream(filePath, { flags: "a" });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @private
|
|
45
|
+
* @param {LogLevel} level
|
|
46
|
+
* @param {string} msg
|
|
47
|
+
*/
|
|
48
|
+
write(level, msg) {
|
|
49
|
+
const line = `${new Date().toISOString()} [${level.toUpperCase()}] ${msg}\n`;
|
|
50
|
+
this.stream.write(line);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} msg
|
|
55
|
+
*/
|
|
56
|
+
debug(msg) {
|
|
57
|
+
this.write("debug", msg);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} msg
|
|
62
|
+
*/
|
|
63
|
+
info(msg) {
|
|
64
|
+
this.write("info", msg);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} msg
|
|
69
|
+
*/
|
|
70
|
+
warn(msg) {
|
|
71
|
+
this.write("warn", msg);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} msg
|
|
76
|
+
*/
|
|
77
|
+
error(msg) {
|
|
78
|
+
this.write("error", msg);
|
|
79
|
+
}
|
|
80
|
+
}
|