@feralfile/cli 3.0.0 → 4.0.0

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 (45) hide show
  1. package/README.md +20 -17
  2. package/config.json.example +1 -1
  3. package/dist/index.js +10 -2
  4. package/dist/package.json +5 -2
  5. package/dist/src/commands/build.js +1 -1
  6. package/dist/src/commands/device.js +84 -39
  7. package/dist/src/commands/enrich.js +2 -2
  8. package/dist/src/commands/find.js +71 -37
  9. package/dist/src/commands/helpers/device-control.js +937 -0
  10. package/dist/src/commands/helpers/hidden-alias-help.js +37 -0
  11. package/dist/src/commands/helpers/playlist-display.js +2 -15
  12. package/dist/src/commands/play.js +39 -26
  13. package/dist/src/commands/report.js +251 -0
  14. package/dist/src/commands/setup.js +1 -1
  15. package/dist/src/config.js +162 -6
  16. package/dist/src/utilities/control/codec.js +578 -0
  17. package/dist/src/utilities/control/control-errors.js +157 -0
  18. package/dist/src/utilities/control/controller-store.js +964 -0
  19. package/dist/src/utilities/control/jwk.js +141 -0
  20. package/dist/src/utilities/control/pairing-channel.js +52 -0
  21. package/dist/src/utilities/control/pairing.js +416 -0
  22. package/dist/src/utilities/control/registration.js +391 -0
  23. package/dist/src/utilities/control/relay-pairing-channel.js +191 -0
  24. package/dist/src/utilities/control/secure-transport.js +850 -0
  25. package/dist/src/utilities/control/strict-json.js +376 -0
  26. package/dist/src/utilities/device-network.js +129 -0
  27. package/dist/src/utilities/device-upsert.js +1 -1
  28. package/dist/src/utilities/ff1-compatibility.js +87 -11
  29. package/dist/src/utilities/ff1-device.js +161 -78
  30. package/dist/src/utilities/ff1-relayer.js +34 -14
  31. package/dist/src/utilities/handoff-client.js +23 -9
  32. package/dist/src/utilities/neort-marketplace.js +3 -3
  33. package/dist/src/utilities/playlist-builder.js +3 -3
  34. package/dist/src/utilities/playlist-cast.js +6 -6
  35. package/dist/src/utilities/playlist-source.js +7 -7
  36. package/dist/src/utilities/raster-client.js +1 -1
  37. package/dist/src/utilities/relay-client.js +774 -0
  38. package/dist/src/utilities/ssh-access.js +192 -58
  39. package/dist/src/utilities/support-report.js +511 -0
  40. package/dist/src/utilities/temporary-channel.js +139 -0
  41. package/docs/CONFIGURATION.md +270 -28
  42. package/docs/EXAMPLES.md +31 -31
  43. package/docs/PROJECT_SPEC.md +32 -19
  44. package/docs/README.md +113 -36
  45. package/package.json +5 -2
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hiddenAliasHelp = void 0;
4
+ const commander_1 = require("commander");
5
+ /**
6
+ * dropAlias removes `cmd`'s first alias from a help term commander built, turning `display|play` back
7
+ * into `display`.
8
+ *
9
+ * Commander adds the alias as `name|alias` in exactly these two terms and nowhere else, so replacing
10
+ * that one token is enough; rebuilding the terms from scratch would copy commander internals that
11
+ * are not exported (humanReadableArgName).
12
+ *
13
+ * @param {string} term - The term commander built
14
+ * @param {Command} cmd - The command it describes
15
+ * @returns {string} The term without the alias
16
+ */
17
+ function dropAlias(term, cmd) {
18
+ const alias = cmd.alias();
19
+ return alias ? term.replace(`${cmd.name()}|${alias}`, cmd.name()) : term;
20
+ }
21
+ /**
22
+ * hiddenAliasHelp is a commander help configuration that names a command by its name only, never
23
+ * by its alias.
24
+ *
25
+ * An alias in ff-cli is a retired command name kept so existing scripts and agents keep working:
26
+ * `play` became `display` (B&F weekly 2026-11-03; Canon reference/naming-brand.md §3.4). Commander
27
+ * would print it in the command list and the usage line (`display|play`), and help is where people
28
+ * and agents learn the CLI's words, so printing it would teach the retired verb again. The alias
29
+ * still parses and `ff-cli help play` still works; only the printed help drops it.
30
+ *
31
+ * Apply it to the program (the command list uses subcommandTerm) and to every aliased command (its
32
+ * own usage line uses commandUsage): `addCommand` does not copy help configuration to subcommands.
33
+ */
34
+ exports.hiddenAliasHelp = {
35
+ subcommandTerm: (cmd) => dropAlias(new commander_1.Help().subcommandTerm(cmd), cmd),
36
+ commandUsage: (cmd) => dropAlias(new commander_1.Help().commandUsage(cmd), cmd),
37
+ };
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.displayPlaylistSummary = displayPlaylistSummary;
7
6
  exports.printPlaylistSourceLoadFailure = printPlaylistSourceLoadFailure;
