xp-command 1.3.2 β 1.5.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 +46 -10
- package/bin/index.js +85 -19
- package/package.json +1 -1
- package/src/api.js +104 -14
- package/src/config.js +33 -6
- package/src/logger.js +80 -0
package/README.md
CHANGED
|
@@ -46,11 +46,7 @@ This installs xp-command globally so you can run it from anywhere.
|
|
|
46
46
|
|
|
47
47
|
## π X-Plane setup
|
|
48
48
|
|
|
49
|
-
You
|
|
50
|
-
|
|
51
|
-
1. In X-Plane 12, go to **Settings β Data Output**
|
|
52
|
-
2. Find **"Enable Web Server API"** and make sure it's enabled
|
|
53
|
-
3. Note the **port number** (default is `8086`)
|
|
49
|
+
You do not need to do anything inside X-Plane for xp-command to work. X-Plane 12 runs a local Web API automatically on `http://localhost:8086` as long as it is not started with the `--no_web_server` command-line option and the port is free. Just make sure X-Plane is running on the same machine as xp-command and that nothing (firewall or another process) is blocking or using port `8086`.
|
|
54
50
|
|
|
55
51
|
---
|
|
56
52
|
|
|
@@ -146,9 +142,11 @@ The first time you run xp-command in a new aircraft, it automatically creates a
|
|
|
146
142
|
- **macOS/Linux:** `~/.xp-command/<Aircraft Name>.yml`
|
|
147
143
|
- **Windows:** `%USERPROFILE%\.xp-command\<Aircraft Name>.yml`
|
|
148
144
|
|
|
145
|
+
You can run the command `config` to open the aircraft configuration file in your systemβs default YAML editor.
|
|
146
|
+
|
|
149
147
|
### Customizing commands
|
|
150
148
|
|
|
151
|
-
You can edit these YAML files to add aircraft-specific commands or modify existing ones.
|
|
149
|
+
You can edit these YAML files to add aircraft-specific commands or modify existing ones.
|
|
152
150
|
|
|
153
151
|
**Example**: Add a command to set heading with 10-degree increments:
|
|
154
152
|
|
|
@@ -164,11 +162,12 @@ You can edit these YAML files to add aircraft-specific commands or modify existi
|
|
|
164
162
|
- `pattern`: Regular expression matching your command (`hdg18`)
|
|
165
163
|
- `type`: Either `get` (read value and copy to clipboard) or `set` (write value)
|
|
166
164
|
- `dataref`: X-Plane dataref path (find these in X-Plane's DataRef Editor)
|
|
167
|
-
- `
|
|
165
|
+
- `command`: X-Plane command path (find these in X-Plane's DataRef Editor)
|
|
166
|
+
- `transform`: Optional value conversions (`mult<number>` to multiply or divide, `round`, `toFixed<number>` etc.)
|
|
168
167
|
|
|
169
168
|
**Finding datarefs**: Use the [DataRefTool plugin](https://datareftool.com) or check [X-Plane datarefs documentation](https://developer.x-plane.com/datarefs/).
|
|
170
169
|
|
|
171
|
-
#### Array
|
|
170
|
+
#### Array datarefs
|
|
172
171
|
|
|
173
172
|
Many X-Plane datarefs are arrays (e.g., for multiple engines, generators, radios). You can access specific array elements using `[index]` notation:
|
|
174
173
|
|
|
@@ -192,11 +191,48 @@ Here is an example of a custom set command which sets multiple datarefs at once
|
|
|
192
191
|
- sim/flightmodel2/gear/is_chocked[2]
|
|
193
192
|
```
|
|
194
193
|
|
|
195
|
-
|
|
194
|
+
#### Using commands
|
|
195
|
+
|
|
196
|
+
In addition to reading/writing datarefs, you can trigger X-Plane commands. Commands are actions (like button presses) rather than values.
|
|
197
|
+
|
|
198
|
+
**Command field**: Use `command` instead of or alongside `dataref` in `set` type operations.
|
|
199
|
+
|
|
200
|
+
**Duration syntax**: Append `[duration]` to specify how long the command stays active (in seconds):
|
|
201
|
+
- `command` or `command[0]`: Press and immediately release (default behavior)
|
|
202
|
+
- `command[5]`: Hold for 5 seconds then release
|
|
203
|
+
- Maximum duration: 10 seconds
|
|
204
|
+
|
|
205
|
+
**Examples**:
|
|
206
|
+
|
|
207
|
+
```yaml
|
|
208
|
+
# Toggle anti-ice group on
|
|
209
|
+
- pattern: "^i(1)$"
|
|
210
|
+
type: set
|
|
211
|
+
command:
|
|
212
|
+
- FJS/Q4XP/Knobs/prop_heat_up
|
|
213
|
+
- FJS/Q4XP/Knobs/Airframe_deice_mode_up
|
|
214
|
+
dataref:
|
|
215
|
+
- FJS/Q4XP/Manips/TwoSwitch_Anim[8]
|
|
216
|
+
- FJS/Q4XP/Manips/TwoSwitch_Anim[9]
|
|
217
|
+
- FJS/Q4XP/Manips/TwoSwitch_Anim[10]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Here is an example demonstrating the usage of the duration option: In the Dash 8 (Q4XP), in order to change the transponder mode, you must press a tiny button for at least two seconds! Not anymore:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
# Toggle transponder mode
|
|
224
|
+
- pattern: "^x$"
|
|
225
|
+
type: set
|
|
226
|
+
command: FJS/Q4XP/SoftKey/arcdu_1/skr4[2]
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Finding commands**: Use the [DataRefTool plugin](https://datareftool.com) to browse available commands.
|
|
230
|
+
|
|
231
|
+
## π Resetting aircraft profiles
|
|
196
232
|
|
|
197
233
|
If you've edited an aircraft configuration and xp-command crashes or stops working, you can reset to default settings by deleting the config files.
|
|
198
234
|
|
|
199
|
-
### Profile
|
|
235
|
+
### Profile location
|
|
200
236
|
|
|
201
237
|
Aircraft profiles are stored in:
|
|
202
238
|
|
package/bin/index.js
CHANGED
|
@@ -11,16 +11,28 @@ 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
|
|
|
15
|
-
import
|
|
17
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
18
|
+
import {
|
|
19
|
+
activateCommands,
|
|
20
|
+
getDatarefValues,
|
|
21
|
+
initAPI,
|
|
22
|
+
setDatarefValues,
|
|
23
|
+
} from "../src/api.js";
|
|
16
24
|
import { copyToClipboard } from "../src/clipboard.js";
|
|
17
|
-
import { getConfig } from "../src/config.js";
|
|
25
|
+
import { editConfig, getConfig } from "../src/config.js";
|
|
18
26
|
import { clearLine, hideCursor, showCursor } from "../src/console.js";
|
|
19
27
|
import { isAPIError, isEconnRefused } from "../src/error.js";
|
|
20
28
|
import history from "../src/history.js";
|
|
29
|
+
import { Logger } from "../src/logger.js";
|
|
21
30
|
import { sleep } from "../src/sleep.js";
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
const osHomedir = homedir();
|
|
33
|
+
const logger = new Logger(join(osHomedir, ".xp-command", "log.txt"), {
|
|
34
|
+
clearOnStart: true,
|
|
35
|
+
});
|
|
24
36
|
|
|
25
37
|
const PREFIX = "π© ";
|
|
26
38
|
|
|
@@ -28,7 +40,7 @@ program
|
|
|
28
40
|
.version(packageJson.version)
|
|
29
41
|
.description(`${PREFIX} ${packageJson.name}\n${packageJson.description}`)
|
|
30
42
|
.option("-p, --port <number>", "server port number")
|
|
31
|
-
.helpOption(
|
|
43
|
+
.helpOption("-h, --help", "display this help text");
|
|
32
44
|
|
|
33
45
|
/**
|
|
34
46
|
* @param {string} command
|
|
@@ -42,6 +54,7 @@ const processCommand = async (command) => {
|
|
|
42
54
|
try {
|
|
43
55
|
config = await getConfig();
|
|
44
56
|
} catch (error) {
|
|
57
|
+
logger.error(error);
|
|
45
58
|
if (isEconnRefused(error) || isAPIError(error)) {
|
|
46
59
|
spinner.fail(chalk.red(`${PREFIX} No connection - in aircraft?`));
|
|
47
60
|
hideCursor();
|
|
@@ -51,9 +64,19 @@ const processCommand = async (command) => {
|
|
|
51
64
|
}
|
|
52
65
|
}
|
|
53
66
|
|
|
67
|
+
if (command?.toLowerCase() === "config") {
|
|
68
|
+
await editConfig();
|
|
69
|
+
spinner.succeed(chalk.green(`${PREFIX} config`));
|
|
70
|
+
hideCursor();
|
|
71
|
+
await sleep(1500);
|
|
72
|
+
clearLine();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
/**
|
|
55
|
-
* @param {number|string} value
|
|
77
|
+
* @param {number|string|Array<number|string>} value
|
|
56
78
|
* @param {import('../src/config.js').Transform} transform
|
|
79
|
+
* @return {number|string|Array<number|string>}
|
|
57
80
|
*/
|
|
58
81
|
const getTransformedValue = (value, transform) => {
|
|
59
82
|
if (Array.isArray(value)) return value.slice();
|
|
@@ -79,20 +102,48 @@ const processCommand = async (command) => {
|
|
|
79
102
|
|
|
80
103
|
/** @type {Array<[RegExp, (regExpResult: Array<string> | null) => Promise<void>]>} */
|
|
81
104
|
const matches = config.commands.map((c) => {
|
|
105
|
+
if (!c.type) {
|
|
106
|
+
return [
|
|
107
|
+
new RegExp(c.pattern),
|
|
108
|
+
async () => {
|
|
109
|
+
spinner.fail(
|
|
110
|
+
chalk.red(`${PREFIX} Missing type (must be get or set)!`),
|
|
111
|
+
);
|
|
112
|
+
hideCursor();
|
|
113
|
+
await sleep(1500);
|
|
114
|
+
clearLine();
|
|
115
|
+
return;
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
82
120
|
switch (c.type) {
|
|
83
121
|
case "get":
|
|
84
122
|
return [
|
|
85
123
|
new RegExp(c.pattern),
|
|
86
124
|
async () => {
|
|
125
|
+
if (!c.dataref) {
|
|
126
|
+
spinner.fail(chalk.red(`${PREFIX} Missing dataref!`));
|
|
127
|
+
hideCursor();
|
|
128
|
+
await sleep(1500);
|
|
129
|
+
clearLine();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @type {number|string|Array<number|string>}*/
|
|
87
134
|
let value = await getDatarefValues(c.dataref);
|
|
88
135
|
c.transform?.forEach((t) => {
|
|
89
136
|
value = getTransformedValue(value, t);
|
|
90
137
|
});
|
|
91
|
-
const asString = String(value)
|
|
138
|
+
const asString = String(value);
|
|
92
139
|
await copyToClipboard(asString);
|
|
93
|
-
const lines = asString.split(
|
|
94
|
-
const firstLine = lines[0]
|
|
95
|
-
spinner.succeed(
|
|
140
|
+
const lines = asString.split("\n");
|
|
141
|
+
const firstLine = lines[0];
|
|
142
|
+
spinner.succeed(
|
|
143
|
+
chalk.green(
|
|
144
|
+
`${PREFIX} ${lines.length > 1 ? firstLine + "..." : firstLine}`,
|
|
145
|
+
),
|
|
146
|
+
);
|
|
96
147
|
hideCursor();
|
|
97
148
|
await sleep(1500);
|
|
98
149
|
clearLine();
|
|
@@ -102,16 +153,30 @@ const processCommand = async (command) => {
|
|
|
102
153
|
return [
|
|
103
154
|
new RegExp(c.pattern),
|
|
104
155
|
async (regExpResult) => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
156
|
+
if (!c.command && !c.dataref) {
|
|
157
|
+
spinner.fail(
|
|
158
|
+
chalk.red(`${PREFIX} Neither dataref nor command provided!`),
|
|
159
|
+
);
|
|
160
|
+
hideCursor();
|
|
161
|
+
await sleep(1500);
|
|
162
|
+
clearLine();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (c.command) {
|
|
167
|
+
await activateCommands(c.command);
|
|
168
|
+
}
|
|
169
|
+
if (c.dataref) {
|
|
170
|
+
let value = String(regExpResult[1]);
|
|
171
|
+
if (isNaN(Number(value))) {
|
|
172
|
+
const base64 = Buffer.from(value, "utf-8").toString("base64");
|
|
173
|
+
await setDatarefValues(c.dataref, base64);
|
|
174
|
+
} else {
|
|
175
|
+
c.transform?.forEach((t) => {
|
|
176
|
+
value = String(getTransformedValue(value, t));
|
|
177
|
+
});
|
|
178
|
+
await setDatarefValues(c.dataref, Number(value));
|
|
179
|
+
}
|
|
115
180
|
}
|
|
116
181
|
spinner.succeed(chalk.green(`${PREFIX} ${command}`));
|
|
117
182
|
hideCursor();
|
|
@@ -133,6 +198,7 @@ const processCommand = async (command) => {
|
|
|
133
198
|
|
|
134
199
|
showCursor();
|
|
135
200
|
} catch (error) {
|
|
201
|
+
logger.error(error);
|
|
136
202
|
spinner.fail();
|
|
137
203
|
hideCursor();
|
|
138
204
|
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -37,6 +37,22 @@ const parseDataref = (datarefString) => {
|
|
|
37
37
|
return [match[1], match[2] ? parseInt(match[2]) : null];
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} commandString
|
|
42
|
+
* @return {[string, number|null]}
|
|
43
|
+
*/
|
|
44
|
+
const parseCommand = (commandString) => {
|
|
45
|
+
// Regex pattern: captures base name and optional duration value
|
|
46
|
+
const pattern = /^(.+?)(?:\[(\d+)\])?$/;
|
|
47
|
+
const match = commandString.match(pattern);
|
|
48
|
+
|
|
49
|
+
if (!match) {
|
|
50
|
+
throw new CustomError("invalid_command");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return [match[1], match[2] ? parseInt(match[2]) : null];
|
|
54
|
+
};
|
|
55
|
+
|
|
40
56
|
/**
|
|
41
57
|
* @param {string} datarefName
|
|
42
58
|
* @return { Promise<number | null> }
|
|
@@ -65,6 +81,54 @@ export const initAPI = (options) => {
|
|
|
65
81
|
port = options.port;
|
|
66
82
|
};
|
|
67
83
|
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} commandName
|
|
86
|
+
* @return {Promise<number|string>}
|
|
87
|
+
*/
|
|
88
|
+
export const getCommandId = async (commandName) => {
|
|
89
|
+
const url = new URL(`http://localhost:${port}/api/v2/commands`);
|
|
90
|
+
url.searchParams.set("filter[name]", commandName);
|
|
91
|
+
url.searchParams.set("fields", "id");
|
|
92
|
+
|
|
93
|
+
const response = await fetch(url);
|
|
94
|
+
const json = /** @type { {data:[{id: number }]} | APIError } */ (
|
|
95
|
+
await response.json()
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if ("error_code" in json) {
|
|
99
|
+
throw new CustomError(json.error_code);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return json.data[0]?.id ?? null;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} commandNameWithOptionalDuration
|
|
107
|
+
*/
|
|
108
|
+
export const activateCommand = async (commandNameWithOptionalDuration) => {
|
|
109
|
+
const [commandName, duration] = parseCommand(commandNameWithOptionalDuration);
|
|
110
|
+
|
|
111
|
+
const commandId = await getCommandId(commandName);
|
|
112
|
+
|
|
113
|
+
const url = new URL(
|
|
114
|
+
`http://localhost:${port}/api/v2/command/${commandId}/activate`,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const response = await fetch(url, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
body: JSON.stringify({ duration: duration || 0 }),
|
|
120
|
+
});
|
|
121
|
+
const json = /** @type { {data: number|string } | APIError } */ (
|
|
122
|
+
await response.json()
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (json && "error_code" in json) {
|
|
126
|
+
throw new CustomError(json.error_code);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return json;
|
|
130
|
+
};
|
|
131
|
+
|
|
68
132
|
/**
|
|
69
133
|
* @param {string} datarefNameWithOptionalIndex
|
|
70
134
|
* @return {Promise<number|string>}
|
|
@@ -112,13 +176,18 @@ export const getDatarefValue = async (datarefNameWithOptionalIndex) => {
|
|
|
112
176
|
*/
|
|
113
177
|
export const getDatarefValues = async (datarefNamesWithOptionalIndex) => {
|
|
114
178
|
if (Array.isArray(datarefNamesWithOptionalIndex)) {
|
|
115
|
-
return Promise.all(
|
|
116
|
-
|
|
117
|
-
|
|
179
|
+
return Promise.all(
|
|
180
|
+
datarefNamesWithOptionalIndex.map((dataref) => getDatarefValue(dataref)),
|
|
181
|
+
).then((results) => {
|
|
182
|
+
return results
|
|
183
|
+
.map((v) => String(v))
|
|
184
|
+
.join("\n")
|
|
185
|
+
.trim();
|
|
186
|
+
});
|
|
118
187
|
}
|
|
119
|
-
|
|
120
|
-
return getDatarefValue(datarefNamesWithOptionalIndex)
|
|
121
|
-
}
|
|
188
|
+
|
|
189
|
+
return getDatarefValue(datarefNamesWithOptionalIndex);
|
|
190
|
+
};
|
|
122
191
|
|
|
123
192
|
/**
|
|
124
193
|
* @param {string} datarefNameWithOptionalIndex
|
|
@@ -154,14 +223,35 @@ export const setDatarefValue = async (datarefNameWithOptionalIndex, value) => {
|
|
|
154
223
|
/**
|
|
155
224
|
* @param {string|Array<string>} datarefNamesWithOptionalIndex
|
|
156
225
|
* @param {number|string} value
|
|
157
|
-
* @return {Promise<
|
|
226
|
+
* @return {Promise<void>}
|
|
158
227
|
*/
|
|
159
|
-
export const setDatarefValues = async (
|
|
228
|
+
export const setDatarefValues = async (
|
|
229
|
+
datarefNamesWithOptionalIndex,
|
|
230
|
+
value,
|
|
231
|
+
) => {
|
|
160
232
|
if (Array.isArray(datarefNamesWithOptionalIndex)) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
233
|
+
await Promise.all(
|
|
234
|
+
datarefNamesWithOptionalIndex.map((dataref) =>
|
|
235
|
+
setDatarefValue(dataref, value),
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
} else {
|
|
239
|
+
await setDatarefValue(datarefNamesWithOptionalIndex, value);
|
|
164
240
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {string|Array<string>} commandNamesWithOptionalDuration
|
|
245
|
+
* @return {Promise<void>}
|
|
246
|
+
*/
|
|
247
|
+
export const activateCommands = async (commandNamesWithOptionalDuration) => {
|
|
248
|
+
if (Array.isArray(commandNamesWithOptionalDuration)) {
|
|
249
|
+
await Promise.all(
|
|
250
|
+
commandNamesWithOptionalDuration.map((command) =>
|
|
251
|
+
activateCommand(command),
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
} else {
|
|
255
|
+
await activateCommand(commandNamesWithOptionalDuration);
|
|
256
|
+
}
|
|
257
|
+
};
|
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
|
|
|
@@ -21,6 +26,7 @@ const __dirname = dirname(__filename);
|
|
|
21
26
|
* @property {RegExp} pattern - Regular expression pattern to match commands
|
|
22
27
|
* @property {'get' | 'set'} type - Operation type
|
|
23
28
|
* @property {string|Array<string>} dataref - X-Plane dataref path(s)
|
|
29
|
+
* @property {string|Array<string>} command - X-Plane command path(s)
|
|
24
30
|
* @property {Transform[]} transform - Array of transformation operations to apply
|
|
25
31
|
*/
|
|
26
32
|
|
|
@@ -35,6 +41,15 @@ const __dirname = dirname(__filename);
|
|
|
35
41
|
|
|
36
42
|
const osHomedir = homedir();
|
|
37
43
|
|
|
44
|
+
const getAircraftConfigPath = async () => {
|
|
45
|
+
const aircraft = /** @type {string} */ (
|
|
46
|
+
await getDatarefValue("sim/aircraft/view/acf_ui_name")
|
|
47
|
+
)
|
|
48
|
+
.replaceAll(/[./\\]/g, " ")
|
|
49
|
+
.replace(/"/g, "");
|
|
50
|
+
return join(osHomedir, ".xp-command", `${aircraft}.yml`);
|
|
51
|
+
};
|
|
52
|
+
|
|
38
53
|
/**
|
|
39
54
|
* @return {Promise<CommandConfig>}
|
|
40
55
|
*/
|
|
@@ -43,12 +58,7 @@ export const getConfig = async () => {
|
|
|
43
58
|
recursive: true,
|
|
44
59
|
});
|
|
45
60
|
|
|
46
|
-
|
|
47
|
-
let aircraft;
|
|
48
|
-
aircraft = /** @type {string} */ (
|
|
49
|
-
await getDatarefValue("sim/aircraft/view/acf_ui_name")
|
|
50
|
-
).replaceAll(/[./\\]/g, ' ');
|
|
51
|
-
const aircraftConfigPath = join(osHomedir, ".xp-command", `${aircraft}.yml`);
|
|
61
|
+
const aircraftConfigPath = await getAircraftConfigPath();
|
|
52
62
|
|
|
53
63
|
let config;
|
|
54
64
|
try {
|
|
@@ -67,3 +77,20 @@ export const getConfig = async () => {
|
|
|
67
77
|
|
|
68
78
|
return /** @type {CommandConfig} */ (config);
|
|
69
79
|
};
|
|
80
|
+
|
|
81
|
+
export const editConfig = async () => {
|
|
82
|
+
const aircraftConfigPath = await getAircraftConfigPath();
|
|
83
|
+
|
|
84
|
+
const command =
|
|
85
|
+
platform === "win32"
|
|
86
|
+
? `start "" "${aircraftConfigPath}"`
|
|
87
|
+
: platform === "darwin"
|
|
88
|
+
? `open "${aircraftConfigPath}"`
|
|
89
|
+
: `xdg-open "${aircraftConfigPath}"`;
|
|
90
|
+
|
|
91
|
+
const { stderr } = await exec(command);
|
|
92
|
+
|
|
93
|
+
if (stderr) {
|
|
94
|
+
throw new Error(stderr);
|
|
95
|
+
}
|
|
96
|
+
};
|
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
|
+
}
|