55 * including payment validation, storage context creation, and result display.
66 */
77
8+ import { isCancel , multiselect } from '@clack/prompts'
89import type { CopyResult , FailedAttempt , Synapse } from '@filoz/synapse-sdk'
910import type { CID } from 'multiformats/cid'
1011import pc from 'picocolors'
1112import type { Logger } from 'pino'
13+ import type { DataSetSummary } from '../core/data-set/types.js'
1214import { DEFAULT_LOCKUP_DAYS , type PaymentCapacityCheck } from '../core/payments/index.js'
1315import {
1416 checkUploadReadiness ,
@@ -21,11 +23,72 @@ import { formatUSDFC } from '../core/utils/format.js'
2123import { autoFund } from '../payments/fund.js'
2224import type { AutoFundOptions } from '../payments/types.js'
2325import type { Spinner } from '../utils/cli-helpers.js'
24- import { cancel , formatFileSize } from '../utils/cli-helpers.js'
26+ import { cancel , formatFileSize , isInteractive } from '../utils/cli-helpers.js'
2527import { log } from '../utils/cli-logger.js'
2628import { createSpinnerFlow } from '../utils/multi-operation-spinner.js'
2729import { CliFatal } from './cli-errors.js'
2830
31+ /**
32+ * Prompt the user to select exactly `expectedCopies` data sets from a list of candidates.
33+ *
34+ * Only called when `--data-set-metadata` matched more datasets than `--copies` requires and
35+ * the process is running in an interactive TTY. In non-interactive contexts the caller must
36+ * throw before reaching here.
37+ *
38+ * Stops the spinner before rendering the Clack prompt (they cannot coexist).
39+ */
40+ export async function promptDataSetSelection (
41+ matchedDataSets : DataSetSummary [ ] ,
42+ expectedCopies : number ,
43+ spinner : Spinner
44+ ) : Promise < bigint [ ] > {
45+ if ( ! isInteractive ( ) ) {
46+ throw new Error (
47+ `--data-set-metadata matched ${ matchedDataSets . length } data sets (${ matchedDataSets . map ( ( d ) => d . dataSetId ) . join ( ', ' ) } ) ` +
48+ `but expected ${ expectedCopies } . Narrow the filter, pass --data-set-ids, or run in a TTY to pick interactively.`
49+ )
50+ }
51+
52+ spinner . stop ( `${ pc . yellow ( '?' ) } --data-set-metadata matched ${ matchedDataSets . length } data sets — select ${ expectedCopies } to upload to` )
53+
54+ const options = matchedDataSets . map ( ( ds ) => {
55+ const spaceName = ds . metadata ?. [ 'space-name' ]
56+ const spaceDid = ds . metadata ?. [ 'space-did' ]
57+ const pieces = Number ( ds . activePieceCount ?? 0n )
58+ const didDisplay = spaceDid ? `${ spaceDid . slice ( 0 , 20 ) } …${ spaceDid . slice ( - 6 ) } ` : undefined
59+
60+ const label = [
61+ `#${ ds . dataSetId } ` ,
62+ spaceName ,
63+ didDisplay ,
64+ `(${ pieces } piece${ pieces !== 1 ? 's' : '' } )` ,
65+ ]
66+ . filter ( Boolean )
67+ . join ( ' ' )
68+
69+ return { value : ds . dataSetId , label }
70+ } )
71+
72+ const chosen = await multiselect < bigint > ( {
73+ message : `Select exactly ${ expectedCopies } data set${ expectedCopies !== 1 ? 's' : '' } :` ,
74+ options,
75+ required : true ,
76+ } )
77+
78+ if ( isCancel ( chosen ) ) {
79+ cancel ( 'Cancelled' )
80+ throw new CliFatal ( 'Dataset selection cancelled' )
81+ }
82+
83+ if ( chosen . length !== expectedCopies ) {
84+ throw new CliFatal (
85+ `Select exactly ${ expectedCopies } data set${ expectedCopies !== 1 ? 's' : '' } — got ${ chosen . length } . Re-run and select the correct number.`
86+ )
87+ }
88+
89+ return chosen
90+ }
91+
2992export interface UploadFlowOptions {
3093 /**
3194 * Context identifier for logging (e.g., 'import', 'add')
0 commit comments