8
7
  exports.printPlaylistVerificationFailure = printPlaylistVerificationFailure;
9
8
  exports.validatePlaylistSource = validatePlaylistSource;
@@ -11,18 +10,6 @@ exports.runValidateCommand = runValidateCommand;
11
10
  exports.runVerifyCommand = runVerifyCommand;
12
11
  const chalk_1 = __importDefault(require("chalk"));
13
12
  const playlist_source_1 = require("../../utilities/playlist-source");
14
- /**
15
- * Print the standard "playlist saved" footer with the next-step hint.
16
- *
17
- * Used by `chat` and `build` after they write a playlist to disk, so the
18
- * surface message stays consistent across both commands.
19
- */
20
- function displayPlaylistSummary(playlist, outputPath) {
21
- console.log(chalk_1.default.green('\nPlaylist saved'));
22
- console.log(chalk_1.default.dim(` Output: ./${outputPath}`));
23
- console.log(chalk_1.default.dim(' Next: play last | publish playlist'));
24
- console.log();
25
- }
26
13
  /**
27
14
  * Print a focused failure for playlist source loading problems with a
28
15
  * URL-vs-file aware hint block.
@@ -45,7 +32,7 @@ function printPlaylistSourceLoadFailure(source, error) {
45
32
  }
46
33
  /**
47
34
  * Print playlist validation failure details consistently for commands that
48
- * validate structure before sending (for example `play`).
35
+ * validate structure before sending (for example `display`).
49
36
  */
50
37
  function printPlaylistVerificationFailure(verifyResult, source) {
51
38
  console.error(chalk_1.default.red(`\nPlaylist validation failed:${source ? ` (${source})` : ''}`), verifyResult.error);
@@ -55,7 +42,7 @@ function printPlaylistVerificationFailure(verifyResult, source) {
55
42
  console.log(chalk_1.default.yellow(` • ${detail.path}: ${detail.message}`));
56
43
  });
57
44
  }
58
- console.log(chalk_1.default.yellow('\n Use --skip-verify to play anyway (not recommended)\n'));
45
+ console.log(chalk_1.default.yellow('\n Use --skip-verify to display anyway (not recommended)\n'));
59
46
  }
