@feralfile/cli 2.10.1 → 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.
- package/README.md +31 -23
- package/config.json.example +6 -1
- package/dist/index.js +10 -2
- package/dist/package.json +5 -2
- package/dist/src/commands/build.js +40 -0
- package/dist/src/commands/config.js +1 -0
- package/dist/src/commands/device.js +85 -40
- package/dist/src/commands/enrich.js +18 -23
- package/dist/src/commands/find.js +144 -106
- package/dist/src/commands/helpers/device-control.js +937 -0
- package/dist/src/commands/helpers/hidden-alias-help.js +37 -0
- package/dist/src/commands/helpers/playlist-display.js +2 -15
- package/dist/src/commands/helpers/prompt.js +85 -10
- package/dist/src/commands/play.js +162 -84
- package/dist/src/commands/report.js +251 -0
- package/dist/src/commands/setup.js +82 -7
- package/dist/src/commands/status.js +4 -1
- package/dist/src/config.js +256 -6
- package/dist/src/main.js +14 -5
- package/dist/src/utilities/control/codec.js +578 -0
- package/dist/src/utilities/control/control-errors.js +157 -0
- package/dist/src/utilities/control/controller-store.js +964 -0
- package/dist/src/utilities/control/jwk.js +141 -0
- package/dist/src/utilities/control/pairing-channel.js +52 -0
- package/dist/src/utilities/control/pairing.js +416 -0
- package/dist/src/utilities/control/registration.js +391 -0
- package/dist/src/utilities/control/relay-pairing-channel.js +191 -0
- package/dist/src/utilities/control/secure-transport.js +850 -0
- package/dist/src/utilities/control/strict-json.js +376 -0
- package/dist/src/utilities/device-lookup.js +36 -11
- package/dist/src/utilities/device-network.js +129 -0
- package/dist/src/utilities/device-upsert.js +1 -1
- package/dist/src/utilities/feral-file-artwork.js +2 -2
- package/dist/src/utilities/ff1-compatibility.js +87 -11
- package/dist/src/utilities/ff1-device.js +161 -78
- package/dist/src/utilities/ff1-relayer.js +34 -14
- package/dist/src/utilities/handoff-client.js +23 -9
- package/dist/src/utilities/index.js +270 -259
- package/dist/src/utilities/media-url.js +117 -0
- package/dist/src/utilities/neort-marketplace.js +3 -3
- package/dist/src/utilities/nft-indexer.js +12 -376
- package/dist/src/utilities/playlist-builder.js +11 -2
- package/dist/src/utilities/playlist-cast.js +6 -6
- package/dist/src/utilities/playlist-source.js +223 -34
- package/dist/src/utilities/raster-client.js +101 -14
- package/dist/src/utilities/relay-client.js +774 -0
- package/dist/src/utilities/ssh-access.js +192 -58
- package/dist/src/utilities/support-report.js +511 -0
- package/dist/src/utilities/temporary-channel.js +139 -0
- package/dist/src/utilities/token-item.js +428 -0
- package/dist/src/utilities/token-source-contract.js +55 -0
- package/dist/src/utilities/token-source-indexer.js +99 -0
- package/dist/src/utilities/token-source-raster.js +378 -0
- package/dist/src/utilities/token-source.js +48 -0
- package/docs/CONFIGURATION.md +331 -28
- package/docs/EXAMPLES.md +33 -33
- package/docs/PROJECT_SPEC.md +55 -36
- package/docs/README.md +145 -43
- 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 `
|
|
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
|
|
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`).
|
|
@@ -36,23 +36,98 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.NonInteractiveError = exports.NON_TTY_ANSWER_TIMEOUT_MS = void 0;
|
|
40
|
+
exports.isNonInteractiveError = isNonInteractiveError;
|
|
39
41
|
exports.createPrompt = createPrompt;
|
|
40
42
|
exports.promptYesNo = promptYesNo;
|
|
41
43
|
const readline = __importStar(require("readline"));
|
|
42
44
|
const chalk_1 = __importDefault(require("chalk"));
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
/**
|
|
46
|
+
* NON_TTY_ANSWER_TIMEOUT_MS bounds how long a question waits for an answer on
|
|
47
|
+
* a stdin that is not a terminal.
|
|
48
|
+
*
|
|
49
|
+
* Piped answers (`echo n | ff-cli find …`) arrive at once and still work. An
|
|
50
|
+
* agent harness that leaves stdin open but never writes to it would otherwise
|
|
51
|
+
* wait forever, which reads as a hang the agent cannot diagnose.
|
|
52
|
+
*/
|
|
53
|
+
exports.NON_TTY_ANSWER_TIMEOUT_MS = 10_000;
|
|
54
|
+
/**
|
|
55
|
+
* NonInteractiveError reports a question that got no answer because stdin is
|
|
56
|
+
* not a terminal: input ended first, or nothing arrived in time.
|
|
57
|
+
*
|
|
58
|
+
* Before this error, end-of-input left readline's callback uncalled and the
|
|
59
|
+
* process exited 0 having done nothing (`find <url>` printed "Build playlist?
|
|
60
|
+
* [Y/n]" and quit). That looks like success to a caller that trusts exit
|
|
61
|
+
* codes, so the question fails loudly instead, naming what would answer it.
|
|
62
|
+
*
|
|
63
|
+
* Callers match on `name` (isNonInteractiveError), not `instanceof`: tests
|
|
64
|
+
* replace this module with a stub that does not export the class.
|
|
65
|
+
*/
|
|
66
|
+
class NonInteractiveError extends Error {
|
|
67
|
+
constructor(question, hint) {
|
|
68
|
+
const asked = question.trim();
|
|
69
|
+
super(`"${asked}" needs an answer, and none came: stdin is not a terminal.${hint ? ` ${hint}` : ''}`);
|
|
70
|
+
this.name = 'NonInteractiveError';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.NonInteractiveError = NonInteractiveError;
|
|
74
|
+
/**
|
|
75
|
+
* isNonInteractiveError reports whether an error came from a question that
|
|
76
|
+
* got no answer on a non-terminal stdin.
|
|
77
|
+
*
|
|
78
|
+
* @param {unknown} error - Any caught value
|
|
79
|
+
* @returns {boolean} True for a NonInteractiveError
|
|
80
|
+
*/
|
|
81
|
+
function isNonInteractiveError(error) {
|
|
82
|
+
return error instanceof Error && error.name === 'NonInteractiveError';
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* createPrompt returns a prompt that asks on stdin and never waits silently.
|
|
86
|
+
*
|
|
87
|
+
* The readline interface opens on the first question, not at creation, so a
|
|
88
|
+
* command that never asks never attaches to stdin. A question rejects when
|
|
89
|
+
* input ends before an answer (on a terminal that is Ctrl-D; otherwise a
|
|
90
|
+
* NonInteractiveError), and, when stdin is not a terminal, when no answer
|
|
91
|
+
* arrives within NON_TTY_ANSWER_TIMEOUT_MS.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} [nonInteractiveHint] - What answers the question without a
|
|
94
|
+
* terminal (usually a flag); appended to the NonInteractiveError message
|
|
95
|
+
* @param {Object} [options] - Test seams
|
|
96
|
+
* @param {PromptInput} [options.input] - Stream to read answers from (default stdin)
|
|
97
|
+
* @param {number} [options.answerTimeoutMs] - Non-terminal wait bound
|
|
98
|
+
* @returns {Prompt} ask/close pair
|
|
99
|
+
*/
|
|
100
|
+
function createPrompt(nonInteractiveHint, options = {}) {
|
|
101
|
+
const input = options.input ?? process.stdin;
|
|
102
|
+
const timeoutMs = options.answerTimeoutMs ?? exports.NON_TTY_ANSWER_TIMEOUT_MS;
|
|
103
|
+
let rl = null;
|
|
104
|
+
const ask = async (question) => {
|
|
105
|
+
rl ??= readline.createInterface({ input, output: process.stdout });
|
|
106
|
+
const iface = rl;
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
let timer;
|
|
109
|
+
const settle = (finish) => {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
iface.off('close', onClose);
|
|
112
|
+
finish();
|
|
113
|
+
};
|
|
114
|
+
const noAnswer = () => input.isTTY
|
|
115
|
+
? new Error('Input closed before an answer was given.')
|
|
116
|
+
: new NonInteractiveError(question, nonInteractiveHint);
|
|
117
|
+
const onClose = () => settle(() => reject(noAnswer()));
|
|
118
|
+
iface.once('close', onClose);
|
|
119
|
+
if (!input.isTTY) {
|
|
120
|
+
timer = setTimeout(() => settle(() => {
|
|
121
|
+
reject(noAnswer());
|
|
122
|
+
iface.close();
|
|
123
|
+
}), timeoutMs);
|
|
124
|
+
}
|
|
125
|
+
iface.question(chalk_1.default.yellow(question), (answer) => settle(() => resolve(answer.trim())));
|
|
51
126
|
});
|
|
52
|
-
}
|
|
127
|
+
};
|
|
53
128
|
return {
|
|
54
129
|
ask,
|
|
55
|
-
close: () => rl
|
|
130
|
+
close: () => rl?.close(),
|
|
56
131
|
};
|
|
57
132
|
}
|
|
58
133
|
async function promptYesNo(ask, question, defaultYes = true) {
|
|
@@ -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.
|
|
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 `
|
|
18
|
+
* Human label for a discovery input that `display` cannot cast directly.
|
|
18
19
|
*
|
|
19
|
-
* `
|
|
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 `
|
|
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 `
|
|
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
|
|
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,15 +73,21 @@ 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
|
|
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)
|
|
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
|
|
83
84
|
* http(s) URLs; coordinates and wallet addresses never hit the network.
|
|
85
|
+
*
|
|
86
|
+
* A URL that does not exist (404/410, no such host, a broken `.json` link)
|
|
87
|
+
* is refused here rather than redirected: the redirect also suggests
|
|
88
|
+
* `display --web`, which would cast the error page.
|
|
89
|
+
*
|
|
90
|
+
* @throws {Error} When the URL does not exist (refusalForLoadFailure)
|
|
84
91
|
*/
|
|
85
92
|
async function probeDiscoveryUrlAsPlaylist(source) {
|
|
86
93
|
if (!(0, playlist_source_1.isPlaylistSourceUrl)(source)) {
|
|
@@ -99,89 +106,63 @@ async function probeDiscoveryUrlAsPlaylist(source) {
|
|
|
99
106
|
source: loaded.source,
|
|
100
107
|
};
|
|
101
108
|
}
|
|
102
|
-
catch {
|
|
109
|
+
catch (error) {
|
|
110
|
+
const refusal = (0, playlist_source_1.refusalForLoadFailure)(source.trim(), error);
|
|
111
|
+
if (refusal) {
|
|
112
|
+
throw refusal;
|
|
113
|
+
}
|
|
103
114
|
return null;
|
|
104
115
|
}
|
|
105
116
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
/**
|
|
118
|
+
* resolveWebPageSource wraps `source` as a web page for `display --web`, without
|
|
119
|
+
* the discovery redirect or the playlist probe.
|
|
120
|
+
*
|
|
121
|
+
* `--web` exists because the discovery parser claims whole hosts: any page on
|
|
122
|
+
* feralfile.com, opensea.io, objkt.com and the like is redirected to `find`,
|
|
123
|
+
* and `find` rejects every page that is not an artwork it can resolve (a
|
|
124
|
+
* homepage, an exhibition overview, a profile). Before `--web` there was no
|
|
125
|
+
* way to put such a page on display; the two commands pointed at each other.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} source - The URL the caller passed
|
|
128
|
+
* @returns {Promise<PlaySource>} A `media` source with a `text/html` item
|
|
129
|
+
* @throws {Error} When `source` is not an http(s) URL
|
|
130
|
+
*/
|
|
131
|
+
async function resolveWebPageSource(source) {
|
|
132
|
+
const url = source.trim();
|
|
133
|
+
if (!(0, playlist_source_1.isPlaylistSourceUrl)(url)) {
|
|
134
|
+
throw new Error(`--web takes an http(s) URL; got "${source}".`);
|
|
135
|
+
}
|
|
136
|
+
const note = await (0, playlist_source_1.checkWebPageUrl)(url);
|
|
137
|
+
return (0, playlist_source_1.buildMediaPlaySource)(url, { asWebPage: true, note });
|
|
138
|
+
}
|
|
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')
|
|
153
|
+
.argument('<source>', 'Playlist file, playlist URL, or any media or web page URL')
|
|
109
154
|
.option('-d, --device <name>', 'Device name (uses first device if not specified)')
|
|
110
|
-
.option('--
|
|
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')
|
|
111
157
|
.action(async (source, options) => {
|
|
112
158
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// playlist document (the discovery parser claims whole hosts, catching
|
|
118
|
-
// feed playlist URLs) plays directly — see probeDiscoveryUrlAsPlaylist.
|
|
119
|
-
const discovery = describeDiscoveryInput(source);
|
|
120
|
-
let probed = null;
|
|
121
|
-
if (discovery) {
|
|
122
|
-
probed = await probeDiscoveryUrlAsPlaylist(source);
|
|
123
|
-
}
|
|
124
|
-
if (discovery && !probed) {
|
|
125
|
-
console.error(chalk_1.default.red('\n`play` casts an exact playlist or media URL — it does not search marketplaces.'));
|
|
126
|
-
console.error(chalk_1.default.dim(` "${source}" looks like ${discovery}, which needs to be resolved first.`));
|
|
127
|
-
const deviceFlag = options.device ? ` -d "${options.device}"` : '';
|
|
128
|
-
console.log(chalk_1.default.yellow(`\nTry: ff-cli find "${source}" --play${deviceFlag}`));
|
|
129
|
-
process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
// No explicit duration: a direct video/audio URL plays its natural
|
|
132
|
-
// length (DP-1 §4.1); static media uses config.defaultDuration.
|
|
133
|
-
const resolved = probed ?? (await (0, playlist_source_1.resolvePlaySource)(source));
|
|
134
|
-
const isPlaylistSource = resolved.kind === 'playlist';
|
|
135
|
-
const sourceLabel = isPlaylistSource
|
|
136
|
-
? `${resolved.sourceType}: ${resolved.source}`
|
|
137
|
-
: resolved.source;
|
|
138
|
-
console.log(chalk_1.default.blue('\nPlay on FF1\n'));
|
|
139
|
-
if (!options.skipVerify && isPlaylistSource) {
|
|
140
|
-
console.log(chalk_1.default.cyan(`Verify playlist (${sourceLabel})`));
|
|
141
|
-
}
|
|
142
|
-
// A direct media/web URL is wrapped in an unsigned single-item playlist,
|
|
143
|
-
// so it must be signed before the verify gate; a loaded playlist is sent
|
|
144
|
-
// as-is. castPlaylist owns the sign → verify → send sequence.
|
|
145
|
-
// Resolve through getPlaylistConfig rather than reading config.playlist directly: it is the one
|
|
146
|
-
// place that knows a config value may still be the config.json.example placeholder, in which case a
|
|
147
|
-
// valid PLAYLIST_PRIVATE_KEY must win. Duplicating the `config || env` precedence here is what made
|
|
148
|
-
// this path sign with the template literal while every other signing path used the real key.
|
|
149
|
-
const privateKey = (0, config_1.getPlaylistConfig)().privateKey || undefined;
|
|
150
|
-
const result = await (0, playlist_cast_1.castPlaylist)(resolved.playlist, {
|
|
151
|
-
deviceName: options.device,
|
|
152
|
-
skipVerify: options.skipVerify,
|
|
153
|
-
requireSignature: resolved.kind === 'media',
|
|
154
|
-
signingKey: privateKey,
|
|
155
|
-
});
|
|
156
|
-
if (!result.success) {
|
|
157
|
-
if (result.stage === 'verify') {
|
|
158
|
-
(0, playlist_display_1.printPlaylistVerificationFailure)({ valid: false, error: result.error, details: result.details }, isPlaylistSource ? `source: ${sourceLabel}` : undefined);
|
|
159
|
-
}
|
|
160
|
-
else if (result.stage === 'sign') {
|
|
161
|
-
console.error(chalk_1.default.red(`\n${result.error}`));
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
console.error(chalk_1.default.red('\nPlay failed:'), result.error);
|
|
165
|
-
if (result.deviceDetails) {
|
|
166
|
-
console.error(chalk_1.default.dim(` Details: ${result.deviceDetails}`));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
process.exit(1);
|
|
170
|
-
}
|
|
171
|
-
if (result.verified) {
|
|
172
|
-
console.log(chalk_1.default.green(result.signed ? '✓ Signed and verified\n' : '✓ Verified\n'));
|
|
173
|
-
}
|
|
174
|
-
console.log(chalk_1.default.green('✓ Playing'));
|
|
175
|
-
if (result.deviceName) {
|
|
176
|
-
console.log(chalk_1.default.dim(` Device: ${result.deviceName}`));
|
|
177
|
-
}
|
|
178
|
-
if (result.device) {
|
|
179
|
-
console.log(chalk_1.default.dim(` Host: ${result.device}`));
|
|
180
|
-
}
|
|
181
|
-
console.log();
|
|
159
|
+
const resolved = options.web
|
|
160
|
+
? await resolveWebPageSource(source)
|
|
161
|
+
: await resolveDefaultSource(source, options.device);
|
|
162
|
+
await castResolved(resolved, options);
|
|
182
163
|
}
|
|
183
164
|
catch (error) {
|
|
184
|
-
if ((0, playlist_source_1.isPlaylistSourceUrl)(source)) {
|
|
165
|
+
if (!options.web && (0, playlist_source_1.isPlaylistSourceUrl)(source)) {
|
|
185
166
|
(0, playlist_display_1.printPlaylistSourceLoadFailure)(source, error);
|
|
186
167
|
}
|
|
187
168
|
else {
|
|
@@ -190,3 +171,100 @@ exports.playCommand = new commander_1.Command('play')
|
|
|
190
171
|
process.exit(1);
|
|
191
172
|
}
|
|
192
173
|
});
|
|
174
|
+
/**
|
|
175
|
+
* resolveDefaultSource resolves a `display` argument without `--web`: discovery
|
|
176
|
+
* inputs go to `find`, everything else loads as a playlist or wraps as media.
|
|
177
|
+
*
|
|
178
|
+
* `display` casts an exact source as it is. A marketplace URL / on-chain coords /
|
|
179
|
+
* wallet must be resolved through the indexer first, so it redirects to `find`
|
|
180
|
+
* rather than silently wrapping the *page* as a web item. Exception: a
|
|
181
|
+
* discovery-classified URL that actually serves a DP-1 playlist document (the
|
|
182
|
+
* discovery parser claims whole hosts, catching feed playlist URLs) displays
|
|
183
|
+
* directly; see probeDiscoveryUrlAsPlaylist. The redirect names `--web` too,
|
|
184
|
+
* for the caller who did mean the page.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} source - The URL, path, or reference the caller passed
|
|
187
|
+
* @param {string} [device] - Device name, carried into the suggested command
|
|
188
|
+
* @returns {Promise<PlaySource>} What to cast; exits 1 on a discovery input
|
|
189
|
+
*/
|
|
190
|
+
async function resolveDefaultSource(source, device) {
|
|
191
|
+
const discovery = describeDiscoveryInput(source);
|
|
192
|
+
let probed = null;
|
|
193
|
+
if (discovery) {
|
|
194
|
+
probed = await probeDiscoveryUrlAsPlaylist(source);
|
|
195
|
+
}
|
|
196
|
+
if (discovery && !probed) {
|
|
197
|
+
console.error(chalk_1.default.red('\n`display` casts an exact playlist or media URL — it does not search marketplaces.'));
|
|
198
|
+
console.error(chalk_1.default.dim(` "${source}" looks like ${discovery}, which needs to be resolved first.`));
|
|
199
|
+
const deviceFlag = device ? ` -d "${device}"` : '';
|
|
200
|
+
console.log(chalk_1.default.yellow(`\nTry: ff-cli find "${source}" --display -y${deviceFlag}`));
|
|
201
|
+
if ((0, playlist_source_1.isPlaylistSourceUrl)(source)) {
|
|
202
|
+
console.log(chalk_1.default.dim(`To show the page itself: ff-cli display --web "${source}"${deviceFlag}`));
|
|
203
|
+
}
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
// No explicit duration: a direct video/audio URL plays its natural
|
|
207
|
+
// length (DP-1 §4.1); static media uses config.defaultDuration.
|
|
208
|
+
return probed ?? (await (0, playlist_source_1.resolvePlaySource)(source));
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* castResolved signs (for a synthesized media playlist), verifies, and sends
|
|
212
|
+
* a resolved source, then reports the outcome. Exits 1 on any failure.
|
|
213
|
+
*
|
|
214
|
+
* @param {PlaySource} resolved - What to cast
|
|
215
|
+
* @param {Object} options - The `display` options
|
|
216
|
+
* @returns {Promise<void>}
|
|
217
|
+
*/
|
|
218
|
+
async function castResolved(resolved, options) {
|
|
219
|
+
const isPlaylistSource = resolved.kind === 'playlist';
|
|
220
|
+
const sourceLabel = isPlaylistSource
|
|
221
|
+
? `${resolved.sourceType}: ${resolved.source}`
|
|
222
|
+
: resolved.source;
|
|
223
|
+
console.log(chalk_1.default.blue('\nDisplay on FF1\n'));
|
|
224
|
+
if (resolved.kind === 'media' && resolved.note) {
|
|
225
|
+
console.log(chalk_1.default.yellow(resolved.note));
|
|
226
|
+
}
|
|
227
|
+
if (!options.skipVerify && isPlaylistSource) {
|
|
228
|
+
console.log(chalk_1.default.cyan(`Verify playlist (${sourceLabel})`));
|
|
229
|
+
}
|
|
230
|
+
// A direct media/web URL is wrapped in an unsigned single-item playlist,
|
|
231
|
+
// so it must be signed before the verify gate; a loaded playlist is sent
|
|
232
|
+
// as-is. castPlaylist owns the sign → verify → send sequence.
|
|
233
|
+
// Resolve through getPlaylistConfig rather than reading config.playlist directly: it is the one
|
|
234
|
+
// place that knows a config value may still be the config.json.example placeholder, in which case a
|
|
235
|
+
// valid PLAYLIST_PRIVATE_KEY must win. Duplicating the `config || env` precedence here is what made
|
|
236
|
+
// this path sign with the template literal while every other signing path used the real key.
|
|
237
|
+
const privateKey = (0, config_1.getPlaylistConfig)().privateKey || undefined;
|
|
238
|
+
const result = await (0, playlist_cast_1.castPlaylist)(resolved.playlist, {
|
|
239
|
+
deviceName: options.device,
|
|
240
|
+
skipVerify: options.skipVerify,
|
|
241
|
+
requireSignature: resolved.kind === 'media',
|
|
242
|
+
signingKey: privateKey,
|
|
243
|
+
});
|
|
244
|
+
if (!result.success) {
|
|
245
|
+
if (result.stage === 'verify') {
|
|
246
|
+
(0, playlist_display_1.printPlaylistVerificationFailure)({ valid: false, error: result.error, details: result.details }, isPlaylistSource ? `source: ${sourceLabel}` : undefined);
|
|
247
|
+
}
|
|
248
|
+
else if (result.stage === 'sign') {
|
|
249
|
+
console.error(chalk_1.default.red(`\n${result.error}`));
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
console.error(chalk_1.default.red('\nDisplay failed:'), result.error);
|
|
253
|
+
if (result.deviceDetails) {
|
|
254
|
+
console.error(chalk_1.default.dim(` Details: ${result.deviceDetails}`));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
if (result.verified) {
|
|
260
|
+
console.log(chalk_1.default.green(result.signed ? '✓ Signed and verified\n' : '✓ Verified\n'));
|
|
261
|
+
}
|
|
262
|
+
console.log(chalk_1.default.green('✓ Displaying'));
|
|
263
|
+
if (result.deviceName) {
|
|
264
|
+
console.log(chalk_1.default.dim(` Device: ${result.deviceName}`));
|
|
265
|
+
}
|
|
266
|
+
if (result.device) {
|
|
267
|
+
console.log(chalk_1.default.dim(` Host: ${result.device}`));
|
|
268
|
+
}
|
|
269
|
+
console.log();
|
|
270
|
+
}
|