@feralfile/cli 2.1.0 → 2.2.1

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
@@ -119,7 +119,6 @@ npm run dev -- setup
119
119
  npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 --play
120
120
  npm run dev -- play "https://example.com/video.mp4" --skip-verify
121
121
  ```
122
-
123
122
  ## Documentation
124
123
 
125
124
  - Getting started and usage: `./docs/README.md`
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feralfile/cli",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "CLI for building, signing, and playing DP-1 playlists of digital art on Feral File devices",
5
5
  "repository": {
6
6
  "type": "git",
@@ -59,7 +59,7 @@
59
59
  "license": "MIT",
60
60
  "dependencies": {
61
61
  "@napi-rs/keyring": "1.3.0",
62
- "@feralfile/source-resolver": "git+https://github.com/feral-file/ff-source-resolver.git#54be943014c8cf7860114510257cdd713bbf4e79",
62
+ "@feralfile/source-resolver": "^1.0.0",
63
63
  "axios": "^1.6.2",
64
64
  "bonjour-service": "^1.3.0",
65
65
  "chalk": "^4.1.2",
@@ -11,6 +11,7 @@ const device_lookup_1 = require("../utilities/device-lookup");
11
11
  const device_normalize_1 = require("../utilities/device-normalize");
12
12
  const device_upsert_1 = require("../utilities/device-upsert");
13
13
  const device_default_1 = require("../utilities/device-default");
14
+ const device_rename_1 = require("../utilities/device-rename");
14
15
  const config_files_1 = require("./helpers/config-files");
15
16
  const device_discovery_1 = require("./helpers/device-discovery");
16
17
  const prompt_1 = require("./helpers/prompt");
@@ -206,8 +207,10 @@ deviceCommand
206
207
  // existingIndex === -1 (no confirmed match, e.g. manual IP → .local migration),
207
208
  // a same-name entry is the upsertDevice case-3 migration path — blocking it would
208
209
  // prevent the user from retaining their existing device name during host migration.
210
+ // Case-insensitive: lookups everywhere else ignore case, so "Kitchen"
211
+ // alongside "kitchen" would make one of them unreachable by name.
209
212
  const nameConflict = existingIndex !== -1
210
- ? existingDevices.find((d, i) => d.name === deviceName && i !== existingIndex)
213
+ ? existingDevices.find((d, i) => d.name?.toLowerCase() === deviceName.toLowerCase() && i !== existingIndex)
211
214
  : undefined;
212
215
  if (nameConflict) {
213
216
  if (options.name) {
@@ -220,7 +223,7 @@ deviceCommand
220
223
  const retryAnswer = await ask('Device name: ');
221
224
  deviceName = retryAnswer || 'ff1';
222
225
  const retryConflict = existingIndex !== -1
223
- ? existingDevices.find((d, i) => d.name === deviceName && i !== existingIndex)
226
+ ? existingDevices.find((d, i) => d.name?.toLowerCase() === deviceName.toLowerCase() && i !== existingIndex)
224
227
  : undefined;
225
228
  if (retryConflict) {
226
229
  console.error(chalk_1.default.red(`\nName "${deviceName}" is also taken. No changes made.`));
@@ -254,21 +257,7 @@ deviceCommand
254
257
  }
255
258
  const config = await (0, config_files_1.readConfigFile)(configPath);
256
259
  const existingDevices = config.ff1Devices?.devices || [];
257
- // Match by name (case-insensitive) or by host URL so unnamed legacy/manual
258
- // entries (stored without a name field) can still be targeted and removed.
259
- const normalizedArg = name.toLowerCase();
260
- let normalizedArgHost = '';
261
- try {
262
- normalizedArgHost = (0, device_normalize_1.normalizeDeviceHost)(name).toLowerCase();
263
- }
264
- catch {
265
- // not a valid URL — host matching will not apply
266
- }
267
- const deviceIndex = existingDevices.findIndex((d) => (d.name && d.name.toLowerCase() === normalizedArg) ||
268
- (d.host && d.host.toLowerCase() === normalizedArg) ||
269
- (normalizedArgHost &&
270
- d.host &&
271
- (0, device_normalize_1.normalizeDeviceHost)(d.host).toLowerCase() === normalizedArgHost));
260
+ const deviceIndex = (0, device_lookup_1.findConfiguredDeviceIndex)(existingDevices, name);
272
261
  if (deviceIndex === -1) {
273
262
  console.error(chalk_1.default.red(`\nDevice "${name}" not found`));
274
263
  if (existingDevices.length > 0) {
@@ -291,6 +280,50 @@ deviceCommand
291
280
  process.exit(1);
292
281
  }
293
282
  });
283
+ deviceCommand
284
+ .command('rename')
285
+ .description('Rename a configured FF1 device (host, API key, and default status are untouched)')
286
+ .argument('<name>', 'Current device name or host')
287
+ .argument('<new-name>', 'New device name')
288
+ .action(async (name, newName) => {
289
+ try {
290
+ const configPath = await (0, config_files_1.resolveExistingConfigPath)();
291
+ if (!configPath) {
292
+ console.log(chalk_1.default.red('config.json not found'));
293
+ console.log(chalk_1.default.dim('Run: ff-cli setup'));
294
+ process.exit(1);
295
+ }
296
+ const config = await (0, config_files_1.readConfigFile)(configPath);
297
+ const existingDevices = config.ff1Devices?.devices || [];
298
+ if (existingDevices.length === 0) {
299
+ console.log(chalk_1.default.yellow('\nNo devices configured'));
300
+ console.log(chalk_1.default.dim('Run: ff-cli device add\n'));
301
+ process.exit(1);
302
+ }
303
+ let result;
304
+ try {
305
+ result = (0, device_rename_1.renameDevice)(existingDevices, name, newName);
306
+ }
307
+ catch (error) {
308
+ console.error(chalk_1.default.red(`\n${error.message}`));
309
+ const names = existingDevices.map((d) => d.name || d.host).join(', ');
310
+ console.log(chalk_1.default.dim(`Available devices: ${names}\n`));
311
+ process.exit(1);
312
+ }
313
+ if (result.unchanged) {
314
+ console.log(chalk_1.default.dim(`\n"${result.renamed.name}" is already named that.\n`));
315
+ return;
316
+ }
317
+ config.ff1Devices = { devices: result.devices };
318
+ await fs_1.promises.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
319
+ const from = result.previousName || result.renamed.host;
320
+ console.log(chalk_1.default.green(`\nRenamed device: ${from} → ${result.renamed.name}\n`));
321
+ }
322
+ catch (error) {
323
+ console.error(chalk_1.default.red('\nError:'), error.message);
324
+ process.exit(1);
325
+ }
326
+ });
294
327
  deviceCommand
295
328
  .command('default')
296
329
  .description('Set the default FF1 device (reorders so this device is used when -d is omitted)')
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.promoteDeviceToDefault = promoteDeviceToDefault;
4
- const device_normalize_1 = require("./device-normalize");
4
+ const device_lookup_1 = require("./device-lookup");
5
5
  /**
6
6
  * Move the named device to index 0 so it becomes the implicit default.
7
7
  *
@@ -11,19 +11,7 @@ const device_normalize_1 = require("./device-normalize");
11
11
  * @throws {Error} When no device matches the identifier
12
12
  */
13
13
  function promoteDeviceToDefault(devices, identifier) {
14
- const normalizedArg = identifier.toLowerCase();
15
- let normalizedArgHost = '';
16
- try {
17
- normalizedArgHost = (0, device_normalize_1.normalizeDeviceHost)(identifier).toLowerCase();
18
- }
19
- catch {
20
- // not a valid URL — host matching will not apply
21
- }
22
- const index = devices.findIndex((d) => (d.name && d.name.toLowerCase() === normalizedArg) ||
23
- (d.host && d.host.toLowerCase() === normalizedArg) ||
24
- (normalizedArgHost &&
25
- d.host &&
26
- (0, device_normalize_1.normalizeDeviceHost)(d.host).toLowerCase() === normalizedArgHost));
14
+ const index = (0, device_lookup_1.findConfiguredDeviceIndex)(devices, identifier);
27
15
  if (index === -1) {
28
16
  throw new Error(`Device "${identifier}" not found`);
29
17
  }
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.findExistingDeviceEntry = findExistingDeviceEntry;
4
+ exports.findConfiguredDeviceIndex = findConfiguredDeviceIndex;
5
+ const device_normalize_1 = require("./device-normalize");
4
6
  /**
5
7
  * Find an existing configured device that corresponds to a newly discovered host.
6
8
  *
@@ -106,3 +108,29 @@ function findExistingDeviceEntry(existingDevices, newHost, discoveredName, disco
106
108
  }
107
109
  return undefined;
108
110
  }
111
+ /**
112
+ * Find the index of a configured device by a user-supplied identifier.
113
+ *
114
+ * Matches by friendly name (case-insensitive) or by host URL — raw or
115
+ * normalized — so unnamed legacy/manual entries (stored without a name field)
116
+ * can still be targeted. Shared by `device remove`, `device default`,
117
+ * `device rename`, and runtime `-d` resolution so every surface answers to
118
+ * the same identifier the same way.
119
+ *
120
+ * Returns -1 when nothing matches.
121
+ */
122
+ function findConfiguredDeviceIndex(devices, identifier) {
123
+ const normalizedArg = identifier.toLowerCase();
124
+ let normalizedArgHost = '';
125
+ try {
126
+ normalizedArgHost = (0, device_normalize_1.normalizeDeviceHost)(identifier).toLowerCase();
127
+ }
128
+ catch {
129
+ // not a valid URL — host matching will not apply
130
+ }
131
+ return devices.findIndex((d) => (d.name && d.name.toLowerCase() === normalizedArg) ||
132
+ (d.host && d.host.toLowerCase() === normalizedArg) ||
133
+ (normalizedArgHost &&
134
+ d.host &&
135
+ (0, device_normalize_1.normalizeDeviceHost)(d.host).toLowerCase() === normalizedArgHost));
136
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renameDevice = renameDevice;
4
+ const device_lookup_1 = require("./device-lookup");
5
+ /**
6
+ * Rename a configured device without touching host, apiKey, topicID, or list
7
+ * position — a pure config edit that needs no discovery or network access
8
+ * (unlike `device add`, whose update path requires the host or mDNS).
9
+ *
10
+ * Matches by name (case-insensitive) or by host URL, mirroring `device remove`
11
+ * so unnamed legacy entries can still be targeted (renaming gives them a name
12
+ * for the first time).
13
+ *
14
+ * @throws {Error} When no device matches the identifier, the new name is
15
+ * empty, or the new name is already used by a different device
16
+ * (case-insensitive, so lookups elsewhere stay unambiguous).
17
+ */
18
+ function renameDevice(devices, identifier, newName) {
19
+ const trimmedName = newName.trim();
20
+ if (!trimmedName) {
21
+ throw new Error('New device name must not be empty');
22
+ }
23
+ const index = (0, device_lookup_1.findConfiguredDeviceIndex)(devices, identifier);
24
+ if (index === -1) {
25
+ throw new Error(`Device "${identifier}" not found`);
26
+ }
27
+ const conflict = devices.find((d, i) => i !== index && d.name && d.name.toLowerCase() === trimmedName.toLowerCase());
28
+ if (conflict) {
29
+ throw new Error(`Device name "${trimmedName}" is already used by another device (${conflict.host})`);
30
+ }
31
+ const target = devices[index];
32
+ if (target.name === trimmedName) {
33
+ return { devices: [...devices], renamed: target, previousName: target.name, unchanged: true };
34
+ }
35
+ const updated = [...devices];
36
+ updated[index] = { ...target, name: trimmedName };
37
+ return { devices: updated, renamed: updated[index], previousName: target.name, unchanged: false };
38
+ }
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.resolveConfiguredDevice = resolveConfiguredDevice;
40
40
  exports.assertFF1CommandCompatibility = assertFF1CommandCompatibility;
41
41
  const config_1 = require("../config");
42
+ const device_lookup_1 = require("./device-lookup");
42
43
  const logger = __importStar(require("../logger"));
43
44
  const FF1_COMMAND_POLICIES = {
44
45
  displayPlaylist: {
@@ -51,7 +52,8 @@ const FF1_COMMAND_POLICIES = {
51
52
  /**
52
53
  * Load and validate the configured FF1 device selected by name.
53
54
  *
54
- * @param {string} [deviceName] - Optional device name, exact match required
55
+ * @param {string} [deviceName] - Optional device name (case-insensitive) or host URL,
56
+ * resolved with the same matching rules as `device remove`/`default`/`rename`
55
57
  * @param {Object} [options] - Optional dependency overrides
56
58
  * @param {Function} [options.getFF1DeviceConfigFn] - Optional config loader override
57
59
  * @returns {FF1DeviceSelectionResult} Selected device or reason for failure
@@ -70,7 +72,8 @@ function resolveConfiguredDevice(deviceName, options = {}) {
70
72
  }
71
73
  let device = deviceConfig.devices[0];
72
74
  if (deviceName) {
73
- device = deviceConfig.devices.find((item) => item.name === deviceName);
75
+ const index = (0, device_lookup_1.findConfiguredDeviceIndex)(deviceConfig.devices, deviceName);
76
+ device = index === -1 ? undefined : deviceConfig.devices[index];
74
77
  if (!device) {
75
78
  const availableNames = deviceConfig.devices
76
79
  .map((item) => item.name)
@@ -139,6 +139,7 @@ You can also manage devices independently with:
139
139
  non-interactively with `--host`, `--name`, and the physical `--id` shown in
140
140
  Feral File mobile. Manual devices need the ID before relayer pairing.
141
141
  - `ff-cli device list` – Show all configured devices.
142
+ - `ff-cli device rename <name> <new-name>` – Rename a device without touching its host, API key, or default status. Works offline; also accepts the host URL as the identifier, so unnamed legacy entries can be given a name.
142
143
  - `ff-cli device remove <name>` – Remove a device by name.
143
144
  - `ff-cli device default <name>` – Promote a device to the top of the list so it is used when `-d` is omitted.
144
145
  - `ff-cli device pair [name]` – Display a one-time handoff code and key-bound
@@ -150,7 +151,7 @@ Setup and `device add` both preserve existing devices. Adding a device with the
150
151
  Selection rules when sending:
151
152
 
152
153
  - If you omit `-d`, the first configured device is used.
153
- - If you pass `-d <name>`, the CLI matches the device by `name` (exact match). If not found, you’ll see an error listing available devices.
154
+ - If you pass `-d <name>`, the CLI matches the device by `name` (case-insensitive) or by host URL — the same rules as `device remove`/`default`/`rename`, so unnamed entries can be targeted by host. If not found, you’ll see an error listing available devices.
154
155
 
155
156
  Device cast contract:
156
157
 
package/docs/EXAMPLES.md CHANGED
@@ -385,6 +385,9 @@ npm run dev -- device add
385
385
  # Add a device non-interactively
386
386
  npm run dev -- device add --host 192.168.1.100 --name kitchen
387
387
 
388
+ # Rename a device (host, API key, and default status are untouched)
389
+ npm run dev -- device rename kitchen gallery
390
+
388
391
  # Remove a device by name
389
392
  npm run dev -- device remove kitchen
390
393
 
package/docs/README.md CHANGED
@@ -120,6 +120,7 @@ Both paths run the same deterministic pipeline: fetch metadata, assemble a DP-1
120
120
  - `device add` – Add a device with mDNS or manual `--host`, `--name`, and `--id`
121
121
  - Options: `--host <host>`, `--name <name>`, `--id <id>`
122
122
  - `device pair [name]` – Securely receive relayer access from Feral File mobile
123
+ - `device rename <name> <new-name>` – Rename a configured FF1 device (host, API key, and default status are untouched)
123
124
  - `device remove <name>` – Remove a configured FF1 device
124
125
  - `device default <name>` – Set the default FF1 device (used when `-d` is omitted)
125
126
  - `config <init|show|validate>` – Manage configuration
@@ -274,6 +275,9 @@ ff-cli device add
274
275
  # Add a device non-interactively
275
276
  ff-cli device add --host 192.168.1.100 --name kitchen --id FF1-XXXXXXXX
276
277
 
278
+ # Rename a device (host, API key, and default status are untouched)
279
+ ff-cli device rename kitchen gallery
280
+
277
281
  # Remove a device by name
278
282
  ff-cli device remove kitchen
279
283
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feralfile/cli",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "CLI for building, signing, and playing DP-1 playlists of digital art on Feral File devices",
5
5
  "repository": {
6
6
  "type": "git",
@@ -59,7 +59,7 @@
59
59
  "license": "MIT",
60
60
  "dependencies": {
61
61
  "@napi-rs/keyring": "1.3.0",
62
- "@feralfile/source-resolver": "git+https://github.com/feral-file/ff-source-resolver.git#54be943014c8cf7860114510257cdd713bbf4e79",
62
+ "@feralfile/source-resolver": "^1.0.0",
63
63
  "axios": "^1.6.2",
64
64
  "bonjour-service": "^1.3.0",
65
65
  "chalk": "^4.1.2",