60
47
  /**
61
48
  * Load a playlist from a path or URL and run parse-only validation (`validatePlaylist`).
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.playCommand = void 0;
6
+ exports.displayCommand = void 0;
7
7
  exports.describeDiscoveryInput = describeDiscoveryInput;
8
8
  exports.probeDiscoveryUrlAsPlaylist = probeDiscoveryUrlAsPlaylist;
9
9
  const commander_1 = require("commander");
@@ -13,20 +13,21 @@ const config_1 = require("../config");
13
13
  const playlist_source_1 = require("../utilities/playlist-source");
14
14
  const playlist_cast_1 = require("../utilities/playlist-cast");
15
15
  const playlist_display_1 = require("./helpers/playlist-display");
16
+ const hidden_alias_help_1 = require("./helpers/hidden-alias-help");
16
17
  /**
17
- * Human label for a discovery input that `play` cannot cast directly.
18
+ * Human label for a discovery input that `display` cannot cast directly.
18
19
  *
19
- * `play` casts an exact, already-playable source (a playlist file, a hosted
20
- * playlist URL, or a direct media/web URL). A marketplace URL, on-chain
20
+ * `display` casts an exact source it can send as it is (a playlist file, a
21
+ * hosted playlist URL, or a direct media/web URL). A marketplace URL, on-chain
21
22
  * coordinates, or a wallet address must be *resolved* through the indexer
22
- * first — that is `find`'s job. Returns a label for those inputs so `play`
23
+ * first — that is `find`'s job. Returns a label for those inputs so `display`
23
24
  * can redirect the user instead of silently wrapping, say, an Art Blocks
24
- * collection *page* as a web item. Returns null for anything `play` should
25
+ * collection *page* as a web item. Returns null for anything `display` should
25
26
  * handle itself (plain URLs and file paths parse to null here).
26
27
  *
27
28
  * A recognized-but-`unsupported` result (e.g. a legacy Art Blocks
28
29
  * `/projects/{id}` URL, or an unsupported-chain OpenSea link) is also a
29
- * discovery input — it's a marketplace URL, not a playable file — so it is
30
+ * discovery input — it's a marketplace URL, not a file it can send — so it is
30
31
  * redirected to `find` too, which surfaces the specific reason.
31
32
  */
