xp-command 1.5.0 → 1.6.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
@@ -73,15 +73,25 @@ If you changed the Web API port from 8086 to another port, you'll need to start
73
73
  xp-command --port 8090
74
74
  ```
75
75
 
76
+ ### One-shot mode
77
+
78
+ Run a single command and exit immediately (useful for scripts or keybindings):
79
+
80
+ ```bash
81
+ xp-command --run x7000
82
+ ```
83
+
84
+ Exits with code `0` on success, `1` on failure.
85
+
76
86
  ### Exiting
77
87
 
78
88
  Type `exit` or press `Ctrl+C`
79
89
 
80
90
  ---
81
91
 
82
- ## 📋 Pre-configured commands
92
+ ## 📋 Pre-configured triggers
83
93
 
84
- All pre-configured commands work well with most aircraft I currently fly, but you may want to adjust them for **your** aircraft (see _Aircraft-specific profiles_ section below).
94
+ All pre-configured triggers work well with most aircraft I currently fly, but you may want to adjust them for **your** aircraft (see _Aircraft-specific profiles_ section below).
85
95
 
86
96
  ### Barometric pressure
87
97
 
@@ -200,7 +210,10 @@ In addition to reading/writing datarefs, you can trigger X-Plane commands. Comma
200
210
  **Duration syntax**: Append `[duration]` to specify how long the command stays active (in seconds):
201
211
  - `command` or `command[0]`: Press and immediately release (default behavior)
202
212
  - `command[5]`: Hold for 5 seconds then release
203
- - Maximum duration: 10 seconds
213
+
214
+ **Repeat syntax**: Append `[repeatCount]` to specify how often the command should be repeated:
215
+ - `command[0][3]`: Press and immediately release three times
216
+ - `command[0][$]`: Press and immediately release as often as defined by captured value
204
217
 
205
218
  **Examples**:
206
219
 
package/bin/index.js CHANGED
@@ -40,10 +40,12 @@ program
40
40
  .version(packageJson.version)
41
41
  .description(`${PREFIX} ${packageJson.name}\n${packageJson.description}`)
42
42
  .option("-p, --port <number>", "server port number")
43
+ .option("-r, --run <command>", "run a single command and exit")
43
44
  .helpOption("-h, --help", "display this help text");
44
45
 
45
46
  /**
46
47
  * @param {string} command
48
+ * @returns {Promise<boolean>} true if command succeeded, false otherwise
47
49
  */
48
50
  const processCommand = async (command) => {
49
51
  const spinner = ora(`${PREFIX} ${chalk.cyan(command ?? "")}`).start();
@@ -60,7 +62,7 @@ const processCommand = async (command) => {
60
62
  hideCursor();
61
63
  await sleep(1500);
62
64
  showCursor();
63
- return;
65
+ return false;
64
66
  }
65
67
  }
66
68
 
@@ -70,7 +72,7 @@ const processCommand = async (command) => {
70
72
  hideCursor();
71
73
  await sleep(1500);
72
74
  clearLine();
73
- return;
75
+ return true;
74
76
  }
75
77
 
76
78
  /**
@@ -100,7 +102,7 @@ const processCommand = async (command) => {
100
102
  return value;
101
103
  };
102
104
 
103
- /** @type {Array<[RegExp, (regExpResult: Array<string> | null) => Promise<void>]>} */
105
+ /** @type {Array<[RegExp, (regExpResult: Array<string> | null) => Promise<boolean>]>} */
104
106
  const matches = config.commands.map((c) => {
105
107
  if (!c.type) {
106
108
  return [
@@ -112,7 +114,7 @@ const processCommand = async (command) => {
112
114
  hideCursor();
113
115
  await sleep(1500);
114
116
  clearLine();
115
- return;
117
+ return false;
116
118
  },
117
119
  ];
118
120
  }
@@ -127,7 +129,7 @@ const processCommand = async (command) => {
127
129
  hideCursor();
128
130
  await sleep(1500);
129
131
  clearLine();
130
- return;
132
+ return false;
131
133
  }
132
134
 
133
135
  /** @type {number|string|Array<number|string>}*/
@@ -147,6 +149,7 @@ const processCommand = async (command) => {
147
149
  hideCursor();
148
150
  await sleep(1500);
149
151
  clearLine();
152
+ return true;
150
153
  },
151
154
  ];
152
155
  case "set":
@@ -160,11 +163,19 @@ const processCommand = async (command) => {
160
163
  hideCursor();
161
164
  await sleep(1500);
162
165
  clearLine();
163
- return;
166
+ return false;
164
167
  }
165
168
 
166
169
  if (c.command) {
167
- await activateCommands(c.command);
170
+ let value = String(regExpResult[1]);
171
+ if (isNaN(Number(value))) {
172
+ await activateCommands(c.command);
173
+ } else {
174
+ c.transform?.forEach((t) => {
175
+ value = String(getTransformedValue(value, t));
176
+ });
177
+ await activateCommands(c.command, Number(value));
178
+ }
168
179
  }
169
180
  if (c.dataref) {
170
181
  let value = String(regExpResult[1]);
@@ -182,6 +193,7 @@ const processCommand = async (command) => {
182
193
  hideCursor();
183
194
  await sleep(1500);
184
195
  clearLine();
196
+ return true;
185
197
  },
186
198
  ];
187
199
  }
@@ -191,12 +203,15 @@ const processCommand = async (command) => {
191
203
  const regexpResult = regexp.exec(command);
192
204
  if (regexpResult !== null) {
193
205
  try {
194
- await cb(regexpResult);
206
+ const success = await cb(regexpResult);
195
207
 
196
- spinner.succeed();
208
+ if (success) {
209
+ spinner.succeed();
210
+ }
197
211
  hideCursor();
198
212
 
199
213
  showCursor();
214
+ return success;
200
215
  } catch (error) {
201
216
  logger.error(error);
202
217
  spinner.fail();
@@ -220,8 +235,8 @@ const processCommand = async (command) => {
220
235
 
221
236
  await sleep(500);
222
237
  showCursor();
238
+ return false;
223
239
  }
224
- return;
225
240
  }
226
241
  }
227
242
 
@@ -230,6 +245,7 @@ const processCommand = async (command) => {
230
245
 
231
246
  await sleep(1500);
232
247
  showCursor();
248
+ return false;
233
249
  };
234
250
 
235
251
  const sayHello = () => {
@@ -291,17 +307,27 @@ const askForCommand = async () => {
291
307
  await askForCommand();
292
308
  };
293
309
 
294
- program.action(async (/** @type {{ port: number | undefined }} */ options) => {
295
- initAPI({ port: options.port ?? 8086 });
310
+ program.action(
311
+ async (
312
+ /** @type {{ port: number | undefined, run: string | undefined }} */ options,
313
+ ) => {
314
+ initAPI({ port: options.port ?? 8086 });
296
315
 
297
- hideCursor();
298
- clearLine();
299
- sayHello();
300
- await sleep(1000);
301
- showCursor();
316
+ if (options.run) {
317
+ history.addCommand(options.run);
318
+ const success = await processCommand(options.run);
319
+ process.exit(success ? 0 : 1);
320
+ }
302
321
 
303
- await askForCommand();
304
- });
322
+ hideCursor();
323
+ clearLine();
324
+ sayHello();
325
+ await sleep(1000);
326
+ showCursor();
327
+
328
+ await askForCommand();
329
+ },
330
+ );
305
331
 
306
332
  program.parse(process.argv);
307
333
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-command",
3
- "version": "1.5.0",
3
+ "version": "1.6.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
@@ -39,18 +39,27 @@ const parseDataref = (datarefString) => {
39
39
 
40
40
  /**
41
41
  * @param {string} commandString
42
- * @return {[string, number|null]}
42
+ * @param {number|string} [value]
43
+ * @return {[string, number|null, number|null]}
43
44
  */
44
- const parseCommand = (commandString) => {
45
- // Regex pattern: captures base name and optional duration value
46
- const pattern = /^(.+?)(?:\[(\d+)\])?$/;
45
+ const parseCommand = (commandString, value) => {
46
+ // Regex pattern: captures base name and optional duration and repeat count values
47
+ const pattern = /^(.+?)(?:\[(\d+)\])?(?:\[(\d+|\$)\])?$/;
47
48
  const match = commandString.match(pattern);
48
49
 
49
50
  if (!match) {
50
51
  throw new CustomError("invalid_command");
51
52
  }
52
53
 
53
- return [match[1], match[2] ? parseInt(match[2]) : null];
54
+ let repeatCount = match[3] ? parseInt(match[3]) : null;
55
+ if (isNaN(repeatCount)) {
56
+ repeatCount = typeof value === "number" ? value : parseInt(value);
57
+ }
58
+ if (isNaN(repeatCount)) {
59
+ repeatCount = 1;
60
+ }
61
+
62
+ return [match[1], match[2] ? parseInt(match[2]) : null, repeatCount];
54
63
  };
55
64
 
56
65
  /**
@@ -104,9 +113,16 @@ export const getCommandId = async (commandName) => {
104
113
 
105
114
  /**
106
115
  * @param {string} commandNameWithOptionalDuration
116
+ * @param {number|string} [value]
107
117
  */
108
- export const activateCommand = async (commandNameWithOptionalDuration) => {
109
- const [commandName, duration] = parseCommand(commandNameWithOptionalDuration);
118
+ export const activateCommand = async (
119
+ commandNameWithOptionalDuration,
120
+ value,
121
+ ) => {
122
+ const [commandName, duration, repeatCount] = parseCommand(
123
+ commandNameWithOptionalDuration,
124
+ value,
125
+ );
110
126
 
111
127
  const commandId = await getCommandId(commandName);
112
128
 
@@ -114,19 +130,19 @@ export const activateCommand = async (commandNameWithOptionalDuration) => {
114
130
  `http://localhost:${port}/api/v2/command/${commandId}/activate`,
