@sentio/cli 3.5.1-rc.3 → 3.6.0-rc.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentio/cli",
3
- "version": "3.5.1-rc.3",
3
+ "version": "3.6.0-rc.2",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -7,6 +7,7 @@ import {
7
7
  createApiContext,
8
8
  handleCommandError,
9
9
  loadJsonInput,
10
+ postApiJson,
10
11
  resolveProjectRef,
11
12
  unwrapApiResult
12
13
  } from '../api.js'
@@ -30,6 +31,12 @@ interface DashboardImportOptions extends DashboardOptions {
30
31
  overrideLayouts?: boolean
31
32
  }
32
33
 
34
+ interface DashboardCreateOptions extends DashboardOptions {
35
+ title?: string
36
+ file?: string
37
+ stdin?: boolean
38
+ }
39
+
33
40
  interface AddPanelOptions extends DashboardOptions {
34
41
  panelName?: string
35
42
  type?: string
@@ -48,6 +55,7 @@ interface AddPanelOptions extends DashboardOptions {
48
55
  export function createDashboardCommand() {
49
56
  const dashboardCommand = new Command('dashboard').description('Manage Sentio dashboards')
50
57
  dashboardCommand.addCommand(createDashboardListCommand())
58
+ dashboardCommand.addCommand(createDashboardCreateCommand())
51
59
  dashboardCommand.addCommand(createDashboardExportCommand())
52
60
  dashboardCommand.addCommand(createDashboardImportCommand())
53
61
  dashboardCommand.addCommand(createDashboardAddPanelCommand())
@@ -68,6 +76,23 @@ function createDashboardListCommand() {
68
76
  })
69
77
  }
70
78
 
79
+ function createDashboardCreateCommand() {
80
+ return withOutputOptions(
81
+ withSharedProjectOptions(withAuthOptions(new Command('create').description('Create a dashboard for a project')))
82
+ )
83
+ .showHelpAfterError()
84
+ .requiredOption('--title <name>', 'Dashboard title')
85
+ .option('--file <path>', 'Read initial dashboard JSON or YAML from file')
86
+ .option('--stdin', 'Read initial dashboard JSON or YAML from stdin')
87
+ .action(async (options, command) => {
88
+ try {
89
+ await runDashboardCreate(options)
90
+ } catch (error) {
91
+ handleDashboardCommandError(error, command)
92
+ }
93
+ })
94
+ }
95
+
71
96
  function createDashboardExportCommand() {
72
97
  return withOutputOptions(
73
98
  withSharedProjectOptions(
@@ -197,6 +222,18 @@ async function runDashboardList(options: DashboardOptions) {
197
222
  printOutput(options, data)
198
223
  }
199
224
 
225
+ async function runDashboardCreate(options: DashboardCreateOptions) {
226
+ const context = createApiContext(options)
227
+ const project = await resolveProjectRef(options, context, { ownerSlug: true })
228
+ const body = buildDashboardCreateBody(options, project)
229
+ const data = await postApiJson<{ dashboard?: Record<string, unknown> }>('/v1/dashboards', context, body)
230
+
231
+ printOutput(options, {
232
+ message: `Dashboard "${options.title}" created`,
233
+ dashboard: data.dashboard ?? data
234
+ })
235
+ }
236
+
200
237
  async function runDashboardExport(dashboardId: string, options: DashboardOptions) {
201
238
  const context = createApiContext(options)
202
239
  const response = await WebService.exportDashboard({
@@ -229,6 +266,47 @@ async function runDashboardImport(dashboardId: string, options: DashboardImportO
229
266
  printOutput(options, { message: `Dashboard imported into ${dashboardId}`, dashboard: data.dashboard })
230
267
  }
231
268
 
269
+ function buildDashboardCreateBody(options: DashboardCreateOptions, project: { owner: string; slug: string }) {
270
+ const input = loadJsonInput(options)
271
+ const initialDashboard = normalizeDashboardInit(input)
272
+
273
+ return {
274
+ name: options.title,
275
+ projectOwner: project.owner,
276
+ projectSlug: project.slug,
277
+ ...initialDashboard
278
+ }
279
+ }
280
+
281
+ function normalizeDashboardInit(input: unknown) {
282
+ if (input === undefined) {
283
+ return {
284
+ panels: {},
285
+ layouts: {
286
+ responsiveLayouts: {
287
+ lg: { layouts: [] }
288
+ }
289
+ }
290
+ }
291
+ }
292
+
293
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
294
+ throw new CliError('Dashboard initialization data must be a JSON or YAML object.')
295
+ }
296
+
297
+ const dashboard = input as Record<string, unknown>
298
+ return {
299
+ panels: isRecord(dashboard.panels) ? dashboard.panels : {},
300
+ layouts: isRecord(dashboard.layouts)
301
+ ? dashboard.layouts
302
+ : {
303
+ responsiveLayouts: {
304
+ lg: { layouts: [] }
305
+ }
306
+ }
307
+ }
308
+ }
309
+
232
310
  async function runDashboardAddPanel(dashboardId: string, options: AddPanelOptions) {
233
311
  const context = createApiContext(options)
234
312
 
@@ -378,6 +456,10 @@ function collectOption(value: string, previous: string[] = []) {
378
456
  return previous
379
457
  }
380
458
 
459
+ function isRecord(value: unknown): value is Record<string, unknown> {
460
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
461
+ }
462
+
381
463
  function withAuthOptions<T extends Command<any, any, any>>(command: T) {
382
464
  return command
383
465
  .option('--host <host>', 'Override Sentio host')
@@ -404,6 +486,10 @@ function handleDashboardCommandError(error: unknown, command?: Command) {
404
486
  error.message.startsWith('Invalid project ') ||
405
487
  error.message.startsWith('Dashboard ') ||
406
488
  error.message.startsWith('Provide --file or --stdin') ||
489
+ error.message.startsWith('Use either --file or --stdin') ||
490
+ error.message.startsWith('Expected JSON or YAML') ||
491
+ error.message.startsWith('Invalid JSON or YAML') ||
492
+ error.message.startsWith('Dashboard initialization data') ||
407
493
  error.message.startsWith('Provide exactly one data source') ||
408
494
  error.message.startsWith('Use exactly one of --sql') ||
409
495
  error.message.startsWith('Invalid chart type') ||
@@ -337,7 +337,7 @@ async function runProcessorResume(processorId: string | undefined, options: Proc
337
337
  })
338
338
  }
339
339
 
340
- async function runProcessorStop(processorId: string | undefined, options: ProcessorOptions & { yes?: boolean }) {
340
+ export async function runProcessorStop(processorId: string | undefined, options: ProcessorOptions & { yes?: boolean }) {
341
341
  const resolvedProcessorId = await resolveAndConfirmProcessor('stop', processorId, options)
342
342
  if (!resolvedProcessorId) return
343
343
  const context = createApiContext(options)
@@ -0,0 +1,63 @@
1
+ import { Command } from '@commander-js/extra-typings'
2
+ import chalk from 'chalk'
3
+ import { handleCommandError } from '../api.js'
4
+ import { confirm } from './upload.js'
5
+ import { runProcessorStop } from './processor.js'
6
+ import {
7
+ getSentioNetworkConfig,
8
+ resolveNetworkAddresses,
9
+ getWalletFromPrivateKey,
10
+ requirePrivateKey,
11
+ stopProcessorOnChain
12
+ } from '../network.js'
13
+
14
+ export function createStopProcessorCommand() {
15
+ return new Command('stop')
16
+ .description('Stop a processor. Uses Sentio Network contract when --sentio-network is specified.')
17
+ .argument('<processorId>', 'ID of the processor to stop')
18
+ .option('--sentio-network <network>', 'Stop via Sentio Network contract (only "testnet" supported)')
19
+ .option('--host <host>', 'Override Sentio host')
20
+ .option('--api-key <key>', 'Use an explicit API key instead of saved credentials')
21
+ .option('--token <token>', 'Use an explicit bearer token instead of saved credentials')
22
+ .option('--project <owner/slug>', 'Sentio project in <owner>/<slug> format')
23
+ .option('--owner <owner>', 'Sentio project owner')
24
+ .option('--name <name>', 'Sentio project name')
25
+ .option('-y, --yes', 'Bypass confirmation')
26
+ .showHelpAfterError()
27
+ .action(async (processorId, options) => {
28
+ try {
29
+ if (options.sentioNetwork) {
30
+ await runStopProcessorOnChain(processorId, options)
31
+ } else {
32
+ await runProcessorStop(processorId, options)
33
+ }
34
+ } catch (error) {
35
+ handleCommandError(error)
36
+ }
37
+ })
38
+ }
39
+
40
+ async function runStopProcessorOnChain(processorId: string, options: { sentioNetwork?: string; yes?: boolean }) {
41
+ const network = options.sentioNetwork!
42
+ const networkConfig = getSentioNetworkConfig(network)
43
+
44
+ const privateKey = requirePrivateKey()
45
+ const wallet = getWalletFromPrivateKey(privateKey)
46
+
47
+ console.log(chalk.blue(`Wallet address: ${wallet.address}`))
48
+ console.log(chalk.blue('Resolving contract addresses from AddressBook...'))
49
+ const addresses = await resolveNetworkAddresses(networkConfig)
50
+
51
+ if (!options.yes) {
52
+ const confirmed = await confirm(`Stop processor "${processorId}" on Sentio Network ${network}?`)
53
+ if (!confirmed) {
54
+ console.log('Stop cancelled.')
55
+ return
56
+ }
57
+ }
58
+
59
+ await stopProcessorOnChain(networkConfig, addresses, wallet, processorId)
60
+
61
+ console.log()
62
+ console.log(chalk.green(`Processor "${processorId}" stopped on Sentio Network ${network}.`))
63
+ }
@@ -13,6 +13,21 @@ import readline from 'readline'
13
13
  import JSZip from 'jszip'
14
14
  import { UserInfo } from '../../../protos/lib/service/common/protos/common.js'
15
15
  import { CommandOptionsType } from './types.js'
16
+ import {
17
+ getSentioNetworkConfig,
18
+ resolveNetworkAddresses,
19
+ getWalletFromPrivateKey,
20
+ requirePrivateKey,
21
+ checkSTBalance,
22
+ checkBillingBalance,
23
+ uploadToIPFS,
24
+ createProcessorOnChain,
25
+ startProcessorOnChain,
26
+ stopProcessorOnChain,
27
+ confirmNoPlatformUpload,
28
+ getProcessorOnChain,
29
+ deleteProcessorOnChain
30
+ } from '../network.js'
16
31
  import { Auth, DefaultBatchUploader, FileType, IPFSBatchUploader, WalrusBatchUploader } from '../uploader.js'
17
32
  export { type Auth } from '../uploader.js'
18
33
 
@@ -56,16 +71,164 @@ export function createUploadCommand() {
56
71
  '--required-chain-id <chain_id...>',
57
72
  '(Optional) Specify chain IDs required for the Sentio network. This option is only available when --sentio-network is used. If omitted, all chain IDs from the project configuration (contracts or network overrides) will be used.'
58
73
  )
74
+ .option(
75
+ '--no-platform',
76
+ 'Upload processor directly to Sentio Network without platform support. Requires $PRIVATE_KEY env var. Only testnet is supported.'
77
+ )
78
+ .option(
79
+ '--ipfs-put-url <url>',
80
+ 'IPFS upload endpoint (used with --no-platform)',
81
+ 'https://api.sentio.xyz/v1/ipfs/add'
82
+ )
59
83
  .action(async (options, command) => {
60
84
  const processorConfig = loadProcessorConfig(options.path)
61
85
  overrideConfigWithOptions(processorConfig, options)
62
86
  if (options.path) {
63
87
  process.chdir(options.path)
64
88
  }
65
- await runUploadInternal(processorConfig, options, command)
89
+ if (options.platform === false) {
90
+ await runNoPlatformUpload(processorConfig, options)
91
+ } else {
92
+ await runUploadInternal(processorConfig, options, command)
93
+ }
66
94
  })
67
95
  }
68
96
 
97
+ async function runNoPlatformUpload(
98
+ processorConfig: YamlProjectConfig,
99
+ options: CommandOptionsType<typeof createUploadCommand>
100
+ ) {
101
+ // Determine network - default to testnet for --no-platform
102
+ const network = options.sentioNetwork || 'testnet'
103
+ const networkConfig = getSentioNetworkConfig(network)
104
+
105
+ // Step 1: Require PRIVATE_KEY
106
+ const privateKey = requirePrivateKey()
107
+ const wallet = getWalletFromPrivateKey(privateKey)
108
+ const walletAddress = wallet.address
109
+
110
+ // Step 2: Resolve contract addresses via AddressBook
111
+ console.log(chalk.blue('Resolving contract addresses from AddressBook...'))
112
+ const addresses = await resolveNetworkAddresses(networkConfig)
113
+
114
+ // Step 3: Check ST balance and Billing balance, then confirm
115
+ const [stBalance, billingBalance] = await Promise.all([
116
+ checkSTBalance(networkConfig, addresses, walletAddress),
117
+ checkBillingBalance(networkConfig, addresses, walletAddress)
118
+ ])
119
+ const confirmed = await confirmNoPlatformUpload(walletAddress, stBalance, billingBalance, addresses, networkConfig)
120
+ if (!confirmed) {
121
+ console.log('Upload cancelled.')
122
+ process.exit(0)
123
+ }
124
+
125
+ // Step 4: Build processor
126
+ if (!options.skipBuild) {
127
+ await buildProcessor(false, options)
128
+ }
129
+
130
+ const processorFile = path.join(process.cwd(), 'dist/lib.js')
131
+ if (!fs.existsSync(processorFile)) {
132
+ console.error(chalk.red('File not found:', processorFile, "- don't use --skip-build"))
133
+ process.exit(1)
134
+ }
135
+
136
+ const codeBuffer = fs.readFileSync(processorFile)
137
+ const stat = fs.statSync(processorFile)
138
+ console.log('Packed processor file size', Math.floor(stat.size / 1024) + 'K, last modified', stat.mtime)
139
+
140
+ // Step 5: Upload to IPFS
141
+ const ipfsPutUrl = options.ipfsPutUrl || 'https://api.sentio.xyz/v1/ipfs/add'
142
+ console.log(chalk.blue(`Uploading processor to IPFS (${ipfsPutUrl})...`))
143
+ const cid = await uploadToIPFS(codeBuffer, ipfsPutUrl)
144
+ console.log(chalk.green(`IPFS upload complete.`))
145
+ console.log('\t', chalk.blue('CID:'), cid)
146
+
147
+ // Step 6: Collect required chain IDs
148
+ let requiredChainIds = options.requiredChainId || processorConfig.requiredChainIds || []
149
+ if (requiredChainIds.length === 0) {
150
+ const chainIds = new Set<string>()
151
+ if (processorConfig.contracts) {
152
+ for (const contract of processorConfig.contracts) {
153
+ chainIds.add(String(contract.chain))
154
+ }
155
+ }
156
+ if (processorConfig.networkOverrides) {
157
+ for (const override of processorConfig.networkOverrides) {
158
+ chainIds.add(String(override.chain))
159
+ }
160
+ }
161
+ requiredChainIds = Array.from(chainIds)
162
+ }
163
+ if (requiredChainIds.length === 0) {
164
+ console.error(
165
+ chalk.red(
166
+ 'Error: No chain IDs found. Specify --required-chain-id or define contracts/networkOverrides in sentio.yaml.'
167
+ )
168
+ )
169
+ process.exit(1)
170
+ }
171
+
172
+ // Step 7: Derive processor ID from project name
173
+ let processorId = processorConfig.project.replace('/', '_')
174
+ const sdkVersion = getSdkVersion() || 'unknown'
175
+
176
+ // Step 7.5: Check if processor already exists on-chain
177
+ console.log(chalk.blue('Checking if processor already exists on-chain...'))
178
+ const existingProcessor = await getProcessorOnChain(networkConfig, addresses, processorId)
179
+
180
+ if (existingProcessor) {
181
+ const existingOwner = existingProcessor.owner.toLowerCase()
182
+ const currentWallet = wallet.address.toLowerCase()
183
+
184
+ if (existingOwner === currentWallet) {
185
+ // Same owner — prompt to replace
186
+ console.log(chalk.yellow(`Processor "${processorId}" already exists (owned by you).`))
187
+ const shouldReplace = await confirm(`Replace existing processor "${processorId}"?`)
188
+ if (!shouldReplace) {
189
+ console.log('Upload cancelled.')
190
+ process.exit(0)
191
+ }
192
+ // Stop and delete existing processor before re-creating
193
+ await stopProcessorOnChain(networkConfig, addresses, wallet, processorId)
194
+ await deleteProcessorOnChain(networkConfig, addresses, wallet, processorId)
195
+ } else {
196
+ // Different owner — prompt to rename
197
+ console.log(
198
+ chalk.yellow(
199
+ `Processor "${processorId}" already exists and is owned by ${existingProcessor.owner} (not your wallet ${wallet.address}).`
200
+ )
201
+ )
202
+ const randomSuffix = Math.random().toString(36).substring(2, 8)
203
+ const defaultNewId = `${processorId}-${randomSuffix}`
204
+
205
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
206
+ const newId: string = await new Promise((resolve) =>
207
+ rl.question(`Enter a new processor ID [${defaultNewId}]: `, (answer) => {
208
+ rl.close()
209
+ resolve(answer.trim() || defaultNewId)
210
+ })
211
+ )
212
+ processorId = newId
213
+ console.log(chalk.blue(`Using processor ID: ${processorId}`))
214
+ }
215
+ }
216
+
217
+ // Step 8: Create processor on-chain
218
+ await createProcessorOnChain(networkConfig, addresses, wallet, processorId, cid, requiredChainIds, sdkVersion)
219
+
220
+ // Step 9: Start processor on-chain
221
+ await startProcessorOnChain(networkConfig, addresses, wallet, processorId)
222
+
223
+ console.log()
224
+ console.log(chalk.green('=== Upload Complete ==='))
225
+ console.log('\t', chalk.blue('Processor ID:'), processorId)
226
+ console.log('\t', chalk.blue('IPFS CID:'), cid)
227
+ console.log('\t', chalk.blue('Network:'), network)
228
+ const codeHash = createHash('sha256').update(codeBuffer).digest('hex')
229
+ console.log('\t', chalk.blue('sha256:'), codeHash)
230
+ }
231
+
69
232
  function parseCheckpoints(
70
233
  checkpoints: string[] | undefined,
71
234
  continueFrom: number | undefined,
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ import { createPriceCommand } from './commands/price.js'
19
19
  import { createSimulationCommand } from './commands/simulation.js'
20
20
  import { createEndpointCommand } from './commands/endpoint.js'
21
21
  import { createDashboardCommand } from './commands/dashboard.js'
22
+ import { createStopProcessorCommand } from './commands/stop-processor.js'
22
23
  import { enableApiDebug } from './api.js'
23
24
  import { printVersions } from './utils.js'
24
25
 
@@ -52,5 +53,6 @@ program.addCommand(createPriceCommand())
52
53
  program.addCommand(createSimulationCommand())
53
54
  program.addCommand(createEndpointCommand())
54
55
  program.addCommand(createDashboardCommand())
56
+ program.addCommand(createStopProcessorCommand())
55
57
 
56
58
  program.parse()