@feralfile/cli 1.1.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/LICENSE +21 -0
- package/README.md +96 -0
- package/config.json.example +96 -0
- package/dist/index.js +54 -0
- package/dist/src/ai-orchestrator/index.js +1019 -0
- package/dist/src/ai-orchestrator/registry.js +96 -0
- package/dist/src/commands/build.js +69 -0
- package/dist/src/commands/chat.js +189 -0
- package/dist/src/commands/config.js +68 -0
- package/dist/src/commands/device.js +278 -0
- package/dist/src/commands/helpers/config-files.js +62 -0
- package/dist/src/commands/helpers/device-discovery.js +111 -0
- package/dist/src/commands/helpers/playlist-display.js +161 -0
- package/dist/src/commands/helpers/prompt.js +65 -0
- package/dist/src/commands/helpers/ssh-helpers.js +44 -0
- package/dist/src/commands/play.js +110 -0
- package/dist/src/commands/publish.js +115 -0
- package/dist/src/commands/setup.js +225 -0
- package/dist/src/commands/sign.js +41 -0
- package/dist/src/commands/ssh.js +108 -0
- package/dist/src/commands/status.js +126 -0
- package/dist/src/commands/validate.js +18 -0
- package/dist/src/config.js +441 -0
- package/dist/src/intent-parser/index.js +1382 -0
- package/dist/src/intent-parser/utils.js +108 -0
- package/dist/src/logger.js +82 -0
- package/dist/src/main.js +459 -0
- package/dist/src/types.js +5 -0
- package/dist/src/utilities/address-validator.js +242 -0
- package/dist/src/utilities/device-default.js +36 -0
- package/dist/src/utilities/device-lookup.js +107 -0
- package/dist/src/utilities/device-normalize.js +62 -0
- package/dist/src/utilities/device-upsert.js +91 -0
- package/dist/src/utilities/domain-resolver.js +291 -0
- package/dist/src/utilities/ed25519-key-derive.js +155 -0
- package/dist/src/utilities/feed-fetcher.js +471 -0
- package/dist/src/utilities/ff1-compatibility.js +269 -0
- package/dist/src/utilities/ff1-device.js +250 -0
- package/dist/src/utilities/ff1-discovery.js +330 -0
- package/dist/src/utilities/functions.js +308 -0
- package/dist/src/utilities/index.js +469 -0
- package/dist/src/utilities/nft-indexer.js +1024 -0
- package/dist/src/utilities/playlist-builder.js +523 -0
- package/dist/src/utilities/playlist-publisher.js +131 -0
- package/dist/src/utilities/playlist-send.js +260 -0
- package/dist/src/utilities/playlist-signer.js +204 -0
- package/dist/src/utilities/playlist-signing-role.js +41 -0
- package/dist/src/utilities/playlist-source.js +128 -0
- package/dist/src/utilities/playlist-verifier.js +274 -0
- package/dist/src/utilities/ssh-access.js +145 -0
- package/dist/src/utils.js +48 -0
- package/docs/CONFIGURATION.md +206 -0
- package/docs/EXAMPLES.md +390 -0
- package/docs/FUNCTION_CALLING.md +96 -0
- package/docs/PROJECT_SPEC.md +228 -0
- package/docs/README.md +348 -0
- package/docs/RELEASING.md +73 -0
- package/package.json +76 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.deviceCommand = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const device_lookup_1 = require("../utilities/device-lookup");
|
|
11
|
+
const device_normalize_1 = require("../utilities/device-normalize");
|
|
12
|
+
const device_upsert_1 = require("../utilities/device-upsert");
|
|
13
|
+
const device_default_1 = require("../utilities/device-default");
|
|
14
|
+
const config_files_1 = require("./helpers/config-files");
|
|
15
|
+
const device_discovery_1 = require("./helpers/device-discovery");
|
|
16
|
+
const prompt_1 = require("./helpers/prompt");
|
|
17
|
+
const deviceCommand = new commander_1.Command('device').description('Manage configured FF1 devices');
|
|
18
|
+
exports.deviceCommand = deviceCommand;
|
|
19
|
+
deviceCommand
|
|
20
|
+
.command('list')
|
|
21
|
+
.description('List all configured FF1 devices')
|
|
22
|
+
.action(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const configPath = await (0, config_files_1.resolveExistingConfigPath)();
|
|
25
|
+
if (!configPath) {
|
|
26
|
+
console.log(chalk_1.default.red('config.json not found'));
|
|
27
|
+
console.log(chalk_1.default.dim('Run: ff1 setup'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const config = await (0, config_files_1.readConfigFile)(configPath);
|
|
31
|
+
const devices = config.ff1Devices?.devices || [];
|
|
32
|
+
if (devices.length === 0) {
|
|
33
|
+
console.log(chalk_1.default.yellow('\nNo devices configured'));
|
|
34
|
+
console.log(chalk_1.default.dim('Run: ff1 device add'));
|
|
35
|
+
console.log();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.log(chalk_1.default.blue(`\nFF1 Devices (${devices.length})\n`));
|
|
39
|
+
devices.forEach((device, index) => {
|
|
40
|
+
const isFirst = index === 0;
|
|
41
|
+
const marker = isFirst ? chalk_1.default.green('→') : ' ';
|
|
42
|
+
const nameLabel = device.name || 'unnamed';
|
|
43
|
+
console.log(`${marker} ${chalk_1.default.bold(nameLabel)}`);
|
|
44
|
+
console.log(` Host: ${chalk_1.default.dim(device.host)}`);
|
|
45
|
+
if (device.apiKey) {
|
|
46
|
+
console.log(` API key: ${chalk_1.default.green('Set')}`);
|
|
47
|
+
}
|
|
48
|
+
if (device.topicID) {
|
|
49
|
+
console.log(` Topic: ${chalk_1.default.dim(device.topicID)}`);
|
|
50
|
+
}
|
|
51
|
+
if (isFirst) {
|
|
52
|
+
console.log(` ${chalk_1.default.dim('(default)')}`);
|
|
53
|
+
}
|
|
54
|
+
console.log();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.error(chalk_1.default.red('\nError:'), error.message);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
deviceCommand
|
|
63
|
+
.command('add')
|
|
64
|
+
.description('Add a new FF1 device (with mDNS discovery)')
|
|
65
|
+
.option('--host <host>', 'Device host (skip discovery)')
|
|
66
|
+
.option('--name <name>', 'Device name')
|
|
67
|
+
.action(async (options) => {
|
|
68
|
+
// Lazy prompt: non-interactive paths (--host + --name) must never block on stdin.
|
|
69
|
+
let prompt = null;
|
|
70
|
+
const ask = async (question) => {
|
|
71
|
+
if (!prompt) {
|
|
72
|
+
prompt = (0, prompt_1.createPrompt)();
|
|
73
|
+
}
|
|
74
|
+
return prompt.ask(question);
|
|
75
|
+
};
|
|
76
|
+
const closePrompt = () => {
|
|
77
|
+
if (prompt) {
|
|
78
|
+
prompt.close();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
const configPath = await (0, config_files_1.resolveExistingConfigPath)();
|
|
83
|
+
if (!configPath) {
|
|
84
|
+
console.log(chalk_1.default.red('config.json not found'));
|
|
85
|
+
console.log(chalk_1.default.dim('Run: ff1 setup'));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const config = await (0, config_files_1.readConfigFile)(configPath);
|
|
89
|
+
const existingDevices = config.ff1Devices?.devices || [];
|
|
90
|
+
let hostValue = '';
|
|
91
|
+
let discoveredName = '';
|
|
92
|
+
let discoveredId;
|
|
93
|
+
let discoveredAddresses;
|
|
94
|
+
if (options.host) {
|
|
95
|
+
hostValue = (0, device_normalize_1.normalizeDeviceHost)(options.host);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.log(chalk_1.default.blue('\nDiscover FF1 devices...\n'));
|
|
99
|
+
const selection = await (0, device_discovery_1.discoverAndSelectDevice)(ask, existingDevices);
|
|
100
|
+
hostValue = selection.hostValue;
|
|
101
|
+
discoveredName = selection.discoveredName;
|
|
102
|
+
discoveredId = selection.discoveredId;
|
|
103
|
+
discoveredAddresses = selection.discoveredAddresses;
|
|
104
|
+
if (!hostValue) {
|
|
105
|
+
console.log(chalk_1.default.dim('\nNo device added.'));
|
|
106
|
+
closePrompt();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Find any existing entry that represents this device, including cases
|
|
111
|
+
// where the host URL changed (IP ↔ .local) since the device was last added.
|
|
112
|
+
const existingEntry = (0, device_lookup_1.findExistingDeviceEntry)(existingDevices, hostValue, discoveredName, discoveredId, discoveredAddresses);
|
|
113
|
+
const existingIndex = existingEntry
|
|
114
|
+
? existingDevices.findIndex((d) => d === existingEntry)
|
|
115
|
+
: -1;
|
|
116
|
+
if (existingIndex !== -1) {
|
|
117
|
+
if (options.host && options.name) {
|
|
118
|
+
// Non-interactive: auto-overwrite when both flags are supplied.
|
|
119
|
+
console.log(chalk_1.default.yellow(`\nUpdating existing device: ${existingDevices[existingIndex].name || existingDevices[existingIndex].host}`));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.log(chalk_1.default.yellow(`\nDevice already configured: ${existingDevices[existingIndex].name || existingDevices[existingIndex].host}`));
|
|
123
|
+
const overwrite = await (0, prompt_1.promptYesNo)(ask, 'Update this device?', false);
|
|
124
|
+
if (!overwrite) {
|
|
125
|
+
console.log(chalk_1.default.dim('No changes made.'));
|
|
126
|
+
closePrompt();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Preserve the stored friendly name as the default so a blank prompt never
|
|
132
|
+
// clobbers a curated label (even after a host-URL change).
|
|
133
|
+
const existingName = existingEntry?.name || '';
|
|
134
|
+
let deviceName;
|
|
135
|
+
if (options.name) {
|
|
136
|
+
deviceName = options.name;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const defaultName = existingName || discoveredName || '';
|
|
140
|
+
const namePrompt = defaultName
|
|
141
|
+
? `Device name (kitchen, office, etc.) [${defaultName}]: `
|
|
142
|
+
: 'Device name (kitchen, office, etc.): ';
|
|
143
|
+
const nameAnswer = await ask(namePrompt);
|
|
144
|
+
deviceName = nameAnswer || defaultName || 'ff1';
|
|
145
|
+
}
|
|
146
|
+
// Reject a name that is already used by a DIFFERENT device (not the one being updated).
|
|
147
|
+
// Only applies when existingIndex !== -1: we know exactly which row to update, so a
|
|
148
|
+
// same-name entry at a different index is provably a different device. When
|
|
149
|
+
// existingIndex === -1 (no confirmed match, e.g. manual IP → .local migration),
|
|
150
|
+
// a same-name entry is the upsertDevice case-3 migration path — blocking it would
|
|
151
|
+
// prevent the user from retaining their existing device name during host migration.
|
|
152
|
+
const nameConflict = existingIndex !== -1
|
|
153
|
+
? existingDevices.find((d, i) => d.name === deviceName && i !== existingIndex)
|
|
154
|
+
: undefined;
|
|
155
|
+
if (nameConflict) {
|
|
156
|
+
if (options.name) {
|
|
157
|
+
console.error(chalk_1.default.red(`\nError: device name "${deviceName}" is already used by another device (${nameConflict.host}).`));
|
|
158
|
+
console.error(chalk_1.default.dim('Use a different name or run "ff1 device remove" first.'));
|
|
159
|
+
closePrompt();
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
console.log(chalk_1.default.yellow(`"${deviceName}" is already used by another device. Please choose a different name.`));
|
|
163
|
+
const retryAnswer = await ask('Device name: ');
|
|
164
|
+
deviceName = retryAnswer || 'ff1';
|
|
165
|
+
const retryConflict = existingIndex !== -1
|
|
166
|
+
? existingDevices.find((d, i) => d.name === deviceName && i !== existingIndex)
|
|
167
|
+
: undefined;
|
|
168
|
+
if (retryConflict) {
|
|
169
|
+
console.error(chalk_1.default.red(`\nName "${deviceName}" is also taken. No changes made.`));
|
|
170
|
+
closePrompt();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const result = (0, device_upsert_1.upsertDevice)(existingDevices, { name: deviceName, host: hostValue, id: discoveredId, addresses: discoveredAddresses }, existingIndex !== -1 ? existingIndex : undefined);
|
|
175
|
+
console.log(chalk_1.default.green(`\n${result.updated ? 'Updated' : 'Added'} device: ${deviceName}`));
|
|
176
|
+
config.ff1Devices = { devices: result.devices };
|
|
177
|
+
await fs_1.promises.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
|
|
178
|
+
console.log(chalk_1.default.dim(`Total devices: ${result.devices.length}\n`));
|
|
179
|
+
closePrompt();
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
console.error(chalk_1.default.red('\nError:'), error.message);
|
|
183
|
+
closePrompt();
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
deviceCommand
|
|
188
|
+
.command('remove')
|
|
189
|
+
.description('Remove a configured FF1 device')
|
|
190
|
+
.argument('<name>', 'Device name to remove')
|
|
191
|
+
.action(async (name) => {
|
|
192
|
+
try {
|
|
193
|
+
const configPath = await (0, config_files_1.resolveExistingConfigPath)();
|
|
194
|
+
if (!configPath) {
|
|
195
|
+
console.log(chalk_1.default.red('config.json not found'));
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
const config = await (0, config_files_1.readConfigFile)(configPath);
|
|
199
|
+
const existingDevices = config.ff1Devices?.devices || [];
|
|
200
|
+
// Match by name (case-insensitive) or by host URL so unnamed legacy/manual
|
|
201
|
+
// entries (stored without a name field) can still be targeted and removed.
|
|
202
|
+
const normalizedArg = name.toLowerCase();
|
|
203
|
+
let normalizedArgHost = '';
|
|
204
|
+
try {
|
|
205
|
+
normalizedArgHost = (0, device_normalize_1.normalizeDeviceHost)(name).toLowerCase();
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// not a valid URL — host matching will not apply
|
|
209
|
+
}
|
|
210
|
+
const deviceIndex = existingDevices.findIndex((d) => (d.name && d.name.toLowerCase() === normalizedArg) ||
|
|
211
|
+
(d.host && d.host.toLowerCase() === normalizedArg) ||
|
|
212
|
+
(normalizedArgHost &&
|
|
213
|
+
d.host &&
|
|
214
|
+
(0, device_normalize_1.normalizeDeviceHost)(d.host).toLowerCase() === normalizedArgHost));
|
|
215
|
+
if (deviceIndex === -1) {
|
|
216
|
+
console.error(chalk_1.default.red(`\nDevice "${name}" not found`));
|
|
217
|
+
if (existingDevices.length > 0) {
|
|
218
|
+
const names = existingDevices.map((d) => d.name || d.host).join(', ');
|
|
219
|
+
console.log(chalk_1.default.dim(`Available devices: ${names}`));
|
|
220
|
+
}
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
const removed = existingDevices[deviceIndex];
|
|
224
|
+
const updatedDevices = existingDevices.filter((_, i) => i !== deviceIndex);
|
|
225
|
+
config.ff1Devices = { devices: updatedDevices };
|
|
226
|
+
await fs_1.promises.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
|
|
227
|
+
console.log(chalk_1.default.green(`\nRemoved device: ${removed.name || removed.host}`));
|
|
228
|
+
console.log(chalk_1.default.dim(`Remaining devices: ${updatedDevices.length}\n`));
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
console.error(chalk_1.default.red('\nError:'), error.message);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
deviceCommand
|
|
236
|
+
.command('default')
|
|
237
|
+
.description('Set the default FF1 device (reorders so this device is used when -d is omitted)')
|
|
238
|
+
.argument('<name>', 'Device name or host to promote to default')
|
|
239
|
+
.action(async (name) => {
|
|
240
|
+
try {
|
|
241
|
+
const configPath = await (0, config_files_1.resolveExistingConfigPath)();
|
|
242
|
+
if (!configPath) {
|
|
243
|
+
console.log(chalk_1.default.red('config.json not found'));
|
|
244
|
+
console.log(chalk_1.default.dim('Run: ff1 setup'));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
const config = await (0, config_files_1.readConfigFile)(configPath);
|
|
248
|
+
const existingDevices = config.ff1Devices?.devices || [];
|
|
249
|
+
if (existingDevices.length === 0) {
|
|
250
|
+
console.log(chalk_1.default.yellow('\nNo devices configured'));
|
|
251
|
+
console.log(chalk_1.default.dim('Run: ff1 device add\n'));
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
let result;
|
|
255
|
+
try {
|
|
256
|
+
result = (0, device_default_1.promoteDeviceToDefault)(existingDevices, name);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
console.error(chalk_1.default.red(`\n${error.message}`));
|
|
260
|
+
const names = existingDevices.map((d) => d.name || d.host).join(', ');
|
|
261
|
+
console.log(chalk_1.default.dim(`Available devices: ${names}\n`));
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
const label = result.promoted.name || result.promoted.host;
|
|
265
|
+
if (result.alreadyDefault) {
|
|
266
|
+
console.log(chalk_1.default.dim(`\n"${label}" is already the default.\n`));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
config.ff1Devices = { devices: result.devices };
|
|
270
|
+
await fs_1.promises.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
|
|
271
|
+
console.log(chalk_1.default.green(`\nDefault device: ${label}`));
|
|
272
|
+
console.log(chalk_1.default.dim('Other commands now target this device when -d is omitted.\n'));
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
console.error(chalk_1.default.red('\nError:'), error.message);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isMissingConfigValue = isMissingConfigValue;
|
|
4
|
+
exports.readConfigFile = readConfigFile;
|
|
5
|
+
exports.resolveExistingConfigPath = resolveExistingConfigPath;
|
|
6
|
+
exports.ensureConfigFile = ensureConfigFile;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const config_1 = require("../../config");
|
|
9
|
+
// Treat any value containing the YOUR_/your_ placeholder pattern as missing,
|
|
10
|
+
// since createSampleConfig writes those literal placeholders into new files
|
|
11
|
+
// and we should not let the user proceed with an unfilled value.
|
|
12
|
+
const placeholderPattern = /YOUR_|your_/;
|
|
13
|
+
/**
|
|
14
|
+
* Whether a config value is unset or still holds a sample placeholder.
|
|
15
|
+
*/
|
|
16
|
+
function isMissingConfigValue(value) {
|
|
17
|
+
if (!value) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return placeholderPattern.test(value);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Read and parse a config.json file at the given path.
|
|
24
|
+
*/
|
|
25
|
+
async function readConfigFile(configPath) {
|
|
26
|
+
const file = await fs_1.promises.readFile(configPath, 'utf-8');
|
|
27
|
+
return JSON.parse(file);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Locate an existing config.json. Local (`./config.json`) wins over user
|
|
31
|
+
* (`~/.config/ff1/config.json`). Returns null when neither exists.
|
|
32
|
+
*/
|
|
33
|
+
async function resolveExistingConfigPath() {
|
|
34
|
+
const { localPath, userPath } = (0, config_1.getConfigPaths)();
|
|
35
|
+
try {
|
|
36
|
+
await fs_1.promises.access(localPath);
|
|
37
|
+
return localPath;
|
|
38
|
+
}
|
|
39
|
+
catch (_error) {
|
|
40
|
+
try {
|
|
41
|
+
await fs_1.promises.access(userPath);
|
|
42
|
+
return userPath;
|
|
43
|
+
}
|
|
44
|
+
catch (_innerError) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the existing config file or write a sample one to the user
|
|
51
|
+
* config path. The `created` flag tells callers whether to print the
|
|
52
|
+
* new-file confirmation.
|
|
53
|
+
*/
|
|
54
|
+
async function ensureConfigFile() {
|
|
55
|
+
const { userPath } = (0, config_1.getConfigPaths)();
|
|
56
|
+
const existingPath = await resolveExistingConfigPath();
|
|
57
|
+
if (existingPath) {
|
|
58
|
+
return { path: existingPath, created: false };
|
|
59
|
+
}
|
|
60
|
+
const createdPath = await (0, config_1.createSampleConfig)(userPath);
|
|
61
|
+
return { path: createdPath, created: true };
|
|
62
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.discoverAndSelectDevice = discoverAndSelectDevice;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ff1_discovery_1 = require("../../utilities/ff1-discovery");
|
|
9
|
+
const device_lookup_1 = require("../../utilities/device-lookup");
|
|
10
|
+
const device_normalize_1 = require("../../utilities/device-normalize");
|
|
11
|
+
/**
|
|
12
|
+
* Run mDNS discovery and prompt the user to pick a device, fall back to
|
|
13
|
+
* manual entry, or skip when an existing device is already configured.
|
|
14
|
+
*
|
|
15
|
+
* Shared between `ff1 setup` and `ff1 device add` so both flows present
|
|
16
|
+
* the same selection UX and matching rules.
|
|
17
|
+
*/
|
|
18
|
+
async function discoverAndSelectDevice(ask, existingDevices, options) {
|
|
19
|
+
const allowSkip = options?.allowSkip && existingDevices.length > 0;
|
|
20
|
+
const discoveryResult = await (0, ff1_discovery_1.discoverFF1Devices)();
|
|
21
|
+
const discoveredDevices = discoveryResult.devices;
|
|
22
|
+
if (discoveryResult.error && discoveredDevices.length === 0) {
|
|
23
|
+
const errorMessage = discoveryResult.error.endsWith('.')
|
|
24
|
+
? discoveryResult.error
|
|
25
|
+
: `${discoveryResult.error}.`;
|
|
26
|
+
console.log(chalk_1.default.dim(`mDNS discovery failed: ${errorMessage} Continuing with manual entry.`));
|
|
27
|
+
}
|
|
28
|
+
else if (discoveryResult.error) {
|
|
29
|
+
console.log(chalk_1.default.dim(`mDNS discovery warning: ${discoveryResult.error}`));
|
|
30
|
+
}
|
|
31
|
+
if (discoveredDevices.length > 0) {
|
|
32
|
+
console.log(chalk_1.default.green('\nFF1 devices on your network:'));
|
|
33
|
+
discoveredDevices.forEach((device, index) => {
|
|
34
|
+
const displayId = device.id || device.name || device.host;
|
|
35
|
+
const normalizedHost = (0, device_normalize_1.normalizeDeviceHost)(`${device.host}:${device.port}`);
|
|
36
|
+
const alreadyConfigured = !!(0, device_lookup_1.findExistingDeviceEntry)(existingDevices, normalizedHost, device.name || device.id || '', device.id, device.addresses);
|
|
37
|
+
const suffix = alreadyConfigured ? chalk_1.default.dim(' (already configured)') : '';
|
|
38
|
+
console.log(chalk_1.default.dim(` ${index + 1}) ${displayId}${suffix}`));
|
|
39
|
+
});
|
|
40
|
+
const skipHint = allowSkip ? ', press Enter to skip' : '';
|
|
41
|
+
const prompt = `Select device [1-${discoveredDevices.length}], enter ID/host${skipHint}, or type m for manual entry: `;
|
|
42
|
+
while (true) {
|
|
43
|
+
const selectionAnswer = (await ask(prompt)).trim();
|
|
44
|
+
if (!selectionAnswer) {
|
|
45
|
+
if (allowSkip) {
|
|
46
|
+
console.log(chalk_1.default.dim('Keeping existing devices.'));
|
|
47
|
+
return { hostValue: '', discoveredName: '', skipped: true };
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
const normalizedSelection = selectionAnswer.toLowerCase();
|
|
52
|
+
if (normalizedSelection === 'm') {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
const parsedIndex = Number.parseInt(selectionAnswer, 10);
|
|
56
|
+
if (!Number.isNaN(parsedIndex) &&
|
|
57
|
+
`${parsedIndex}` === selectionAnswer &&
|
|
58
|
+
parsedIndex >= 1 &&
|
|
59
|
+
parsedIndex <= discoveredDevices.length) {
|
|
60
|
+
const selected = discoveredDevices[parsedIndex - 1];
|
|
61
|
+
return {
|
|
62
|
+
hostValue: (0, device_normalize_1.normalizeDeviceHost)(`${selected.host}:${selected.port}`),
|
|
63
|
+
discoveredName: selected.name || selected.id || '',
|
|
64
|
+
discoveredId: selected.id,
|
|
65
|
+
discoveredAddresses: selected.addresses,
|
|
66
|
+
skipped: false,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const normalizedWithPrefix = normalizedSelection.startsWith('ff1-')
|
|
70
|
+
? normalizedSelection
|
|
71
|
+
: `ff1-${normalizedSelection}`;
|
|
72
|
+
// Also normalize the answer as a URL-form host so pasted URLs like
|
|
73
|
+
// "http://ff1-hh9jsnoc.local:1111" match the device's normalized host.
|
|
74
|
+
let normalizedSelectionAsHost = '';
|
|
75
|
+
try {
|
|
76
|
+
normalizedSelectionAsHost = (0, device_normalize_1.normalizeDeviceHost)(selectionAnswer).toLowerCase();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// not a valid URL — skip URL-form matching
|
|
80
|
+
}
|
|
81
|
+
const matched = discoveredDevices.find((device) => {
|
|
82
|
+
const deviceNormalizedHost = (0, device_normalize_1.normalizeDeviceHost)(`${device.host}:${device.port}`).toLowerCase();
|
|
83
|
+
const candidates = [device.id, device.name, device.host, `${device.host}:${device.port}`]
|
|
84
|
+
.filter((value) => Boolean(value))
|
|
85
|
+
.map((value) => value.toLowerCase());
|
|
86
|
+
return (candidates.includes(normalizedSelection) ||
|
|
87
|
+
candidates.includes(normalizedWithPrefix) ||
|
|
88
|
+
(normalizedSelectionAsHost !== '' && normalizedSelectionAsHost === deviceNormalizedHost));
|
|
89
|
+
});
|
|
90
|
+
if (matched) {
|
|
91
|
+
return {
|
|
92
|
+
hostValue: (0, device_normalize_1.normalizeDeviceHost)(`${matched.host}:${matched.port}`),
|
|
93
|
+
discoveredName: matched.name || matched.id || '',
|
|
94
|
+
discoveredId: matched.id,
|
|
95
|
+
discoveredAddresses: matched.addresses,
|
|
96
|
+
skipped: false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
console.log(chalk_1.default.red('Invalid selection. Enter a number, m, or a discovered device ID/host.'));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (!discoveryResult.error) {
|
|
103
|
+
console.log(chalk_1.default.dim('No FF1 devices found via mDNS. Continuing with manual entry.'));
|
|
104
|
+
}
|
|
105
|
+
// Manual entry fallback
|
|
106
|
+
const idAnswer = await ask('Device ID or host (e.g. ff1-ABCD1234): ');
|
|
107
|
+
if (!idAnswer) {
|
|
108
|
+
return { hostValue: '', discoveredName: '', skipped: false };
|
|
109
|
+
}
|
|
110
|
+
return { hostValue: (0, device_normalize_1.normalizeDeviceIdToHost)(idAnswer), discoveredName: '', skipped: false };
|
|
111
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.displayPlaylistSummary = displayPlaylistSummary;
|
|
40
|
+
exports.printPlaylistSourceLoadFailure = printPlaylistSourceLoadFailure;
|
|
41
|
+
exports.printPlaylistVerificationFailure = printPlaylistVerificationFailure;
|
|
42
|
+
exports.validatePlaylistSource = validatePlaylistSource;
|
|
43
|
+
exports.runValidateCommand = runValidateCommand;
|
|
44
|
+
exports.runVerifyCommand = runVerifyCommand;
|
|
45
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
46
|
+
const playlist_source_1 = require("../../utilities/playlist-source");
|
|
47
|
+
/**
|
|
48
|
+
* Print the standard "playlist saved" footer with the next-step hint.
|
|
49
|
+
*
|
|
50
|
+
* Used by `chat` and `build` after they write a playlist to disk, so the
|
|
51
|
+
* surface message stays consistent across both commands.
|
|
52
|
+
*/
|
|
53
|
+
function displayPlaylistSummary(playlist, outputPath) {
|
|
54
|
+
console.log(chalk_1.default.green('\nPlaylist saved'));
|
|
55
|
+
console.log(chalk_1.default.dim(` Output: ./${outputPath}`));
|
|
56
|
+
console.log(chalk_1.default.dim(' Next: play last | publish playlist'));
|
|
57
|
+
console.log();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Print a focused failure for playlist source loading problems with a
|
|
61
|
+
* URL-vs-file aware hint block.
|
|
62
|
+
*/
|
|
63
|
+
function printPlaylistSourceLoadFailure(source, error) {
|
|
64
|
+
const isUrl = (0, playlist_source_1.isPlaylistSourceUrl)(source);
|
|
65
|
+
if (isUrl) {
|
|
66
|
+
console.error(chalk_1.default.red('\nCould not load hosted playlist URL'));
|
|
67
|
+
console.error(chalk_1.default.red(` Source: ${source}`));
|
|
68
|
+
console.error(chalk_1.default.red(` Error: ${error.message}`));
|
|
69
|
+
console.log(chalk_1.default.yellow('\n Hint:'));
|
|
70
|
+
console.log(chalk_1.default.yellow(' • Check the URL is reachable'));
|
|
71
|
+
console.log(chalk_1.default.yellow(' • Confirm the response is JSON'));
|
|
72
|
+
console.log(chalk_1.default.yellow(' • Use a local file path if network access is unavailable'));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
console.error(chalk_1.default.red(`\nCould not load playlist file`));
|
|
76
|
+
console.error(chalk_1.default.red(` Source: ${source}`));
|
|
77
|
+
console.error(chalk_1.default.red(` Error: ${error.message}`));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Print playlist validation failure details consistently for commands that
|
|
81
|
+
* validate structure before sending (for example `play`).
|
|
82
|
+
*/
|
|
83
|
+
function printPlaylistVerificationFailure(verifyResult, source) {
|
|
84
|
+
console.error(chalk_1.default.red(`\nPlaylist validation failed:${source ? ` (${source})` : ''}`), verifyResult.error);
|
|
85
|
+
if (verifyResult.details && verifyResult.details.length > 0) {
|
|
86
|
+
console.log(chalk_1.default.yellow('\n Validation errors:'));
|
|
87
|
+
verifyResult.details.forEach((detail) => {
|
|
88
|
+
console.log(chalk_1.default.yellow(` • ${detail.path}: ${detail.message}`));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk_1.default.yellow('\n Use --skip-verify to play anyway (not recommended)\n'));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Load a playlist from a path or URL and run parse-only validation (`validatePlaylist`).
|
|
95
|
+
*
|
|
96
|
+
* The `validate` command uses this as the full check. The `verify` command uses it
|
|
97
|
+
* for the structure pass only, then runs `verifyPlaylist` for cryptographic checks.
|
|
98
|
+
*/
|
|
99
|
+
async function validatePlaylistSource(source) {
|
|
100
|
+
const loaded = await (0, playlist_source_1.loadPlaylistSource)(source);
|
|
101
|
+
const verifier = await Promise.resolve().then(() => __importStar(require('../../utilities/playlist-verifier')));
|
|
102
|
+
const { validatePlaylist } = verifier;
|
|
103
|
+
const verifyResult = await validatePlaylist(loaded.playlist);
|
|
104
|
+
return {
|
|
105
|
+
...verifyResult,
|
|
106
|
+
playlist: verifyResult.valid ? loaded.playlist : undefined,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Run the `validate` command flow. It checks DP-1 structure only and does
|
|
111
|
+
* not require signatures or public keys.
|
|
112
|
+
*/
|
|
113
|
+
async function runValidateCommand(source) {
|
|
114
|
+
try {
|
|
115
|
+
console.log(chalk_1.default.blue('\nValidate playlist\n'));
|
|
116
|
+
const verifier = await Promise.resolve().then(() => __importStar(require('../../utilities/playlist-verifier')));
|
|
117
|
+
const { printVerificationResult } = verifier;
|
|
118
|
+
const result = await validatePlaylistSource(source);
|
|
119
|
+
printVerificationResult(result, source, { failureKind: 'structure' });
|
|
120
|
+
if (!result.valid) {
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
printPlaylistSourceLoadFailure(source, error);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Run the `verify` command flow: same DP-1 structure checks as `validate`, then
|
|
131
|
+
* cryptographic verification via dp1-js. Unsigned playlists fail at the crypto
|
|
132
|
+
* step. dp1-js `verifyPlaylist` uses the optional second argument **only** to
|
|
133
|
+
* verify legacy flat `signature` strings; `signatures[]` envelopes do not rely
|
|
134
|
+
* on it. When `--public-key` is omitted, the CLI may still pass a key derived
|
|
135
|
+
* from `playlist.privateKey` / `PLAYLIST_PRIVATE_KEY`; dp1-js ignores it unless
|
|
136
|
+
* the playlist is on the legacy signature path.
|
|
137
|
+
*/
|
|
138
|
+
async function runVerifyCommand(source, publicKey) {
|
|
139
|
+
try {
|
|
140
|
+
console.log(chalk_1.default.blue('\nVerify playlist\n'));
|
|
141
|
+
const verifier = await Promise.resolve().then(() => __importStar(require('../../utilities/playlist-verifier')));
|
|
142
|
+
const { printVerificationResult } = verifier;
|
|
143
|
+
const result = await validatePlaylistSource(source);
|
|
144
|
+
if (!result.valid) {
|
|
145
|
+
printVerificationResult(result, source, { failureKind: 'structure' });
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
const signedResult = await verifier.verifyPlaylist(result.playlist, publicKey);
|
|
149
|
+
const finalResult = signedResult.valid
|
|
150
|
+
? { ...result, valid: true, error: undefined, details: undefined }
|
|
151
|
+
: { valid: false, error: signedResult.error, details: signedResult.details };
|
|
152
|
+
printVerificationResult(finalResult, source, { failureKind: 'signature' });
|
|
153
|
+
if (!signedResult.valid) {
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
printPlaylistSourceLoadFailure(source, error);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.createPrompt = createPrompt;
|
|
40
|
+
exports.promptYesNo = promptYesNo;
|
|
41
|
+
const readline = __importStar(require("readline"));
|
|
42
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
43
|
+
function createPrompt() {
|
|
44
|
+
const rl = readline.createInterface({
|
|
45
|
+
input: process.stdin,
|
|
46
|
+
output: process.stdout,
|
|
47
|
+
});
|
|
48
|
+
const ask = async (question) => new Promise((resolve) => {
|
|
49
|
+
rl.question(chalk_1.default.yellow(question), (answer) => {
|
|
50
|
+
resolve(answer.trim());
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
ask,
|
|
55
|
+
close: () => rl.close(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function promptYesNo(ask, question, defaultYes = true) {
|
|
59
|
+
const suffix = defaultYes ? 'Y/n' : 'y/N';
|
|
60
|
+
const answer = (await ask(`${question} [${suffix}] `)).trim().toLowerCase();
|
|
61
|
+
if (!answer) {
|
|
62
|
+
return defaultYes;
|
|
63
|
+
}
|
|
64
|
+
return answer === 'y' || answer === 'yes';
|
|
65
|
+
}
|