Skip to content

Commit 97ada7d

Browse files
committed
feat: prompt dataset selection on ambiguous --data-set-metadata match
1 parent 10b5dbe commit 97ada7d

4 files changed

Lines changed: 75 additions & 15 deletions

File tree

src/add/add.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import pino from 'pino'
1313
import { CliFatal, isCliFatal } from '../common/cli-errors.js'
1414
import { DEVNET_CHAIN_ID } from '../common/get-rpc-url.js'
1515
import { describeLockupShortfall } from '../common/lockup-error.js'
16-
import { displayUploadResults, performAutoFunding, performUpload, validatePaymentSetup } from '../common/upload-flow.js'
16+
import { displayUploadResults, performAutoFunding, performUpload, promptDataSetSelection, validatePaymentSetup } from '../common/upload-flow.js'
1717
import { carInputError, INPUT_IS_CAR, isCar } from '../core/car/index.js'
1818
import { resolveDataSetIdsByMetadata } from '../core/data-set/index.js'
1919
import { normalizeMetadataConfig, withDerivedNameMetadata } from '../core/metadata/index.js'
@@ -197,11 +197,9 @@ export async function runAdd(options: AddOptions): Promise<AddResult> {
197197
`${pc.green('✓')} Matched existing data sets ${resolution.dataSetIds.join(', ')} via metadata filter`
198198
)
199199
} else if (resolution.kind === 'too-many-matches') {
200-
spinner.stop(`${pc.red('✗')} --data-set-metadata matched too many data sets`)
201-
throw new Error(
202-
`--data-set-metadata matched ${resolution.matchedIds.length} data sets (${resolution.matchedIds.join(', ')}) ` +
203-
`but expected ${resolution.expected} (narrow the filter or pass --data-set-id to pin the target).`
204-
)
200+
const chosenIds = await promptDataSetSelection(resolution.matchedDataSets, resolution.expected, spinner)
201+
contextSelection.dataSetIds = chosenIds
202+
effectiveDataSetMetadata = undefined
205203
} else if (resolution.kind === 'too-few-matches') {
206204
spinner.stop(`${pc.red('✗')} --data-set-metadata matched too few data sets`)
207205
throw new Error(

src/common/upload-flow.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
* including payment validation, storage context creation, and result display.
66
*/
77

8+
import { isCancel, multiselect } from '@clack/prompts'
89
import type { CopyResult, FailedAttempt, Synapse } from '@filoz/synapse-sdk'
910
import type { CID } from 'multiformats/cid'
1011
import pc from 'picocolors'
1112
import type { Logger } from 'pino'
13+
import type { DataSetSummary } from '../core/data-set/types.js'
1214
import { DEFAULT_LOCKUP_DAYS, type PaymentCapacityCheck } from '../core/payments/index.js'
1315
import {
1416
checkUploadReadiness,
@@ -21,11 +23,72 @@ import { formatUSDFC } from '../core/utils/format.js'
2123
import { autoFund } from '../payments/fund.js'
2224
import type { AutoFundOptions } from '../payments/types.js'
2325
import 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'
2527
import { log } from '../utils/cli-logger.js'
2628
import { createSpinnerFlow } from '../utils/multi-operation-spinner.js'
2729
import { 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+
2992
export interface UploadFlowOptions {
3093
/**
3194
* Context identifier for logging (e.g., 'import', 'add')

src/core/data-set/resolve-by-metadata.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { Synapse } from '@filoz/synapse-sdk'
22
import type { Logger } from 'pino'
33
import { listDataSets } from './list-data-sets.js'
4+
import type { DataSetSummary } from './types.js'
45

56
export type MetadataResolution =
67
| { kind: 'no-match' }
78
| { kind: 'matched'; dataSetIds: bigint[] }
8-
| { kind: 'too-many-matches'; matchedIds: bigint[]; expected: number }
9+
| { kind: 'too-many-matches'; matchedIds: bigint[]; matchedDataSets: DataSetSummary[]; expected: number }
910
| { kind: 'too-few-matches'; matchedIds: bigint[]; expected: number }
1011

1112
export interface ResolveByMetadataOptions {
@@ -58,7 +59,7 @@ export async function resolveDataSetIdsByMetadata(
5859
const matchedIds = matched.map((d) => d.dataSetId)
5960

6061
if (matched.length > options.expectedCopies) {
61-
return { kind: 'too-many-matches', matchedIds, expected: options.expectedCopies }
62+
return { kind: 'too-many-matches', matchedIds, matchedDataSets: matched, expected: options.expectedCopies }
6263
}
6364

6465
if (matched.length < options.expectedCopies) {

src/import/import.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import pino from 'pino'
1515
import { CliFatal, isCliFatal } from '../common/cli-errors.js'
1616
import { DEVNET_CHAIN_ID } from '../common/get-rpc-url.js'
1717
import { describeLockupShortfall } from '../common/lockup-error.js'
18-
import { displayUploadResults, performAutoFunding, performUpload, validatePaymentSetup } from '../common/upload-flow.js'
18+
import { displayUploadResults, performAutoFunding, performUpload, promptDataSetSelection, validatePaymentSetup } from '../common/upload-flow.js'
1919
import { resolveDataSetIdsByMetadata } from '../core/data-set/index.js'
2020
import { normalizeMetadataConfig } from '../core/metadata/index.js'
2121
import { DEFAULT_COPIES } from '../core/synapse/constants.js'
@@ -264,11 +264,9 @@ export async function runCarImport(options: ImportOptions): Promise<ImportResult
264264
`${pc.green('✓')} Matched existing data sets ${resolution.dataSetIds.join(', ')} via metadata filter`
265265
)
266266
} else if (resolution.kind === 'too-many-matches') {
267-
spinner.stop(`${pc.red('✗')} --data-set-metadata matched too many data sets`)
268-
throw new Error(
269-
`--data-set-metadata matched ${resolution.matchedIds.length} data sets (${resolution.matchedIds.join(', ')}) ` +
270-
`but expected ${resolution.expected} (narrow the filter or pass --data-set-id to pin the target).`
271-
)
267+
const chosenIds = await promptDataSetSelection(resolution.matchedDataSets, resolution.expected, spinner)
268+
contextSelection.dataSetIds = chosenIds
269+
effectiveDataSetMetadata = undefined
272270
} else if (resolution.kind === 'too-few-matches') {
273271
spinner.stop(`${pc.red('✗')} --data-set-metadata matched too few data sets`)
274272
throw new Error(

0 commit comments

Comments
 (0)