32
33
  function describeDiscoveryInput(source) {
@@ -72,11 +73,11 @@ function describeDiscoveryInput(source) {
72
73
  * which wrongly redirected DP-1 feed playlist URLs
73
74
  * (https://feed.feralfile.com/api/v1/playlists/{slug}, exactly the URLs the
74
75
  * feed hands out) to `find`, which has no feed route either; the documented
75
- * "hosted playlist URL plays directly" contract was unreachable for them
76
+ * "hosted playlist URL displays directly" contract was unreachable for them
76
77
  * (found 2026-08-02 casting a published playlist to an FF1).
77
78
  *
78
79
  * Probing the document keeps both behaviors without a host allowlist: a URL
79
- * that serves DP-1 JSON (`dpVersion` marker) plays directly; a marketplace
80
+ * that serves DP-1 JSON (`dpVersion` marker) displays directly; a marketplace
80
81
  * *page* fails the probe (non-JSON, or JSON with no marker) and still
81
82
  * redirects to `find` instead of being wrapped as a web item. The probe
82
83
  * costs one fetch before a redirect, only for discovery inputs that are
@@ -84,7 +85,7 @@ function describeDiscoveryInput(source) {
84
85
  *
85
86
  * A URL that does not exist (404/410, no such host, a broken `.json` link)
86
87
  * is refused here rather than redirected: the redirect also suggests
87
- * `play --web`, which would cast the error page.
88
+ * `display --web`, which would cast the error page.
88
89
  *
89
90
  * @throws {Error} When the URL does not exist (refusalForLoadFailure)
90
91
  */
@@ -114,14 +115,14 @@ async function probeDiscoveryUrlAsPlaylist(source) {
114
115
  }
115
116
  }
116
117
  /**
117
- * resolveWebPageSource wraps `source` as a web page for `play --web`, without
118
+ * resolveWebPageSource wraps `source` as a web page for `display --web`, without
118
119
  * the discovery redirect or the playlist probe.
119
120
  *
120
121
  * `--web` exists because the discovery parser claims whole hosts: any page on
121
122
  * feralfile.com, opensea.io, objkt.com and the like is redirected to `find`,
122
123
  * and `find` rejects every page that is not an artwork it can resolve (a
123
124
  * homepage, an exhibition overview, a profile). Before `--web` there was no
124
- * way to put such a page on a wall; the two commands pointed at each other.
125
+ * way to put such a page on display; the two commands pointed at each other.
125
126
  *
126
127
  * @param {string} source - The URL the caller passed
127
128
  * @returns {Promise<PlaySource>} A `media` source with a `text/html` item
@@ -135,12 +136,24 @@ async function resolveWebPageSource(source) {
135
136
  const note = await (0, playlist_source_1.checkWebPageUrl)(url);
136
137
  return (0, playlist_source_1.buildMediaPlaySource)(url, { asWebPage: true, note });
137
138
  }
138
- exports.playCommand = new commander_1.Command('play')
139
- .description('Play a playlist file, playlist URL, media URL, or web page on an FF1 device')
139
+ /**
140
+ * displayCommand puts a playlist, media URL, or web page on an FF1.
141
+ *
142
+ * The verb for art is display, not play (B&F weekly 2026-11-03; Canon
143
+ * reference/naming-brand.md §3.4): people play games and video, they display
144
+ * art. `play` was this command's name, so it stays as an alias that existing
145
+ * scripts and agents keep calling; hiddenAliasHelp keeps it out of every help
146
+ * screen so help teaches only `display`. Do not drop the alias without a
147
+ * decision to break those callers.
148
+ */
149
+ exports.displayCommand = new commander_1.Command('display')
150
+ .alias('play')
151
+ .configureHelp(hidden_alias_help_1.hiddenAliasHelp)
152
+ .description('Display a playlist file, playlist URL, media URL, or web page on an FF1 device')
140
153
  .argument('<source>', 'Playlist file, playlist URL, or any media or web page URL')
141
154
  .option('-d, --device <name>', 'Device name (uses first device if not specified)')
142
- .option('--web', 'Play the URL as a web page, even on a marketplace site where play would otherwise send you to find')
143
- .option('--skip-verify', 'Skip signature verification and auto-sign gating before playing; use only when you accept malformed envelopes')
155
+ .option('--web', 'Display the URL as a web page, even on a marketplace site where display would otherwise send you to find')
156
+ .option('--skip-verify', 'Skip signature verification and auto-sign gating before displaying; use only when you accept malformed envelopes')
144
157
  .action(async (source, options) => {
145
158
  try {
146
159
  const resolved = options.web
@@ -159,14 +172,14 @@ exports.playCommand = new commander_1.Command('play')
159
172
  }
160
173
  });
161
174
  /**
162
- * resolveDefaultSource resolves a `play` argument without `--web`: discovery
175
+ * resolveDefaultSource resolves a `display` argument without `--web`: discovery
163
176
  * inputs go to `find`, everything else loads as a playlist or wraps as media.
164
177
  *
165
- * `play` casts an exact playable source. A marketplace URL / on-chain coords /
178
+ * `display` casts an exact source as it is. A marketplace URL / on-chain coords /
166
179
  * wallet must be resolved through the indexer first, so it redirects to `find`
167
180
  * rather than silently wrapping the *page* as a web item. Exception: a
168
181
  * discovery-classified URL that actually serves a DP-1 playlist document (the
169
- * discovery parser claims whole hosts, catching feed playlist URLs) plays
182
+ * discovery parser claims whole hosts, catching feed playlist URLs) displays
170
183
  * directly; see probeDiscoveryUrlAsPlaylist. The redirect names `--web` too,
171
184
  * for the caller who did mean the page.
172
185
  *
@@ -181,12 +194,12 @@ async function resolveDefaultSource(source, device) {
181
194
  probed = await probeDiscoveryUrlAsPlaylist(source);
182
195
  }
183
196
  if (discovery && !probed) {
184
- console.error(chalk_1.default.red('\n`play` casts an exact playlist or media URL — it does not search marketplaces.'));
197
+ console.error(chalk_1.default.red('\n`display` casts an exact playlist or media URL — it does not search marketplaces.'));
185
198
  console.error(chalk_1.default.dim(` "${source}" looks like ${discovery}, which needs to be resolved first.`));
186
199
  const deviceFlag = device ? ` -d "${device}"` : '';
187
- console.log(chalk_1.default.yellow(`\nTry: ff-cli find "${source}" --play -y${deviceFlag}`));
200
+ console.log(chalk_1.default.yellow(`\nTry: ff-cli find "${source}" --display -y${deviceFlag}`));
188
201
  if ((0, playlist_source_1.isPlaylistSourceUrl)(source)) {
189
- console.log(chalk_1.default.dim(`To show the page itself: ff-cli play --web "${source}"${deviceFlag}`));
202
+ console.log(chalk_1.default.dim(`To show the page itself: ff-cli display --web "${source}"${deviceFlag}`));
190
203
  }
191
204
  process.exit(1);
192
205
  }
@@ -199,7 +212,7 @@ async function resolveDefaultSource(source, device) {
199
212
  * a resolved source, then reports the outcome. Exits 1 on any failure.
200
213
  *
201
214
  * @param {PlaySource} resolved - What to cast
202
- * @param {Object} options - The `play` options
215
+ * @param {Object} options - The `display` options
203
216
  * @returns {Promise<void>}
204
217
  */
205
218
  async function castResolved(resolved, options) {
@@ -207,7 +220,7 @@ async function castResolved(resolved, options) {
207
220
  const sourceLabel = isPlaylistSource
208
221
  ? `${resolved.sourceType}: ${resolved.source}`
209
222
  : resolved.source;
210
- console.log(chalk_1.default.blue('\nPlay on FF1\n'));
223
+ console.log(chalk_1.default.blue('\nDisplay on FF1\n'));
211
224
  if (resolved.kind === 'media' && resolved.note) {
212
225
  console.log(chalk_1.default.yellow(resolved.note));
213
226
  }
@@ -236,7 +249,7 @@ async function castResolved(resolved, options) {
236
249
  console.error(chalk_1.default.red(`\n${result.error}`));
237
250
  }
238
251
  else {
239
- console.error(chalk_1.default.red('\nPlay failed:'), result.error);
252
+ console.error(chalk_1.default.red('\nDisplay failed:'), result.error);
240
253
  if (result.deviceDetails) {
241
254
  console.error(chalk_1.default.dim(` Details: ${result.deviceDetails}`));
242
255
  }
@@ -246,7 +259,7 @@ async function castResolved(resolved, options) {
246
259
  if (result.verified) {
247
260
  console.log(chalk_1.default.green(result.signed ? '✓ Signed and verified\n' : '✓ Verified\n'));
248
261
  }
249
- console.log(chalk_1.default.green('✓ Playing'));
262
+ console.log(chalk_1.default.green('✓ Displaying'));
250
263
  if (result.deviceName) {
251
264
  console.log(chalk_1.default.dim(` Device: ${result.deviceName}`));
252
265
  }
@@ -0,0 +1,251 @@
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.reportCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const fs_1 = require("fs");
10
+ const config_1 = require("../config");
11
+ const ff1_compatibility_1 = require("../utilities/ff1-compatibility");
12
+ const support_report_1 = require("../utilities/support-report");
13
+ /** REPORT_HELP says what a report carries, so a caller knows what leaves the machine before sending. */
14
+ const REPORT_HELP = `
15
+ The report text comes from --message or --file (not both), or from stdin when
16
+ neither is given and stdin is not a terminal. It goes privately to Feral File
17
+ support with this ff-cli version, the OS, and the Node version, and with
18
+ --email when given. Nothing else leaves this computer: no config, keys, or
19
+ playlists.
20
+
21
+ With -d, the report also carries the Art Computer's device ID, FF OS version and
22
+ branch, and once it is in, the Art Computer is asked to upload its logs under
23
+ the same reference. It uploads in the background and needs internet. On FF OS
24
+ with secure control, that request needs ff-cli paired with the owner role.
25
+
26
+ Exit status: 0 when the report reached Feral File support, even if the Art
27
+ Computer did not take the log request; 1 when it did not.
28
+
29
+ Examples:
30
+ ff-cli report --message "The work displays black on the Art Computer; in a desktop browser it renders." -d Office
31
+ ff-cli report --file report.txt --email you@example.com
32
+ printf '%s\\n' "$REPORT" | ff-cli report -d Office --dry-run
33
+ `;
34
+ /**
35
+ * `ff-cli report` sends a problem report to Feral File support, the same
36
+ * pipeline the app's Support & Feedback feeds, so an agent driving ff-cli can
37
+ * report what went wrong without a person pressing send. The wire contract and
38
+ * what may be sent live in utilities/support-report.ts.
39
+ */
40
+ exports.reportCommand = new commander_1.Command('report')
41
+ .description("Send a problem report to Feral File support, with an Art Computer's logs when one is involved")
42
+ .option('--message <text>', 'The report text')
43
+ .option('--file <path>', 'Read the report text from this file')
44
+ .option('--email <address>', 'Where Feral File support may reply: a bare address, name@example.com')
45
+ .option('-d, --device <name>', 'The Art Computer involved: adds its device ID, version and branch, and asks it to upload its logs')
46
+ .option('--dry-run', 'Print the JSON that would be sent, and send nothing')
47
+ .addHelpText('after', REPORT_HELP)
48
+ .action(async (options, command) => {
49
+ try {
50
+ // The program's own version, so the report carries exactly what
51
+ // `ff-cli --version` prints (index.ts reads the root package.json).
52
+ await runReport(options, command.parent?.version() || 'unknown');
53
+ }
54
+ catch (error) {
55
+ console.error(chalk_1.default.red('\nError:'), error.message);
56
+ process.exit(1);
57
+ }
58
+ });
59
+ /**
60
+ * runReport checks the report, reads the Art Computer's status, posts the
61
+ * report, then asks the Art Computer for its logs. Every refusal before the
62
+ * post exits 1 with nothing sent; after the post the exit status is 0.
63
+ *
64
+ * @param options - Command options
65
+ * @param ffCliVersion - The version `ff-cli --version` prints
66
+ * @returns Promise that resolves when the report is done
67
+ */
68
+ async function runReport(options, ffCliVersion) {
69
+ const report = await readReport(options);
70
+ const email = options.email?.trim();
71
+ checkReport(report, email);
72
+ const { baseUrl } = (0, config_1.getSupportLogsConfig)();
73
+ const device = options.device !== undefined ? selectDevice(options.device) : undefined;
74
+ const deviceBlock = device ? await describeDevice(device) : undefined;
75
+ // One serialization, so --dry-run prints the very bytes a real run posts.
76
+ const payload = (0, support_report_1.buildReportPayload)({ report, ffCliVersion, email, device: deviceBlock });
77
+ const body = JSON.stringify(payload, null, 2);
78
+ if (options.dryRun) {
79
+ printDryRun(body, baseUrl, device);
80
+ return;
81
+ }
82
+ const posted = await (0, support_report_1.postReport)(baseUrl, body);
83
+ if (posted.ok === false) {
84
+ exitWith(posted);
85
+ }
86
+ printReceipt(posted.receipt, email);
87
+ await askForLogs(device, posted.receipt);
88
+ }
89
+ /**
90
+ * readReport returns the trimmed report text from exactly one source. Flags
91
+ * win and stdin is read only when neither is given: an agent's stdin is often
92
+ * a pipe that carries nothing, and reading it beside a flag would wait on it.
93
+ *
94
+ * @param options - Command options
95
+ * @returns The report text, possibly empty
96
+ */
97
+ async function readReport(options) {
98
+ if (options.message !== undefined && options.file !== undefined) {
99
+ exitWith({ error: 'Give the report once: --message or --file, not both.' });
100
+ }
101
+ if (options.message !== undefined) {
102
+ return options.message.trim();
103
+ }
104
+ if (options.file !== undefined) {
105
+ try {
106
+ return (await fs_1.promises.readFile(options.file, 'utf-8')).trim();
107
+ }
108
+ catch (error) {
109
+ exitWith({ error: `Could not read --file ${options.file}: ${error.message}` });
110
+ }
111
+ }
112
+ if (process.stdin.isTTY) {
113
+ exitWith({
114
+ error: 'No report given.',
115
+ details: 'Pass --message "<text>" or --file <path>, or pipe the text on stdin.',
116
+ });
117
+ }
118
+ return (await readStdin()).trim();
119
+ }
120
+ /** readStdin reads all of stdin as UTF-8. */
121
+ function readStdin() {
122
+ return new Promise((resolve, reject) => {
123
+ let data = '';
124
+ process.stdin.setEncoding('utf8');
125
+ process.stdin.on('data', (chunk) => {
126
+ data += chunk;
127
+ });
128
+ process.stdin.on('end', () => resolve(data));
129
+ process.stdin.on('error', reject);
130
+ });
131
+ }
132
+ /**
133
+ * checkReport refuses a report support-logs would refuse, a reply address
134
+ * that is not one, and a report carrying this computer's signing key.
135
+ *
136
+ * @param report - Trimmed report text
137
+ * @param email - Trimmed --email value; an empty one is refused, not dropped
138
+ */
139
+ function checkReport(report, email) {
140
+ if (!report) {
141
+ exitWith({
142
+ error: 'The report is empty.',
143
+ details: 'If the text came from a shell variable, check that it is set.',
144
+ });
145
+ }
146
+ const bytes = Buffer.byteLength(report, 'utf8');
147
+ if (bytes > support_report_1.MAX_REPORT_BYTES) {
148
+ exitWith({
149
+ error: `The report is ${bytes} bytes; Feral File support takes at most ${support_report_1.MAX_REPORT_BYTES}.`,
150
+ details: "Shorten it. With -d the Art Computer's logs travel separately, so leave log excerpts out.",
151
+ });
152
+ }
153
+ if (email !== undefined && !(0, support_report_1.isBareAddress)(email)) {
154
+ exitWith({
155
+ error: `--email "${email}" is not an address Feral File support can reply to.`,
156
+ details: 'Give a bare ASCII address such as name@example.com, at most 254 characters. ' +
157
+ "Before the @: 1-64 characters of letters, digits and ! # $ % & ' * + / = ? ^ _ ` { | } ~ -, with single dots only between them. " +
158
+ 'After it: two or more dot-separated labels of letters, digits and hyphens, no label starting or ending with a hyphen. ' +
159
+ 'No display name, angle brackets, quotes, spaces, IP address, underscore in the domain, or non-ASCII characters.',
160
+ });
161
+ }
162
+ if (email !== undefined && (0, support_report_1.isReservedAddress)(email)) {
163
+ exitWith({
164
+ error: `--email "${email}" is an address Feral File support does not reply to.`,
165
+ details: "Support's own address and postmaster or mailer-daemon mailboxes are refused. Give the address where you want the reply, or leave --email out.",
166
+ });
167
+ }
168
+ if ((0, support_report_1.containsSigningKey)(report, (0, config_1.getPlaylistConfig)().privateKey)) {
169
+ exitWith({
170
+ error: "The report contains this computer's playlist signing key.",
171
+ details: 'Remove the key and run the command again. Nothing was sent.',
172
+ });
173
+ }
174
+ }
175
+ /** selectDevice resolves -d the way `display -d` does, or exits 1 naming the configured devices. */
176
+ function selectDevice(name) {
177
+ const selected = (0, ff1_compatibility_1.resolveConfiguredDevice)(name);
178
+ if (!selected.success || !selected.device) {
179
+ exitWith({ error: selected.error || 'The Art Computer is not configured correctly.' });
180
+ }
181
+ return selected.device;
182
+ }
183
+ /** describeDevice reads the Art Computer block, noting a failed status read without stopping. */
184
+ async function describeDevice(device) {
185
+ const status = await (0, support_report_1.readDeviceStatus)(device);
186
+ if (status.error) {
187
+ console.error(chalk_1.default.yellow(`Could not read the status of ${deviceLabel(device)} (${status.error}); the report goes without its version and branch.`));
188
+ }
189
+ return status.device;
190
+ }
191
+ /** printDryRun prints the body on stdout and what a real run would do on stderr. */
192
+ function printDryRun(body, baseUrl, device) {
193
+ process.stdout.write(`${body}\n`);
194
+ console.error(chalk_1.default.dim(`Dry run: nothing was sent. A real run posts this to ${baseUrl}/v2/cli/reports`));
195
+ if (device) {
196
+ console.error(chalk_1.default.dim(`Then it asks ${deviceLabel(device)} to upload its logs under the reference that comes back.`));
197
+ }
198
+ }
199
+ /** printReceipt reports the reference in the app's `Support [<id>]` form and who support can reply to. */
200
+ function printReceipt(receipt, email) {
201
+ console.log(chalk_1.default.green('✓ Report sent to Feral File support'));
202
+ console.log(` Reference: Support [${receipt.supportBundleId}]`);
203
+ console.log(chalk_1.default.dim(email
204
+ ? ` Support may reply to ${email}`
205
+ : ' No --email given, so support has no address to reply to'));
206
+ }
207
+ /**
208
+ * askForLogs asks the Art Computer to upload its logs under the report's
209
+ * reference and prints the outcome. Best effort: a failure prints a line and
210
+ * leaves the exit status at 0, since the report already reached support.
211
+ *
212
+ * @param device - Art Computer named with -d, if any
213
+ * @param receipt - The posted report's receipt
214
+ */
215
+ async function askForLogs(device, receipt) {
216
+ if (!device) {
217
+ console.log(chalk_1.default.dim(' No Art Computer named (-d), so no device logs were requested'));
218
+ return;
219
+ }
220
+ const label = deviceLabel(device);
221
+ if (!receipt.uploadToken) {
222
+ console.log(chalk_1.default.yellow(`${label} was not asked to upload its logs: support returned no upload token.`));
223
+ return;
224
+ }
225
+ const result = await (0, support_report_1.requestLogUpload)(device, {
226
+ supportBundleId: receipt.supportBundleId,
227
+ uploadToken: receipt.uploadToken,
228
+ });
229
+ if (result.success) {
230
+ console.log(chalk_1.default.green(`✓ Asked ${label} to upload its logs under the same reference`));
231
+ console.log(chalk_1.default.dim(' It uploads in the background and needs internet to do it'));
232
+ return;
233
+ }
234
+ console.log(chalk_1.default.yellow(`${label} did not take the request to upload its logs: ${result.error}`));
235
+ if (result.details) {
236
+ console.log(chalk_1.default.dim(` ${result.details}`));
237
+ }
238
+ console.log(chalk_1.default.dim(' The report itself reached support.'));
239
+ }
240
+ /** deviceLabel names a device in output. */
241
+ function deviceLabel(device) {
242
+ return device.name || device.host;
243
+ }
244
+ /** exitWith prints a failure and exits 1. */
245
+ function exitWith(failure) {
246
+ console.error(chalk_1.default.red(`\n${failure.error}`));
247
+ if (failure.details) {
248
+ console.error(chalk_1.default.dim(` ${failure.details}`));
249
+ }
250
+ process.exit(1);
251
+ }
@@ -318,7 +318,7 @@ exports.setupCommand = new commander_1.Command('setup')
318
318
  console.log(chalk_1.default.yellow(' • Add an FF1 device host'));
319
319
  }
320
320
  }
321
- console.log(chalk_1.default.dim('\nRun: ff-cli play <url-or-playlist>'));
321
+ console.log(chalk_1.default.dim('\nRun: ff-cli display <url-or-playlist>'));
322
322
  }
323
323
  catch (error) {
324
324
  console.error(chalk_1.default.red('\nSetup failed:'), error.message);