@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,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseTtlSeconds = parseTtlSeconds;
4
+ exports.readPublicKeyFile = readPublicKeyFile;
5
+ const fs_1 = require("fs");
6
+ /**
7
+ * Parse a TTL duration string into seconds.
8
+ *
9
+ * Accepts a bare number ("900"), or a number with an `s`/`m`/`h` unit
10
+ * suffix ("15m", "2h"). Used by `ff1 ssh enable --ttl <ttl>`.
11
+ *
12
+ * @throws Error When the input does not match the supported format
13
+ */
14
+ function parseTtlSeconds(ttl) {
15
+ const trimmed = ttl.trim();
16
+ const match = trimmed.match(/^(\d+)([smh]?)$/i);
17
+ if (!match) {
18
+ throw new Error('TTL must be a number of seconds or a duration like 15m or 2h');
19
+ }
20
+ const value = parseInt(match[1], 10);
21
+ const unit = match[2].toLowerCase();
22
+ if (Number.isNaN(value)) {
23
+ throw new Error('TTL value is not a number');
24
+ }
25
+ if (unit === 'm') {
26
+ return value * 60;
27
+ }
28
+ if (unit === 'h') {
29
+ return value * 60 * 60;
30
+ }
31
+ return value;
32
+ }
33
+ /**
34
+ * Read an SSH public key from a file. The contents are trimmed and
35
+ * required to be non-empty so callers can fail fast on truncated files.
36
+ */
37
+ async function readPublicKeyFile(keyPath) {
38
+ const content = await fs_1.promises.readFile(keyPath, 'utf-8');
39
+ const trimmed = content.trim();
40
+ if (!trimmed) {
41
+ throw new Error('Public key file is empty');
42
+ }
43
+ return trimmed;
44
+ }
@@ -0,0 +1,110 @@
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.playCommand = void 0;
40
+ const commander_1 = require("commander");
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const config_1 = require("../config");
43
+ const playlist_source_1 = require("../utilities/playlist-source");
44
+ const playlist_display_1 = require("./helpers/playlist-display");
45
+ // ff1-device is still CommonJS; require keeps the interop simple.
46
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
47
+ const { sendPlaylistToDevice } = require('../utilities/ff1-device');
48
+ exports.playCommand = new commander_1.Command('play')
49
+ .description('Play a playlist or media URL on an FF1 device')
50
+ .argument('<source>', 'Playlist file, playlist URL, or media URL')
51
+ .option('-d, --device <name>', 'Device name (uses first device if not specified)')
52
+ .option('--skip-verify', 'Skip DP-1 structure validation (parse/schema) before playing; use only when you accept malformed envelopes')
53
+ .action(async (source, options) => {
54
+ try {
55
+ const config = (0, config_1.getConfig)();
56
+ const resolved = await (0, playlist_source_1.resolvePlaySource)(source, config.defaultDuration || 10);
57
+ const isPlaylistSource = resolved.kind === 'playlist';
58
+ const sourceLabel = isPlaylistSource
59
+ ? `${resolved.sourceType}: ${resolved.source}`
60
+ : resolved.source;
61
+ console.log(chalk_1.default.blue('\nPlay on FF1\n'));
62
+ if (!options.skipVerify) {
63
+ // Structure-only validation (same as `validate`, `verify`, send, and publish).
64
+ // Synthesized media-URL playlists are unsigned but parse as valid DP-1.
65
+ if (isPlaylistSource) {
66
+ console.log(chalk_1.default.cyan(`Validate playlist (${sourceLabel})`));
67
+ }
68
+ const verifier = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-verifier')));
69
+ const { validatePlaylist } = verifier;
70
+ const validateResult = await validatePlaylist(resolved.playlist);
71
+ if (!validateResult.valid) {
72
+ (0, playlist_display_1.printPlaylistVerificationFailure)(validateResult, isPlaylistSource ? `source: ${sourceLabel}` : undefined);
73
+ process.exit(1);
74
+ }
75
+ if (isPlaylistSource) {
76
+ console.log(chalk_1.default.green('✓ Validated\n'));
77
+ }
78
+ }
79
+ const result = await sendPlaylistToDevice({
80
+ playlist: resolved.playlist,
81
+ deviceName: options.device,
82
+ });
83
+ if (result.success) {
84
+ console.log(chalk_1.default.green('✓ Playing'));
85
+ if (result.deviceName) {
86
+ console.log(chalk_1.default.dim(` Device: ${result.deviceName}`));
87
+ }
88
+ if (result.device) {
89
+ console.log(chalk_1.default.dim(` Host: ${result.device}`));
90
+ }
91
+ console.log();
92
+ }
93
+ else {
94
+ console.error(chalk_1.default.red('\nPlay failed:'), result.error);
95
+ if (result.details) {
96
+ console.error(chalk_1.default.dim(` Details: ${result.details}`));
97
+ }
98
+ process.exit(1);
99
+ }
100
+ }
101
+ catch (error) {
102
+ if ((0, playlist_source_1.isPlaylistSourceUrl)(source)) {
103
+ (0, playlist_display_1.printPlaylistSourceLoadFailure)(source, error);
104
+ }
105
+ else {
106
+ console.error(chalk_1.default.red('\nError:'), error.message);
107
+ }
108
+ process.exit(1);
109
+ }
110
+ });
@@ -0,0 +1,115 @@
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.publishCommand = void 0;
40
+ const commander_1 = require("commander");
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const prompt_1 = require("./helpers/prompt");
43
+ exports.publishCommand = new commander_1.Command('publish')
44
+ .description('Publish a playlist to a feed server')
45
+ .argument('<file>', 'Path to the playlist file')
46
+ .option('-s, --server <index>', 'Feed server index (use this if multiple servers configured)')
47
+ .action(async (file, options) => {
48
+ try {
49
+ console.log(chalk_1.default.blue('\nPublish playlist\n'));
50
+ const { getFeedConfig } = await Promise.resolve().then(() => __importStar(require('../config')));
51
+ const { publishPlaylist } = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-publisher')));
52
+ const feedConfig = getFeedConfig();
53
+ if (!feedConfig.baseURLs || feedConfig.baseURLs.length === 0) {
54
+ console.error(chalk_1.default.red('\nNo feed servers configured'));
55
+ console.log(chalk_1.default.yellow(' Add feed server URLs to config.json: feed.baseURLs\n'));
56
+ process.exit(1);
57
+ }
58
+ let serverUrl = feedConfig.baseURLs[0];
59
+ let serverApiKey = feedConfig.apiKey;
60
+ if (feedConfig.baseURLs.length > 1) {
61
+ if (!options.server) {
62
+ console.log(chalk_1.default.yellow('Multiple feed servers found. Select one:'));
63
+ console.log();
64
+ feedConfig.baseURLs.forEach((url, index) => {
65
+ console.log(chalk_1.default.cyan(` ${index}: ${url}`));
66
+ });
67
+ console.log();
68
+ const prompt = (0, prompt_1.createPrompt)();
69
+ const selection = await prompt.ask('Select server (0-based index): ');
70
+ prompt.close();
71
+ console.log();
72
+ options.server = selection;
73
+ }
74
+ const serverIndex = parseInt(options.server || '0', 10);
75
+ if (isNaN(serverIndex) || serverIndex < 0 || serverIndex >= feedConfig.baseURLs.length) {
76
+ console.error(chalk_1.default.red('\nInvalid server index'));
77
+ process.exit(1);
78
+ }
79
+ serverUrl = feedConfig.baseURLs[serverIndex];
80
+ if (feedConfig.servers && feedConfig.servers[serverIndex]) {
81
+ serverApiKey = feedConfig.servers[serverIndex].apiKey;
82
+ }
83
+ }
84
+ else if (feedConfig.servers && feedConfig.servers[0]) {
85
+ serverApiKey = feedConfig.servers[0].apiKey;
86
+ }
87
+ const result = await publishPlaylist(file, serverUrl, serverApiKey);
88
+ if (result.success) {
89
+ console.log(chalk_1.default.green('Published'));
90
+ if (result.playlistId) {
91
+ console.log(chalk_1.default.dim(` Playlist ID: ${result.playlistId}`));
92
+ }
93
+ console.log(chalk_1.default.dim(` Server: ${result.feedServer}`));
94
+ if (result.message) {
95
+ console.log(chalk_1.default.dim(` Status: ${result.message}`));
96
+ }
97
+ console.log();
98
+ }
99
+ else {
100
+ console.error(chalk_1.default.red('\nPublish failed'));
101
+ if (result.error) {
102
+ console.error(chalk_1.default.red(` ${result.error}`));
103
+ }
104
+ if (result.message) {
105
+ console.log(chalk_1.default.yellow(`\n${result.message}`));
106
+ }
107
+ console.log();
108
+ process.exit(1);
109
+ }
110
+ }
111
+ catch (error) {
112
+ console.error(chalk_1.default.red('\nError:'), error.message);
113
+ process.exit(1);
114
+ }
115
+ });
@@ -0,0 +1,225 @@
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.setupCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ const fs_1 = require("fs");
11
+ const device_lookup_1 = require("../utilities/device-lookup");
12
+ const device_upsert_1 = require("../utilities/device-upsert");
13
+ const config_1 = require("../config");
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 playlist_signing_role_1 = require("../utilities/playlist-signing-role");
18
+ exports.setupCommand = new commander_1.Command('setup')
19
+ .description('Guided setup for config, signing key, and device')
20
+ .action(async () => {
21
+ let prompt = null;
22
+ try {
23
+ const { path: configPath, created } = await (0, config_files_1.ensureConfigFile)();
24
+ if (created) {
25
+ console.log(chalk_1.default.green(`Created ${configPath}`));
26
+ }
27
+ const config = await (0, config_files_1.readConfigFile)(configPath);
28
+ const mergedDefaults = (0, config_1.getConfig)();
29
+ // Show every known provider in the menu (in-code defaults first, then any
30
+ // custom providers the user has added to the file) so adding a new provider
31
+ // to defaults shows up for existing users without rerunning createSampleConfig.
32
+ const modelNames = Array.from(new Set([...(0, config_1.listAvailableModels)(), ...Object.keys(config.models || {})]));
33
+ if (modelNames.length === 0) {
34
+ console.error(chalk_1.default.red('No models found in config.json'));
35
+ process.exit(1);
36
+ }
37
+ console.log(chalk_1.default.blue('\nFF1 Setup\n'));
38
+ prompt = (0, prompt_1.createPrompt)();
39
+ const ask = prompt.ask;
40
+ const currentModel = config.defaultModel && modelNames.includes(config.defaultModel)
41
+ ? config.defaultModel
42
+ : modelNames[0];
43
+ let selectedModel = currentModel;
44
+ while (true) {
45
+ const modelAnswer = await ask(`Default model (${modelNames.join(', ')}) [${currentModel}]: `);
46
+ if (!modelAnswer) {
47
+ selectedModel = currentModel;
48
+ break;
49
+ }
50
+ if (modelNames.includes(modelAnswer)) {
51
+ selectedModel = modelAnswer;
52
+ break;
53
+ }
54
+ console.log(chalk_1.default.red(`Unknown model: ${modelAnswer}`));
55
+ }
56
+ config.defaultModel = selectedModel;
57
+ if (!config.models) {
58
+ config.models = {};
59
+ }
60
+ // Seed missing entries from in-code defaults so baseURL/model are populated,
61
+ // but blank the apiKey so the user is prompted instead of silently inheriting
62
+ // an env-derived value.
63
+ const providerDefaults = mergedDefaults.models[selectedModel];
64
+ const selectedModelConfig = config.models[selectedModel] ||
65
+ (providerDefaults
66
+ ? { ...providerDefaults, apiKey: '' }
67
+ : {
68
+ apiKey: '',
69
+ baseURL: '',
70
+ model: '',
71
+ timeout: 0,
72
+ maxRetries: 0,
73
+ temperature: 0,
74
+ maxTokens: 0,
75
+ supportsFunctionCalling: true,
76
+ });
77
+ const hasApiKeyForModel = !(0, config_files_1.isMissingConfigValue)(selectedModelConfig.apiKey);
78
+ const keyHelpUrls = {
79
+ grok: 'https://console.x.ai/',
80
+ gpt: 'https://platform.openai.com/api-keys',
81
+ gemini: 'https://aistudio.google.com/app/apikey',
82
+ claude: 'https://console.anthropic.com/settings/keys',
83
+ };
84
+ if (!hasApiKeyForModel) {
85
+ const helpUrl = keyHelpUrls[selectedModel];
86
+ if (helpUrl) {
87
+ console.log(chalk_1.default.dim(helpUrl));
88
+ }
89
+ }
90
+ const apiKeyPrompt = hasApiKeyForModel
91
+ ? `API key for ${selectedModel} (leave blank to keep current): `
92
+ : `API key for ${selectedModel} (optional, only needed for chat): `;
93
+ const apiKeyAnswer = await ask(apiKeyPrompt);
94
+ if (apiKeyAnswer) {
95
+ selectedModelConfig.apiKey = apiKeyAnswer;
96
+ }
97
+ config.models[selectedModel] = selectedModelConfig;
98
+ const currentKey = config.playlist?.privateKey || '';
99
+ const currentRole = config.playlist?.role || '';
100
+ let signingKey = currentKey;
101
+ let signingRole = currentRole;
102
+ if ((0, config_files_1.isMissingConfigValue)(currentKey)) {
103
+ const keyPair = crypto_1.default.generateKeyPairSync('ed25519');
104
+ signingKey = keyPair.privateKey.export({ format: 'der', type: 'pkcs8' }).toString('base64');
105
+ }
106
+ else {
107
+ const keepKey = await (0, prompt_1.promptYesNo)(ask, 'Keep existing signing key?', true);
108
+ if (!keepKey) {
109
+ const keyAnswer = await ask('Paste signing key (base64 or hex), or leave blank to regenerate: ');
110
+ if (keyAnswer) {
111
+ signingKey = keyAnswer;
112
+ }
113
+ else {
114
+ const keyPair = crypto_1.default.generateKeyPairSync('ed25519');
115
+ signingKey = keyPair.privateKey
116
+ .export({ format: 'der', type: 'pkcs8' })
117
+ .toString('base64');
118
+ }
119
+ }
120
+ }
121
+ if (signingKey) {
122
+ const roleHint = playlist_signing_role_1.DP1_PLAYLIST_SIGNING_ROLES.join(', ');
123
+ while (true) {
124
+ const roleAnswer = await ask(`Signing role (${roleHint}) [${currentRole || 'agent'}]: `);
125
+ try {
126
+ signingRole = (0, playlist_signing_role_1.resolveDp1PlaylistSigningRole)(roleAnswer, signingRole || 'agent');
127
+ break;
128
+ }
129
+ catch (error) {
130
+ console.log(chalk_1.default.red(error.message));
131
+ if (!roleAnswer.trim()) {
132
+ console.log(chalk_1.default.dim('Enter one of the supported roles above, or fix the stored value.'));
133
+ }
134
+ }
135
+ }
136
+ config.playlist = {
137
+ ...(config.playlist || {}),
138
+ privateKey: signingKey,
139
+ role: signingRole,
140
+ };
141
+ }
142
+ const existingDevices = config.ff1Devices?.devices || [];
143
+ if (existingDevices.length > 0) {
144
+ console.log(chalk_1.default.dim(`\nConfigured devices: ${existingDevices.map((d) => d.name || d.host).join(', ')}`));
145
+ }
146
+ const selection = await (0, device_discovery_1.discoverAndSelectDevice)(ask, existingDevices, { allowSkip: true });
147
+ if (selection.hostValue) {
148
+ // Prefer the already-stored label so re-running setup (or re-adding a device
149
+ // that returned on a new IP) doesn't clobber the friendly name.
150
+ const existingEntry = (0, device_lookup_1.findExistingDeviceEntry)(existingDevices, selection.hostValue, selection.discoveredName, selection.discoveredId, selection.discoveredAddresses);
151
+ const existingIndex = existingEntry
152
+ ? existingDevices.findIndex((d) => d === existingEntry)
153
+ : -1;
154
+ const existingName = existingEntry?.name || '';
155
+ const defaultName = existingName || selection.discoveredName || 'art-computer';
156
+ const namePrompt = defaultName !== 'art-computer'
157
+ ? `Device name (kitchen, office, etc.) [${defaultName}]: `
158
+ : 'Device name (kitchen, office, etc.): ';
159
+ const nameAnswer = await ask(namePrompt);
160
+ let deviceName = nameAnswer || defaultName || 'art-computer';
161
+ // Same name-collision guard as `device add`: reject names that would
162
+ // clobber a different device entry. Only fires when existingIndex !== -1
163
+ // (we know the row); when existingIndex === -1, a same-name entry is the
164
+ // case-3 migration path.
165
+ const setupNameConflict = existingIndex !== -1
166
+ ? existingDevices.find((d, i) => d.name === deviceName && i !== existingIndex)
167
+ : undefined;
168
+ if (setupNameConflict) {
169
+ console.log(chalk_1.default.yellow(`"${deviceName}" is already used by another device. Please choose a different name.`));
170
+ const retryAnswer = await ask('Device name: ');
171
+ deviceName = retryAnswer || 'art-computer';
172
+ const retryConflict = existingIndex !== -1
173
+ ? existingDevices.find((d, i) => d.name === deviceName && i !== existingIndex)
174
+ : undefined;
175
+ if (retryConflict) {
176
+ console.log(chalk_1.default.yellow(`"${deviceName}" is also taken. Skipping device.`));
177
+ config.ff1Devices = { devices: existingDevices };
178
+ await fs_1.promises.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
179
+ return;
180
+ }
181
+ }
182
+ const result = (0, device_upsert_1.upsertDevice)(existingDevices, {
183
+ name: deviceName,
184
+ host: selection.hostValue,
185
+ id: selection.discoveredId,
186
+ addresses: selection.discoveredAddresses,
187
+ }, existingIndex !== -1 ? existingIndex : undefined);
188
+ console.log(chalk_1.default.dim(`${result.updated ? 'Updated' : 'Added'} device: ${deviceName}`));
189
+ config.ff1Devices = { devices: result.devices };
190
+ }
191
+ else if (existingDevices.length > 0) {
192
+ config.ff1Devices = { devices: existingDevices };
193
+ }
194
+ await fs_1.promises.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
195
+ prompt.close();
196
+ prompt = null;
197
+ console.log(chalk_1.default.green('\nSetup complete'));
198
+ console.log(chalk_1.default.dim(` Config: ${configPath}`));
199
+ const hasApiKey = !(0, config_files_1.isMissingConfigValue)(config.models[selectedModel]?.apiKey);
200
+ const hasSigningKey = !(0, config_files_1.isMissingConfigValue)(config.playlist?.privateKey || '');
201
+ const hasDevice = Boolean(config.ff1Devices?.devices?.[0]?.host);
202
+ if (!hasSigningKey || !hasDevice) {
203
+ console.log(chalk_1.default.yellow('\nNext steps:'));
204
+ if (!hasSigningKey) {
205
+ console.log(chalk_1.default.yellow(' • Add a playlist signing key'));
206
+ }
207
+ if (!hasDevice) {
208
+ console.log(chalk_1.default.yellow(' • Add an FF1 device host'));
209
+ }
210
+ }
211
+ if (!hasApiKey) {
212
+ console.log(chalk_1.default.dim(`\nTo use ff-cli chat, add an API key for ${selectedModel}`));
213
+ }
214
+ console.log(chalk_1.default.dim('\nRun: ff-cli play'));
215
+ }
216
+ catch (error) {
217
+ console.error(chalk_1.default.red('\nSetup failed:'), error.message);
218
+ process.exit(1);
219
+ }
220
+ finally {
221
+ if (prompt) {
222
+ prompt.close();
223
+ }
224
+ }
225
+ });
@@ -0,0 +1,41 @@
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.signCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ // playlist-signer is still CommonJS; require keeps the interop simple.
10
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
11
+ const { signPlaylistFile } = require('../utilities/playlist-signer');
12
+ exports.signCommand = new commander_1.Command('sign')
13
+ .description('Sign a DP1 playlist file with a DP-1 signature envelope')
14
+ .argument('<file>', 'Path to the playlist file to sign')
15
+ .option('-k, --key <privateKey>', 'Ed25519 private key in base64 format (overrides config)')
16
+ .option('-r, --role <role>', 'DP-1 signing role (overrides config)')
17
+ .option('-o, --output <file>', 'Output file path (defaults to overwriting input file)')
18
+ .action(async (file, options) => {
19
+ try {
20
+ console.log(chalk_1.default.blue('\nSign playlist\n'));
21
+ const result = await signPlaylistFile(file, options.key, options.output, options.role);
22
+ if (result.success) {
23
+ console.log(chalk_1.default.green('\nPlaylist signed'));
24
+ if (Array.isArray(result.playlist?.signatures)) {
25
+ console.log(chalk_1.default.dim(` Signatures: ${result.playlist.signatures.length}`));
26
+ }
27
+ else if (result.playlist?.signature) {
28
+ console.log(chalk_1.default.dim(` Signature: ${result.playlist.signature.substring(0, 30)}...`));
29
+ }
30
+ console.log();
31
+ }
32
+ else {
33
+ console.error(chalk_1.default.red('\nSign failed:'), result.error);
34
+ process.exit(1);
35
+ }
36
+ }
37
+ catch (error) {
38
+ console.error(chalk_1.default.red('\nError:'), error.message);
39
+ process.exit(1);
40
+ }
41
+ });
@@ -0,0 +1,108 @@
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.sshCommand = void 0;
40
+ const commander_1 = require("commander");
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const ssh_helpers_1 = require("./helpers/ssh-helpers");
43
+ // `ssh` is a single command with an action argument rather than a
44
+ // commander subcommand group. Kept this way to preserve the existing
45
+ // CLI surface (`ff1 ssh enable|disable`) used in scripts.
46
+ exports.sshCommand = new commander_1.Command('ssh')
47
+ .description('Enable or disable SSH access on an FF1 device')
48
+ .argument('<action>', 'Action: enable or disable')
49
+ .option('-d, --device <name>', 'Device name (uses first device if not specified)')
50
+ .option('--pubkey <path>', 'SSH public key file (required for enable)')
51
+ .option('--ttl <duration>', 'Auto-disable after duration (e.g. 30m, 2h, 900s)')
52
+ .action(async (action, options) => {
53
+ try {
54
+ const normalizedAction = action.trim().toLowerCase();
55
+ if (normalizedAction !== 'enable' && normalizedAction !== 'disable') {
56
+ console.error(chalk_1.default.red('\nUnknown action:'), action);
57
+ console.log(chalk_1.default.yellow('Available actions: enable, disable\n'));
58
+ process.exit(1);
59
+ }
60
+ const isEnable = normalizedAction === 'enable';
61
+ let publicKey;
62
+ if (isEnable) {
63
+ if (!options.pubkey) {
64
+ console.error(chalk_1.default.red('\nPublic key is required to enable SSH'));
65
+ console.log(chalk_1.default.yellow('Use: ff1 ssh enable --pubkey ~/.ssh/id_ed25519.pub\n'));
66
+ process.exit(1);
67
+ }
68
+ publicKey = await (0, ssh_helpers_1.readPublicKeyFile)(options.pubkey);
69
+ }
70
+ let ttlSeconds;
71
+ if (options.ttl) {
72
+ ttlSeconds = (0, ssh_helpers_1.parseTtlSeconds)(options.ttl);
73
+ }
74
+ const { sendSshAccessCommand } = await Promise.resolve().then(() => __importStar(require('../utilities/ssh-access')));
75
+ const result = await sendSshAccessCommand({
76
+ enabled: isEnable,
77
+ deviceName: options.device,
78
+ publicKey,
79
+ ttlSeconds,
80
+ });
81
+ if (result.success) {
82
+ console.log(chalk_1.default.green(`SSH ${isEnable ? 'enabled' : 'disabled'}`));
83
+ if (result.deviceName) {
84
+ console.log(chalk_1.default.dim(` Device: ${result.deviceName}`));
85
+ }
86
+ if (result.device) {
87
+ console.log(chalk_1.default.dim(` Host: ${result.device}`));
88
+ }
89
+ if (result.response && typeof result.response === 'object') {
90
+ const expiresAt = result.response.expiresAt;
91
+ if (expiresAt) {
92
+ console.log(chalk_1.default.dim(` Expires: ${expiresAt}`));
93
+ }
94
+ }
95
+ console.log();
96
+ return;
97
+ }
98
+ console.error(chalk_1.default.red('\nSSH request failed:'), result.error);
99
+ if (result.details) {
100
+ console.error(chalk_1.default.dim(` Details: ${result.details}`));
101
+ }
102
+ process.exit(1);
103
+ }
104
+ catch (error) {
105
+ console.error(chalk_1.default.red('\nError:'), error.message);
106
+ process.exit(1);
107
+ }
108
+ });