@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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/config.json.example +96 -0
  4. package/dist/index.js +54 -0
  5. package/dist/src/ai-orchestrator/index.js +1019 -0
  6. package/dist/src/ai-orchestrator/registry.js +96 -0
  7. package/dist/src/commands/build.js +69 -0
  8. package/dist/src/commands/chat.js +189 -0
  9. package/dist/src/commands/config.js +68 -0
  10. package/dist/src/commands/device.js +278 -0
  11. package/dist/src/commands/helpers/config-files.js +62 -0
  12. package/dist/src/commands/helpers/device-discovery.js +111 -0
  13. package/dist/src/commands/helpers/playlist-display.js +161 -0
  14. package/dist/src/commands/helpers/prompt.js +65 -0
  15. package/dist/src/commands/helpers/ssh-helpers.js +44 -0
  16. package/dist/src/commands/play.js +110 -0
  17. package/dist/src/commands/publish.js +115 -0
  18. package/dist/src/commands/setup.js +225 -0
  19. package/dist/src/commands/sign.js +41 -0
  20. package/dist/src/commands/ssh.js +108 -0
  21. package/dist/src/commands/status.js +126 -0
  22. package/dist/src/commands/validate.js +18 -0
  23. package/dist/src/config.js +441 -0
  24. package/dist/src/intent-parser/index.js +1382 -0
  25. package/dist/src/intent-parser/utils.js +108 -0
  26. package/dist/src/logger.js +82 -0
  27. package/dist/src/main.js +459 -0
  28. package/dist/src/types.js +5 -0
  29. package/dist/src/utilities/address-validator.js +242 -0
  30. package/dist/src/utilities/device-default.js +36 -0
  31. package/dist/src/utilities/device-lookup.js +107 -0
  32. package/dist/src/utilities/device-normalize.js +62 -0
  33. package/dist/src/utilities/device-upsert.js +91 -0
  34. package/dist/src/utilities/domain-resolver.js +291 -0
  35. package/dist/src/utilities/ed25519-key-derive.js +155 -0
  36. package/dist/src/utilities/feed-fetcher.js +471 -0
  37. package/dist/src/utilities/ff1-compatibility.js +269 -0
  38. package/dist/src/utilities/ff1-device.js +250 -0
  39. package/dist/src/utilities/ff1-discovery.js +330 -0
  40. package/dist/src/utilities/functions.js +308 -0
  41. package/dist/src/utilities/index.js +469 -0
  42. package/dist/src/utilities/nft-indexer.js +1024 -0
  43. package/dist/src/utilities/playlist-builder.js +523 -0
  44. package/dist/src/utilities/playlist-publisher.js +131 -0
  45. package/dist/src/utilities/playlist-send.js +260 -0
  46. package/dist/src/utilities/playlist-signer.js +204 -0
  47. package/dist/src/utilities/playlist-signing-role.js +41 -0
  48. package/dist/src/utilities/playlist-source.js +128 -0
  49. package/dist/src/utilities/playlist-verifier.js +274 -0
  50. package/dist/src/utilities/ssh-access.js +145 -0
  51. package/dist/src/utils.js +48 -0
  52. package/docs/CONFIGURATION.md +206 -0
  53. package/docs/EXAMPLES.md +390 -0
  54. package/docs/FUNCTION_CALLING.md +96 -0
  55. package/docs/PROJECT_SPEC.md +228 -0
  56. package/docs/README.md +348 -0
  57. package/docs/RELEASING.md +73 -0
  58. 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
+ }