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 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. Note that for changes to take effect you'll need to restart xp-command.
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
- import packageJson from '../package.json' with { type: 'json' };
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('-h, --help', 'display this help text');
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('\n')
94
- const firstLine = lines[0]
95
- spinner.succeed(chalk.green(`${PREFIX} ${lines.length > 1 ? firstLine + '...' : firstLine}`));
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, 'utf-8').toString('base64');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-command",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Quick cockpit commands for X-Plane 12 - set your radios, altimeter, autopilot, and more from the terminal while flying.",
5
5
  "keywords": [
6
6
  "xplane",
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(datarefNamesWithOptionalIndex.map(dataref => getDatarefValue(dataref))).then(results => {
116
- return results.map(v => String(v)).join('\n').trim()
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<number|string>}
162
+ * @return {Promise<void>}
158
163
  */
159
- export const setDatarefValues = async (datarefNamesWithOptionalIndex, value) => {
164
+ export const setDatarefValues = async (
165
+ datarefNamesWithOptionalIndex,
166
+ value,
167
+ ) => {
160
168
  if (Array.isArray(datarefNamesWithOptionalIndex)) {
161
- return Promise.all(datarefNamesWithOptionalIndex.map(dataref => setDatarefValue(dataref, value))).then(results => {
162
- return results[0]
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
- /** @type {string} */
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
+ }