@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
|
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.findCommand = void 0;
|
|
7
|
-
exports.buildRasterMediaItems = buildRasterMediaItems;
|
|
8
7
|
exports.parseLimitOption = parseLimitOption;
|
|
9
8
|
exports.decideActions = decideActions;
|
|
10
9
|
const commander_1 = require("commander");
|
|
@@ -22,9 +21,10 @@ const neort_marketplace_1 = require("../utilities/neort-marketplace");
|
|
|
22
21
|
const verse_marketplace_1 = require("../utilities/verse-marketplace");
|
|
23
22
|
const raster_client_1 = require("../utilities/raster-client");
|
|
24
23
|
const playlist_cast_1 = require("../utilities/playlist-cast");
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
24
|
+
const token_source_1 = require("../utilities/token-source");
|
|
25
|
+
const token_source_raster_1 = require("../utilities/token-source-raster");
|
|
26
|
+
const config_1 = require("../config");
|
|
27
|
+
// playlist-builder is still CommonJS; require keeps interop simple.
|
|
28
28
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
29
|
const { buildDP1Playlist, buildUrlItem } = require('../utilities/playlist-builder');
|
|
30
30
|
/**
|
|
@@ -35,18 +35,56 @@ const { buildDP1Playlist, buildUrlItem } = require('../utilities/playlist-builde
|
|
|
35
35
|
*/
|
|
36
36
|
const DP1_MAX_ITEMS = 1024;
|
|
37
37
|
const RASTER_PAGE_SIZE = 100;
|
|
38
|
+
/**
|
|
39
|
+
* FIND_NON_INTERACTIVE_HINT is what `find` tells a caller without a terminal
|
|
40
|
+
* when one of its questions comes up. `-y` answers every question with its
|
|
41
|
+
* default, and that default is to display, so the hint names `--no-display` in
|
|
42
|
+
* the same breath for a caller that only wants the file.
|
|
43
|
+
*/
|
|
44
|
+
const FIND_NON_INTERACTIVE_HINT = 'Re-run with -y to accept the defaults (it builds the playlist and displays it on your FF1); add --no-display to only save it.';
|
|
45
|
+
/**
|
|
46
|
+
* playAPageHint points a caller who handed `find` a page it cannot resolve to
|
|
47
|
+
* the command that shows a page as it is.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} input - The URL the caller gave `find`
|
|
50
|
+
* @returns {string} One line naming `ff-cli display --web`
|
|
51
|
+
*/
|
|
52
|
+
function playAPageHint(input) {
|
|
53
|
+
return `To show this page itself on your FF1, run: ff-cli display --web "${input}"`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* displayIntent folds the hidden `--play` / `--no-play` spellings into
|
|
57
|
+
* `--display` / `--no-display`.
|
|
58
|
+
*
|
|
59
|
+
* The verb for art is display, not play (B&F weekly 2026-11-03; Canon
|
|
60
|
+
* reference/naming-brand.md §3.4), so `find` takes `--display` and
|
|
61
|
+
* `--no-display`. `-p, --play` and `--no-play` stay accepted, hidden from
|
|
62
|
+
* help, so existing scripts and agents keep working. Commander stores the two
|
|
63
|
+
* spellings under separate keys (`display`, `play`); this is the one place
|
|
64
|
+
* that reads both. The current spelling wins when a caller passes both.
|
|
65
|
+
*
|
|
66
|
+
* @param {FindOptions} options - Parsed `find` options
|
|
67
|
+
* @returns {boolean | undefined} true to display, false to never cast,
|
|
68
|
+
* undefined when neither spelling was passed
|
|
69
|
+
*/
|
|
70
|
+
function displayIntent(options) {
|
|
71
|
+
return options.display ?? options.play;
|
|
72
|
+
}
|
|
38
73
|
exports.findCommand = new commander_1.Command('find')
|
|
39
74
|
.description('Find an artwork on the web and build a DP-1 playlist')
|
|
40
|
-
.argument('<input>', 'URL (Objkt / fxhash / Art Blocks / OpenSea / SuperRare / Feral File / Neort / Verse / Raster), `ethereum:{contract}:{tokenId}`, `tezos:{contract}:{tokenId}`, or a wallet address')
|
|
75
|
+
.argument('<input>', 'URL (Objkt / fxhash / Art Blocks / OpenSea / SuperRare / Feral File / Neort / Verse / Raster), `ethereum:{contract}:{tokenId}`, `tezos:{contract}:{tokenId}`, or a wallet address (picks one work by the artist at that address; for what a wallet owns, see `ff-cli build --help`)')
|
|
41
76
|
.option('-o, --output <path>', 'Save the playlist to this file (default: ./<slug>.json)')
|
|
42
77
|
.option('-l, --limit <n>', 'Max tokens to include from the series (default: all)')
|
|
43
|
-
.option('
|
|
44
|
-
.option('--no-
|
|
45
|
-
|
|
78
|
+
.option('--display', 'Display the playlist on an FF1 device after building')
|
|
79
|
+
.option('--no-display', 'Never cast to a device; combine with -y to build and save without displaying')
|
|
80
|
+
// The retired spellings of the two flags above, accepted but never listed; see displayIntent.
|
|
81
|
+
.addOption(new commander_1.Option('-p, --play').hideHelp())
|
|
82
|
+
.addOption(new commander_1.Option('--no-play').hideHelp())
|
|
83
|
+
.option('-d, --device <name>', 'Device to display on (used with --display; default: first configured)')
|
|
46
84
|
.option('--publish', 'Publish the playlist to a configured feed server')
|
|
47
85
|
.option('-s, --server <index>', 'Feed server index (used with --publish)')
|
|
48
|
-
.option('-y, --yes', 'Skip interactive prompts. Without --output or --publish, -y
|
|
49
|
-
.option('--skip-verify', 'Skip signature verification before --
|
|
86
|
+
.option('-y, --yes', 'Skip interactive prompts. Without --output or --publish, -y displays the playlist on a device; add --no-display to skip casting')
|
|
87
|
+
.option('--skip-verify', 'Skip signature verification before --display (mirrors `ff-cli display --skip-verify`)')
|
|
50
88
|
.action(async (input, options) => {
|
|
51
89
|
try {
|
|
52
90
|
console.log(chalk_1.default.blue('\nFind on Feral File CLI\n'));
|
|
@@ -56,6 +94,11 @@ exports.findCommand = new commander_1.Command('find')
|
|
|
56
94
|
console.error(chalk_1.default.dim('Supported URLs: Objkt, fxhash, Art Blocks, OpenSea, SuperRare, Feral File, Neort, Verse, Raster. ' +
|
|
57
95
|
'Or: `ethereum:{contract}:{tokenId}`, `tezos:{contract}:{tokenId}`, ' +
|
|
58
96
|
'or a wallet address (`0x...` / `tz1.../tz2.../tz3...`).'));
|
|
97
|
+
// A plain web page or media URL is not an artwork lookup at all; `display`
|
|
98
|
+
// casts those directly, so name it rather than leave the caller stuck.
|
|
99
|
+
if (/^https?:\/\//i.test(input.trim())) {
|
|
100
|
+
console.error(chalk_1.default.dim(`To display a web page or media URL as it is, run: ff-cli display "${input}"`));
|
|
101
|
+
}
|
|
59
102
|
process.exit(1);
|
|
60
103
|
}
|
|
61
104
|
if (parsed.kind === 'unsupported') {
|
|
@@ -65,9 +108,12 @@ exports.findCommand = new commander_1.Command('find')
|
|
|
65
108
|
return;
|
|
66
109
|
}
|
|
67
110
|
console.error(chalk_1.default.red(parsed.reason));
|
|
111
|
+
// `display` sends unrecognized pages on these hosts here, so the way out
|
|
112
|
+
// of that loop has to be printed on this side too.
|
|
113
|
+
console.error(chalk_1.default.dim(playAPageHint(input)));
|
|
68
114
|
process.exit(1);
|
|
69
115
|
}
|
|
70
|
-
// Neort is off-chain; its items skip
|
|
116
|
+
// Neort is off-chain; its items skip the token source entirely and
|
|
71
117
|
// build a DP-1 entry directly from Neort's API response.
|
|
72
118
|
if (parsed.kind === 'neort-art') {
|
|
73
119
|
await runNeortFind(parsed.id, options);
|
|
@@ -113,14 +159,13 @@ async function runResolvedTarget(target, options) {
|
|
|
113
159
|
else if (target.kind === 'token-list') {
|
|
114
160
|
const allTokens = target.coords.map((coords) => ({
|
|
115
161
|
...coords,
|
|
116
|
-
|
|
117
|
-
mediaType: null,
|
|
162
|
+
rasterToken: null,
|
|
118
163
|
}));
|
|
119
164
|
tokens = allTokens.slice(0, limit);
|
|
120
165
|
seriesHasMore = allTokens.length > limit || !!target.hasMore;
|
|
121
166
|
}
|
|
122
167
|
else {
|
|
123
|
-
tokens = [{ ...target.coords,
|
|
168
|
+
tokens = [{ ...target.coords, rasterToken: null }];
|
|
124
169
|
}
|
|
125
170
|
if (tokens.length === 0) {
|
|
126
171
|
console.error(chalk_1.default.red('Series has no tokens on supported chains (Ethereum + Tezos mainnet).'));
|
|
@@ -136,48 +181,21 @@ async function runResolvedTarget(target, options) {
|
|
|
136
181
|
console.log(chalk_1.default.dim('Cancelled.'));
|
|
137
182
|
return;
|
|
138
183
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
if (tokens.length > 10) {
|
|
184
|
+
const source = (0, token_source_1.getTokenSource)();
|
|
185
|
+
const label = source.name === 'raster' ? 'Raster' : 'FF indexer';
|
|
186
|
+
console.log(chalk_1.default.dim(`Resolving ${tokens.length} token${tokens.length === 1 ? '' : 's'} via ${label}...`));
|
|
187
|
+
// The indexer warms previously-unseen tokens by polling, which
|
|
188
|
+
// takes minutes on a large series; Raster answers from its own index.
|
|
189
|
+
if (source.name === 'indexer' && tokens.length > 10) {
|
|
145
190
|
console.log(chalk_1.default.dim(' First-time tokens can take the indexer a while to warm; use `--limit 5` for a faster first pass.'));
|
|
146
191
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
contractAddress: t.contract,
|
|
154
|
-
tokenId: t.tokenId,
|
|
155
|
-
})), undefined,
|
|
156
|
-
// Unconditional: suppressing the done===total call meant a one-batch
|
|
157
|
-
// index (including the recommended --limit 5 warm-up) printed nothing
|
|
158
|
-
// and multi-batch runs never showed completion. The final N/N line IS
|
|
159
|
-
// the "indexing finished" signal.
|
|
160
|
-
(done, total) => {
|
|
161
|
-
console.log(chalk_1.default.dim(` ${done}/${total} tokens indexed...`));
|
|
162
|
-
});
|
|
163
|
-
// FF-indexer bypass for Raster-minted tokens. The indexer doesn't carry
|
|
164
|
-
// Raster's tokens, so it returns nothing — but Raster handed us each
|
|
165
|
-
// token's playable `media.contentUrl`. Build DP-1 items straight from
|
|
166
|
-
// that (off-chain provenance, same shape as the Neort path), so the
|
|
167
|
-
// series still builds into a signable, verifiable playlist instead of
|
|
168
|
-
// failing the way it did before. The indexer is tried first so tokens
|
|
169
|
-
// it *can* resolve keep their richer on-chain provenance.
|
|
170
|
-
if (!Array.isArray(items) || items.length === 0) {
|
|
171
|
-
const rasterItems = buildRasterMediaItems(tokens, target);
|
|
172
|
-
if (rasterItems.length > 0) {
|
|
173
|
-
console.log(chalk_1.default.dim(` FF indexer returned nothing; building ${rasterItems.length} item${rasterItems.length === 1 ? '' : 's'} directly from Raster media.`));
|
|
174
|
-
items = rasterItems;
|
|
192
|
+
const results = await resolveBuildTokens(source, tokens);
|
|
193
|
+
const items = (0, token_source_1.itemsOf)(results);
|
|
194
|
+
if (items.length === 0) {
|
|
195
|
+
console.error(chalk_1.default.red(`No tokens could be resolved via ${label}; cannot build playlist.`));
|
|
196
|
+
for (const message of (0, token_source_1.failureMessages)(results)) {
|
|
197
|
+
console.error(chalk_1.default.dim(` ${message}`));
|
|
175
198
|
}
|
|
176
|
-
}
|
|
177
|
-
if (!Array.isArray(items) || items.length === 0) {
|
|
178
|
-
console.error(chalk_1.default.red('No tokens could be indexed; cannot build playlist.'));
|
|
179
|
-
console.error(chalk_1.default.dim(' The FF indexer may be under load when warming a series of previously-unseen tokens.'));
|
|
180
|
-
console.error(chalk_1.default.dim(' Try `--limit 5` first to warm the cache, then re-run without --limit.'));
|
|
181
199
|
process.exit(1);
|
|
182
200
|
}
|
|
183
201
|
const title = playlistTitleFor(target, items);
|
|
@@ -188,7 +206,10 @@ async function runResolvedTarget(target, options) {
|
|
|
188
206
|
console.log(chalk_1.default.dim(` ${items.length} item${items.length === 1 ? '' : 's'}`));
|
|
189
207
|
const dropped = tokens.length - items.length;
|
|
190
208
|
if (dropped > 0) {
|
|
191
|
-
console.log(chalk_1.default.dim(` ${dropped} token${dropped === 1 ? '' : 's'}
|
|
209
|
+
console.log(chalk_1.default.dim(` ${dropped} token${dropped === 1 ? '' : 's'} could not be resolved:`));
|
|
210
|
+
for (const message of (0, token_source_1.failureMessages)(results)) {
|
|
211
|
+
console.log(chalk_1.default.dim(` ${message}`));
|
|
212
|
+
}
|
|
192
213
|
}
|
|
193
214
|
console.log();
|
|
194
215
|
const actions = await decideActions(options);
|
|
@@ -279,6 +300,11 @@ async function resolveTarget(input, parsed, skipPrompt, limitOption) {
|
|
|
279
300
|
if (catalog.length === 0) {
|
|
280
301
|
throw new Error('Artist has no artworks indexed on Raster.');
|
|
281
302
|
}
|
|
303
|
+
// A wallet address reads naturally as "what this wallet owns", and this
|
|
304
|
+
// path builds something else: one work by the artist at the address. Say
|
|
305
|
+
// so every time, so neither a person nor an agent mistakes the result.
|
|
306
|
+
console.log(chalk_1.default.dim('find on an address displays one work by the artist at that address. ' +
|
|
307
|
+
'For what the wallet owns, use build with a query_address requirement (ff-cli build --help).'));
|
|
282
308
|
const picked = await pickFromCatalog(catalog, skipPrompt);
|
|
283
309
|
return {
|
|
284
310
|
kind: 'series',
|
|
@@ -318,11 +344,11 @@ async function resolveTokenListInput(input, limit, fallbackTitle = 'Resolved sou
|
|
|
318
344
|
}
|
|
319
345
|
/**
|
|
320
346
|
* Resolve on-chain coords to a target. If Raster doesn't index this token,
|
|
321
|
-
* fall back to single-token mode so we still build something
|
|
347
|
+
* fall back to single-token mode so we still build something to display.
|
|
322
348
|
*
|
|
323
349
|
* Raster being unreachable gets the same fallback: it only enriches the find
|
|
324
350
|
* (series enumeration, artist labels) — the coords in hand are enough to
|
|
325
|
-
* build a
|
|
351
|
+
* build a single-token playlist to display, and a network blip on one optional
|
|
326
352
|
* dependency must not kill the whole command (#97).
|
|
327
353
|
*/
|
|
328
354
|
async function resolveCoords(coords) {
|
|
@@ -345,7 +371,7 @@ async function resolveCoords(coords) {
|
|
|
345
371
|
}
|
|
346
372
|
/**
|
|
347
373
|
* Build the playlist title. For series, "Artist — Series Title". For a
|
|
348
|
-
* one-item fallback, use the
|
|
374
|
+
* one-item fallback, use the token source's name (already in the item)
|
|
349
375
|
* with a fall-through to coords if name is missing.
|
|
350
376
|
*/
|
|
351
377
|
function playlistTitleFor(target, items) {
|
|
@@ -359,30 +385,30 @@ function playlistTitleFor(target, items) {
|
|
|
359
385
|
return items[0]?.title ?? `Token ${target.coords.tokenId}`;
|
|
360
386
|
}
|
|
361
387
|
/**
|
|
362
|
-
*
|
|
363
|
-
* indexer. Used as a fallback when the indexer resolves nothing — tokens
|
|
364
|
-
* without a Raster `mediaUrl` are dropped (nothing playable to point at).
|
|
388
|
+
* resolveBuildTokens turns the tokens to build into results, one per token.
|
|
365
389
|
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
390
|
+
* When every token already carries its Raster row (a Raster series page) and
|
|
391
|
+
* Raster is the token source, the rows are mapped directly: the page already
|
|
392
|
+
* paid for every field, so asking again would double the requests. Any other
|
|
393
|
+
* mix goes through the token source by coordinate.
|
|
370
394
|
*/
|
|
371
|
-
function
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
395
|
+
async function resolveBuildTokens(source, tokens) {
|
|
396
|
+
const rows = tokens.map((t) => t.rasterToken);
|
|
397
|
+
if (source.name === 'raster' && rows.every((row) => row !== null)) {
|
|
398
|
+
const entries = tokens.map((t, i) => ({ token: rows[i], requested: coordinateOf(t) }));
|
|
399
|
+
return (0, token_source_raster_1.itemsFromRasterTokens)(entries, (0, config_1.getTokenSourceConfig)().ipfsGateways);
|
|
400
|
+
}
|
|
401
|
+
// Unconditional progress: the final N/N line IS the "resolution finished"
|
|
402
|
+
// signal, including for a single-batch run.
|
|
403
|
+
return source.resolveTokens(tokens.map(coordinateOf), {
|
|
404
|
+
onProgress: (done, total) => {
|
|
405
|
+
console.log(chalk_1.default.dim(` ${done}/${total} tokens resolved...`));
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
/** coordinateOf converts a find token to the token source's coordinate shape. */
|
|
410
|
+
function coordinateOf(token) {
|
|
411
|
+
return { chain: token.chain, contractAddress: token.contract, tokenId: token.tokenId };
|
|
386
412
|
}
|
|
387
413
|
function parseLimitOption(limitStr) {
|
|
388
414
|
if (limitStr === undefined) {
|
|
@@ -425,8 +451,7 @@ async function fetchTokens(summary, limit) {
|
|
|
425
451
|
chain: t.chain,
|
|
426
452
|
contract: t.contractAddress,
|
|
427
453
|
tokenId: t.tokenId,
|
|
428
|
-
|
|
429
|
-
mediaType: t.mediaType,
|
|
454
|
+
rasterToken: t.token,
|
|
430
455
|
});
|
|
431
456
|
}
|
|
432
457
|
if (hasMore || page.nextCursor === null) {
|
|
@@ -439,7 +464,7 @@ async function fetchTokens(summary, limit) {
|
|
|
439
464
|
cursor = page.nextCursor;
|
|
440
465
|
}
|
|
441
466
|
if (skipped > 0) {
|
|
442
|
-
console.log(chalk_1.default.yellow(` Skipped ${skipped} token${skipped === 1 ? '' : 's'} on unsupported chains (
|
|
467
|
+
console.log(chalk_1.default.yellow(` Skipped ${skipped} token${skipped === 1 ? '' : 's'} on unsupported chains (ff-cli builds Ethereum and Tezos mainnet only).`));
|
|
443
468
|
}
|
|
444
469
|
return { tokens: collected, hasMore };
|
|
445
470
|
}
|
|
@@ -452,7 +477,7 @@ async function pickFromCatalog(catalog, skipPrompt) {
|
|
|
452
477
|
console.log(chalk_1.default.dim(` ${i}: ${row.title}`));
|
|
453
478
|
});
|
|
454
479
|
console.log();
|
|
455
|
-
const prompt = (0, prompt_1.createPrompt)();
|
|
480
|
+
const prompt = (0, prompt_1.createPrompt)(FIND_NON_INTERACTIVE_HINT);
|
|
456
481
|
const answer = await prompt.ask(`Pick one (0-${catalog.length - 1}, default 0): `);
|
|
457
482
|
prompt.close();
|
|
458
483
|
console.log();
|
|
@@ -467,7 +492,7 @@ async function pickFromCatalog(catalog, skipPrompt) {
|
|
|
467
492
|
async function confirmMakePlaylist(count, hasMore) {
|
|
468
493
|
const noun = count === 1 ? 'token' : 'tokens';
|
|
469
494
|
const label = hasMore ? `the first ${count} ${noun}` : `${count} ${noun}`;
|
|
470
|
-
const prompt = (0, prompt_1.createPrompt)();
|
|
495
|
+
const prompt = (0, prompt_1.createPrompt)(FIND_NON_INTERACTIVE_HINT);
|
|
471
496
|
const yes = await (0, prompt_1.promptYesNo)(prompt.ask, `Build playlist with ${label}?`, true);
|
|
472
497
|
prompt.close();
|
|
473
498
|
console.log();
|
|
@@ -476,20 +501,23 @@ async function confirmMakePlaylist(count, hasMore) {
|
|
|
476
501
|
/**
|
|
477
502
|
* Decide which post-build actions to run.
|
|
478
503
|
*
|
|
479
|
-
* Precedence: explicit flags (--
|
|
480
|
-
* flag is set, fall back to --yes default (
|
|
481
|
-
* prompt. Save is unconditional and already done before this is called.
|
|
504
|
+
* Precedence: explicit flags (--display / --publish) win and may combine. If
|
|
505
|
+
* no flag is set, fall back to the --yes default (display) or an interactive
|
|
506
|
+
* 3-way prompt. Save is unconditional and already done before this is called.
|
|
482
507
|
*
|
|
483
|
-
* `--no-
|
|
484
|
-
* casting from every path, including the `--yes` default and the
|
|
485
|
-
* prompt. It is additive: `--yes` alone keeps its deliberate
|
|
486
|
-
* default for scripted use" of
|
|
487
|
-
* `--no-
|
|
508
|
+
* `--no-display` (commander sets `display === false`) is an opt-out that
|
|
509
|
+
* removes casting from every path, including the `--yes` default and the
|
|
510
|
+
* interactive prompt. It is additive: `--yes` alone keeps its deliberate
|
|
511
|
+
* "max-satisfaction default for scripted use" of displaying, and this must not
|
|
512
|
+
* change that. Because `--no-display` already states the user's intent, it
|
|
513
|
+
* never opens the prompt. The hidden `--play` / `--no-play` spellings mean the
|
|
514
|
+
* same (displayIntent). The internal action keeps its name, 'play'.
|
|
488
515
|
*/
|
|
489
516
|
async function decideActions(options) {
|
|
490
517
|
const flagActions = [];
|
|
491
|
-
const
|
|
492
|
-
|
|
518
|
+
const display = displayIntent(options);
|
|
519
|
+
const noDisplay = display === false;
|
|
520
|
+
if (display) {
|
|
493
521
|
flagActions.push('play');
|
|
494
522
|
}
|
|
495
523
|
if (options.publish) {
|
|
@@ -499,24 +527,34 @@ async function decideActions(options) {
|
|
|
499
527
|
return flagActions;
|
|
500
528
|
}
|
|
501
529
|
// --output alone (with or without --yes) means "save mode" — the save was the action.
|
|
502
|
-
// --no-
|
|
503
|
-
if (options.output ||
|
|
530
|
+
// --no-display likewise means the save was the action; never prompt or default to display.
|
|
531
|
+
if (options.output || noDisplay) {
|
|
504
532
|
return [];
|
|
505
533
|
}
|
|
506
|
-
// --yes without --output defaults to
|
|
534
|
+
// --yes without --output defaults to display (max-satisfaction default for scripted use).
|
|
507
535
|
if (options.yes) {
|
|
508
536
|
return ['play'];
|
|
509
537
|
}
|
|
510
538
|
return [await promptNextAction()];
|
|
511
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* promptNextAction asks what to do with a saved playlist when no flag said so.
|
|
542
|
+
*
|
|
543
|
+
* `p` and `play` stay accepted after the rename to display: a script that
|
|
544
|
+
* pipes its answer in must keep getting the same action.
|
|
545
|
+
*
|
|
546
|
+
* @returns {Promise<PostBuildAction>} The chosen action
|
|
547
|
+
* @throws {Error} On an answer that names no action
|
|
548
|
+
*/
|
|
512
549
|
async function promptNextAction() {
|
|
513
|
-
const prompt = (0, prompt_1.createPrompt)();
|
|
514
|
-
const answer = (await prompt.ask('Next? [
|
|
550
|
+
const prompt = (0, prompt_1.createPrompt)('The playlist is saved. Pass --display to cast it, or --publish to publish it.');
|
|
551
|
+
const answer = (await prompt.ask('Next? [D]isplay on FF1 / just [S]ave / pub[L]ish to feed (default: Display): '))
|
|
515
552
|
.trim()
|
|
516
553
|
.toLowerCase();
|
|
517
554
|
prompt.close();
|
|
518
555
|
console.log();
|
|
519
|
-
|
|
556
|
+
const displayAnswers = ['', 'd', 'display', 'p', 'play', 'y', 'yes'];
|
|
557
|
+
if (displayAnswers.includes(answer)) {
|
|
520
558
|
return 'play';
|
|
521
559
|
}
|
|
522
560
|
if (answer === 's' || answer === 'save' || answer === 'n' || answer === 'no') {
|
|
@@ -525,10 +563,10 @@ async function promptNextAction() {
|
|
|
525
563
|
if (answer === 'l' || answer === 'pub' || answer === 'publish') {
|
|
526
564
|
return 'publish';
|
|
527
565
|
}
|
|
528
|
-
throw new Error(`Unrecognized choice: "${answer}". Use
|
|
566
|
+
throw new Error(`Unrecognized choice: "${answer}". Use D, S, or L.`);
|
|
529
567
|
}
|
|
530
568
|
async function doPlay(playlist, deviceName, skipVerify) {
|
|
531
|
-
// Same verify → send gate `ff-cli
|
|
569
|
+
// Same verify → send gate `ff-cli display` uses, via the shared castPlaylist
|
|
532
570
|
// helper. `find`'s playlists are already signed by buildDP1Playlist (when a
|
|
533
571
|
// key is configured), so no signing is requested here — an unsigned one
|
|
534
572
|
// fails the verify gate with an actionable message instead of reaching the
|
|
@@ -549,7 +587,7 @@ async function doPlay(playlist, deviceName, skipVerify) {
|
|
|
549
587
|
'or pass --skip-verify to bypass.'));
|
|
550
588
|
}
|
|
551
589
|
else {
|
|
552
|
-
console.error(chalk_1.default.red('
|
|
590
|
+
console.error(chalk_1.default.red('Display failed:'), result.error);
|
|
553
591
|
if (result.deviceDetails) {
|
|
554
592
|
console.error(chalk_1.default.dim(` Details: ${result.deviceDetails}`));
|
|
555
593
|
}
|
|
@@ -560,8 +598,8 @@ async function doPlay(playlist, deviceName, skipVerify) {
|
|
|
560
598
|
if (result.verified) {
|
|
561
599
|
console.log(chalk_1.default.green('✓ Verified\n'));
|
|
562
600
|
}
|
|
563
|
-
console.log(chalk_1.default.blue('
|
|
564
|
-
console.log(chalk_1.default.green('✓
|
|
601
|
+
console.log(chalk_1.default.blue('Display on FF1'));
|
|
602
|
+
console.log(chalk_1.default.green('✓ Displaying'));
|
|
565
603
|
if (result.deviceName) {
|
|
566
604
|
console.log(chalk_1.default.dim(` Device: ${result.deviceName}`));
|
|
567
605
|
}
|
|
@@ -610,7 +648,7 @@ async function doPublish(savedPath, serverArg, nonInteractive) {
|
|
|
610
648
|
}
|
|
611
649
|
/**
|
|
612
650
|
* Neort flow. Off-chain platform with no on-chain coords, so we bypass
|
|
613
|
-
*
|
|
651
|
+
* the token source and construct a single-item DP-1 playlist directly
|
|
614
652
|
* from Neort's API response using the off-chain `provenance.offChainURI`
|
|
615
653
|
* shape produced by `buildUrlItem`.
|
|
616
654
|
*/
|