115
131
  );
116
132
 
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
- );
133
+ for (let i = Math.max(1, repeatCount ?? 1); i--; ) {
134
+ const response = await fetch(url, {
135
+ method: "POST",
136
+ body: JSON.stringify({ duration: duration || 0 }),
137
+ });
138
+ const json = /** @type { {data: number|string } | APIError } */ (
139
+ await response.json()
140
+ );
124
141
 
125
- if (json && "error_code" in json) {
126
- throw new CustomError(json.error_code);
142
+ if (json && "error_code" in json) {
143
+ throw new CustomError(json.error_code);
144
+ }
127
145
  }
128
-
129
- return json;
130
146
  };
131
147
 
132
148
  /**
@@ -242,16 +258,20 @@ export const setDatarefValues = async (
242
258
 
243
259
  /**
244
260
  * @param {string|Array<string>} commandNamesWithOptionalDuration
261
+ * @param {number|string} [value]
245
262
  * @return {Promise<void>}
246
263
  */
247
- export const activateCommands = async (commandNamesWithOptionalDuration) => {
264
+ export const activateCommands = async (
265
+ commandNamesWithOptionalDuration,
266
+ value,
267
+ ) => {
248
268
  if (Array.isArray(commandNamesWithOptionalDuration)) {
249
269
  await Promise.all(
250
270
  commandNamesWithOptionalDuration.map((command) =>
251
- activateCommand(command),
271
+ activateCommand(command, value),
252
272
  ),
253
273
  );
254
274
  } else {
255
- await activateCommand(commandNamesWithOptionalDuration);
275
+ await activateCommand(commandNamesWithOptionalDuration, value);
256
276
  }
257
277
  };