@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 +0 -1
- package/dist/package.json +2 -2
- package/dist/src/commands/device.js +50 -17
- package/dist/src/utilities/device-default.js +2 -14
- package/dist/src/utilities/device-lookup.js +28 -0
- package/dist/src/utilities/device-rename.js +38 -0
- package/dist/src/utilities/ff1-compatibility.js +5 -2
- package/docs/CONFIGURATION.md +2 -1
- package/docs/EXAMPLES.md +3 -0
- package/docs/README.md +4 -0
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feralfile/cli",
|
|
3
|
-
"version": "2.1
|
|
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": "
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -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` (
|
|
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
|
|
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": "
|
|
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",
|