@sentio/cli 3.4.1 → 3.5.0-rc.1

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/lib/index.js CHANGED
@@ -150515,6 +150515,31 @@ async function postApiJson(apiPath, context, body, query) {
150515
150515
  }
150516
150516
  return await response.json();
150517
150517
  }
150518
+ async function putApiJson(apiPath, context, body, query) {
150519
+ const url5 = getApiUrl(apiPath, context.host);
150520
+ if (query) {
150521
+ for (const [key, value] of Object.entries(query)) {
150522
+ if (value !== void 0) {
150523
+ url5.searchParams.set(key, String(value));
150524
+ }
150525
+ }
150526
+ }
150527
+ const response = await fetch(url5.href, {
150528
+ method: "PUT",
150529
+ headers: {
150530
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {},
150531
+ ...context.headers
150532
+ },
150533
+ body: body !== void 0 ? JSON.stringify(body) : void 0
150534
+ });
150535
+ if (!response.ok) {
150536
+ const text = await response.text();
150537
+ throw new CliError(
150538
+ `Sentio API request failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`
150539
+ );
150540
+ }
150541
+ return await response.json();
150542
+ }
150518
150543
  function parseStructuredInput(content, source, filename) {
150519
150544
  if (!content.trim()) {
150520
150545
  throw new CliError(`Expected JSON or YAML in ${source}, but it was empty.`);
@@ -151913,6 +151938,10 @@ function createProcessorCommand() {
151913
151938
  processorCommand.addCommand(createProcessorStatusCommand());
151914
151939
  processorCommand.addCommand(createProcessorSourceCommand());
151915
151940
  processorCommand.addCommand(createProcessorActivatePendingCommand());
151941
+ processorCommand.addCommand(createProcessorPauseCommand());
151942
+ processorCommand.addCommand(createProcessorResumeCommand());
151943
+ processorCommand.addCommand(createProcessorStopCommand());
151944
+ processorCommand.addCommand(createProcessorLogsCommand());
151916
151945
  return processorCommand;
151917
151946
  }
151918
151947
  function createProcessorStatusCommand() {
@@ -151942,7 +151971,7 @@ function createProcessorActivatePendingCommand() {
151942
151971
  withSharedProjectOptions3(
151943
151972
  withAuthOptions3(new Command("activate-pending").description("Activate the pending version"))
151944
151973
  )
151945
- ).showHelpAfterError().action(async (options, command) => {
151974
+ ).showHelpAfterError().option("-y, --yes", "Bypass confirmation").action(async (options, command) => {
151946
151975
  try {
151947
151976
  await runActivatePending(options);
151948
151977
  } catch (error) {
@@ -151950,6 +151979,50 @@ function createProcessorActivatePendingCommand() {
151950
151979
  }
151951
151980
  });
151952
151981
  }
151982
+ function createProcessorPauseCommand() {
151983
+ return withOutputOptions3(
151984
+ withSharedProjectOptions3(withAuthOptions3(new Command("pause").description("Pause a processor")))
151985
+ ).showHelpAfterError().argument("[processorId]", "ID of the processor").option("--reason <reason>", "Reason for pausing").option("-y, --yes", "Bypass confirmation").action(async (processorId, options, command) => {
151986
+ try {
151987
+ await runProcessorPause(processorId, options);
151988
+ } catch (error) {
151989
+ handleProcessorCommandError(error, command);
151990
+ }
151991
+ });
151992
+ }
151993
+ function createProcessorResumeCommand() {
151994
+ return withOutputOptions3(
151995
+ withSharedProjectOptions3(withAuthOptions3(new Command("resume").description("Resume a processor")))
151996
+ ).showHelpAfterError().argument("[processorId]", "ID of the processor").option("-y, --yes", "Bypass confirmation").action(async (processorId, options, command) => {
151997
+ try {
151998
+ await runProcessorResume(processorId, options);
151999
+ } catch (error) {
152000
+ handleProcessorCommandError(error, command);
152001
+ }
152002
+ });
152003
+ }
152004
+ function createProcessorStopCommand() {
152005
+ return withOutputOptions3(
152006
+ withSharedProjectOptions3(withAuthOptions3(new Command("stop").description("Stop a processor")))
152007
+ ).showHelpAfterError().argument("[processorId]", "ID of the processor").option("-y, --yes", "Bypass confirmation").action(async (processorId, options, command) => {
152008
+ try {
152009
+ await runProcessorStop(processorId, options);
152010
+ } catch (error) {
152011
+ handleProcessorCommandError(error, command);
152012
+ }
152013
+ });
152014
+ }
152015
+ function createProcessorLogsCommand() {
152016
+ return withOutputOptions3(
152017
+ withSharedProjectOptions3(withAuthOptions3(new Command("logs").description("View processor logs")))
152018
+ ).showHelpAfterError().argument("[processorId]", "ID of the processor (defaults to active processor)").option("--limit <count>", "Maximum number of log entries to fetch", parseInteger2, 100).option("-f, --follow", "Poll for new log entries continuously").option("--log-type <type>", "Filter by log type (e.g. execution, system)").option("--level <level>", "Filter by log level: DEBUG, INFO, WARNING, ERROR").option("--query <query>", "Free-text filter query").action(async (processorId, options, command) => {
152019
+ try {
152020
+ await runProcessorLogs(processorId, options);
152021
+ } catch (error) {
152022
+ handleProcessorCommandError(error, command);
152023
+ }
152024
+ });
152025
+ }
151953
152026
  async function runProcessorStatus(options) {
151954
152027
  const context = createApiContext(options);
151955
152028
  const project = await resolveProjectRef(options, context, { ownerSlug: true });
@@ -151998,6 +152071,32 @@ async function runProcessorSource(options) {
151998
152071
  async function runActivatePending(options) {
151999
152072
  const context = createApiContext(options);
152000
152073
  const project = await resolveProjectRef(options, context, { ownerSlug: true });
152074
+ const statusResponse = await import_api5.ProcessorService.getProcessorStatusV2({
152075
+ path: {
152076
+ owner: project.owner,
152077
+ slug: project.slug
152078
+ },
152079
+ query: { version: "ALL" },
152080
+ headers: context.headers
152081
+ });
152082
+ const data = unwrapApiResult(statusResponse);
152083
+ const processors = Array.isArray(data.processors) ? data.processors : [];
152084
+ const activeProcessor = processors.find((p7) => asString3(p7.versionState) === "ACTIVE");
152085
+ const pendingProcessor = processors.find((p7) => asString3(p7.versionState) === "PENDING");
152086
+ if (!pendingProcessor) {
152087
+ throw new CliError(`No pending version found for project ${project.owner}/${project.slug}.`);
152088
+ }
152089
+ if (!options.yes) {
152090
+ let message = `Activate the pending version ${asNumber(pendingProcessor.version)}. Are you sure you want to proceed?`;
152091
+ if (activeProcessor) {
152092
+ message = `Activate the pending version ${asNumber(pendingProcessor.version)} may obsolete the active version ${asNumber(activeProcessor.version)}. Are you sure you want to proceed?`;
152093
+ }
152094
+ const isConfirmed = await confirm(message);
152095
+ if (!isConfirmed) {
152096
+ console.log("Activation cancelled.");
152097
+ return;
152098
+ }
152099
+ }
152001
152100
  const response = await import_api5.ProcessorService.activatePendingVersion({
152002
152101
  path: {
152003
152102
  owner: project.owner,
@@ -152010,6 +152109,168 @@ async function runActivatePending(options) {
152010
152109
  ...unwrapApiResult(response)
152011
152110
  });
152012
152111
  }
152112
+ async function resolveAndConfirmProcessor(actionName, processorId, options) {
152113
+ const context = createApiContext(options);
152114
+ let resolvedProcessorId = processorId;
152115
+ let versionToConfirm = "";
152116
+ if (!resolvedProcessorId) {
152117
+ const project = await resolveProjectRef(options, context, { ownerSlug: true });
152118
+ const statusResponse = await import_api5.ProcessorService.getProcessorStatusV2({
152119
+ path: {
152120
+ owner: project.owner,
152121
+ slug: project.slug
152122
+ },
152123
+ query: { version: "ACTIVE" },
152124
+ headers: context.headers
152125
+ });
152126
+ const data = unwrapApiResult(statusResponse);
152127
+ const processors = Array.isArray(data.processors) ? data.processors : [];
152128
+ const activeProcessor = processors.find((p7) => asString3(p7.versionState) === "ACTIVE");
152129
+ if (!activeProcessor || !activeProcessor.processorId) {
152130
+ throw new CliError(
152131
+ `No active processor found for project ${project.owner}/${project.slug}. Please specify a processorId.`
152132
+ );
152133
+ }
152134
+ resolvedProcessorId = asString3(activeProcessor.processorId);
152135
+ versionToConfirm = `version ${asNumber(activeProcessor.version)} of project ${project.owner}/${project.slug}`;
152136
+ } else {
152137
+ versionToConfirm = `processor ${resolvedProcessorId}`;
152138
+ }
152139
+ if (!options.yes) {
152140
+ const isConfirmed = await confirm(`Are you sure you want to ${actionName} ${versionToConfirm}?`);
152141
+ if (!isConfirmed) {
152142
+ console.log(`${actionName.charAt(0).toUpperCase() + actionName.slice(1)} cancelled.`);
152143
+ return void 0;
152144
+ }
152145
+ }
152146
+ return resolvedProcessorId;
152147
+ }
152148
+ async function runProcessorPause(processorId, options) {
152149
+ const resolvedProcessorId = await resolveAndConfirmProcessor("pause", processorId, options);
152150
+ if (!resolvedProcessorId) return;
152151
+ const context = createApiContext(options);
152152
+ const response = await putApiJson(`/api/v1/processors/${resolvedProcessorId}/pause`, context, {
152153
+ reason: options.reason
152154
+ });
152155
+ printOutput3(options, { processorId: resolvedProcessorId, action: "paused", ...response });
152156
+ }
152157
+ async function runProcessorResume(processorId, options) {
152158
+ const resolvedProcessorId = await resolveAndConfirmProcessor("resume", processorId, options);
152159
+ if (!resolvedProcessorId) return;
152160
+ const context = createApiContext(options);
152161
+ const response = await putApiJson(`/api/v1/processors/${resolvedProcessorId}/resume`, context);
152162
+ printOutput3(options, {
152163
+ processorId: resolvedProcessorId,
152164
+ action: "resumed",
152165
+ ...response
152166
+ });
152167
+ }
152168
+ async function runProcessorStop(processorId, options) {
152169
+ const resolvedProcessorId = await resolveAndConfirmProcessor("stop", processorId, options);
152170
+ if (!resolvedProcessorId) return;
152171
+ const context = createApiContext(options);
152172
+ const response = await postApiJson(`/api/v1/processors/stop`, context, { processorId: resolvedProcessorId });
152173
+ printOutput3(options, {
152174
+ processorId: resolvedProcessorId,
152175
+ action: "stopped",
152176
+ ...response
152177
+ });
152178
+ }
152179
+ async function resolveProcessorId(processorId, options) {
152180
+ if (processorId) return processorId;
152181
+ const context = createApiContext(options);
152182
+ const project = await resolveProjectRef(options, context, { ownerSlug: true });
152183
+ const statusResponse = await import_api5.ProcessorService.getProcessorStatusV2({
152184
+ path: { owner: project.owner, slug: project.slug },
152185
+ query: { version: "ACTIVE" },
152186
+ headers: context.headers
152187
+ });
152188
+ const data = unwrapApiResult(statusResponse);
152189
+ const processors = Array.isArray(data.processors) ? data.processors : [];
152190
+ const activeProcessor = processors.find((p7) => asString3(p7.versionState) === "ACTIVE");
152191
+ if (!activeProcessor || !activeProcessor.processorId) {
152192
+ throw new CliError(
152193
+ `No active processor found for project ${project.owner}/${project.slug}. Please specify a processorId.`
152194
+ );
152195
+ }
152196
+ return asString3(activeProcessor.processorId);
152197
+ }
152198
+ async function runProcessorLogs(processorId, options) {
152199
+ const resolvedProcessorId = await resolveProcessorId(processorId, options);
152200
+ const context = createApiContext(options);
152201
+ if (options.follow && !options.json && !options.yaml) {
152202
+ await followProcessorLogs(resolvedProcessorId, context, options);
152203
+ return;
152204
+ }
152205
+ const response = await postApiJson(
152206
+ `/api/v1/processors/${resolvedProcessorId}/logs`,
152207
+ context,
152208
+ buildLogsRequestBody(resolvedProcessorId, options)
152209
+ );
152210
+ printOutput3(options, response);
152211
+ }
152212
+ async function followProcessorLogs(processorId, context, options) {
152213
+ let until;
152214
+ let running = true;
152215
+ const seenIds = /* @__PURE__ */ new Set();
152216
+ process19.on("SIGINT", () => {
152217
+ running = false;
152218
+ });
152219
+ while (running) {
152220
+ try {
152221
+ const body = { ...buildLogsRequestBody(processorId, options), until };
152222
+ const response = await postApiJson(`/api/v1/processors/${processorId}/logs`, context, body);
152223
+ const entries2 = Array.isArray(response.logs) ? response.logs : [];
152224
+ for (const entry of entries2) {
152225
+ const e10 = entry;
152226
+ const id = e10.id ?? "";
152227
+ if (id && seenIds.has(id)) continue;
152228
+ if (id) seenIds.add(id);
152229
+ process19.stdout.write(formatLogEntry(e10) + "\n");
152230
+ }
152231
+ if (response.until) {
152232
+ until = response.until;
152233
+ }
152234
+ } catch {
152235
+ }
152236
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
152237
+ }
152238
+ }
152239
+ function buildLogsRequestBody(processorId, options) {
152240
+ const body = { processorId, limit: options.limit };
152241
+ if (options.logType) {
152242
+ body.logTypeFilters = [options.logType];
152243
+ }
152244
+ if (options.level || options.query) {
152245
+ const parts = [];
152246
+ if (options.level) parts.push(options.level.toUpperCase());
152247
+ if (options.query) parts.push(options.query);
152248
+ body.query = parts.join(" ");
152249
+ }
152250
+ return body;
152251
+ }
152252
+ function formatLogEntry(entry) {
152253
+ const formattedTime = entry.timestamp ? source_default.gray(entry.timestamp.replace("T", " ").replace("Z", "")) : "";
152254
+ const level = (entry.level ?? "INFO").toUpperCase();
152255
+ const logType = entry.logType ? source_default.gray(`[${entry.logType}]`) : "";
152256
+ const coloredLevel = colorSeverity(level);
152257
+ const message = entry.message ?? "";
152258
+ const chain4 = entry.chainId ? source_default.gray(`(chain=${entry.chainId})`) : "";
152259
+ return [formattedTime, coloredLevel, logType, message, chain4].filter(Boolean).join(" ");
152260
+ }
152261
+ function colorSeverity(severity) {
152262
+ switch (severity) {
152263
+ case "ERROR":
152264
+ return source_default.red(`[${severity}]`);
152265
+ case "WARNING":
152266
+ case "WARN":
152267
+ return source_default.yellow(`[${severity}]`);
152268
+ case "DEBUG":
152269
+ return source_default.gray(`[${severity}]`);
152270
+ default:
152271
+ return source_default.cyan(`[${severity}]`);
152272
+ }
152273
+ }
152013
152274
  function withAuthOptions3(command) {
152014
152275
  return command.option("--host <host>", "Override Sentio host").option("--api-key <key>", "Use an explicit API key instead of saved credentials").option("--token <token>", "Use an explicit bearer token instead of saved credentials");
152015
152276
  }
@@ -152053,7 +152314,8 @@ function formatOutput2(data) {
152053
152314
  lines.push(`${group.versionState} (${group.processors.length})`);
152054
152315
  for (const processor of group.processors) {
152055
152316
  const version2 = asNumber(processor.version);
152056
- const statusState = asString3(processor.processorStatus?.state) ?? "UNKNOWN";
152317
+ const processorStatus = processor.processorStatus;
152318
+ const statusState = asString3(processorStatus?.state) ?? "UNKNOWN";
152057
152319
  const uploadedAt = asString3(processor.uploadedAt);
152058
152320
  lines.push(`- v${version2 ?? "?"} status=${statusState}${uploadedAt ? ` uploaded=${uploadedAt}` : ""}`);
152059
152321
  if (asString3(processor.processorId)) {
@@ -152063,9 +152325,17 @@ function formatOutput2(data) {
152063
152325
  for (const stateEntry of states.slice(0, 5)) {
152064
152326
  const state = stateEntry;
152065
152327
  const chainId = asString3(state.chainId) ?? "?";
152066
- const chainState = asString3(state.status?.state) ?? "UNKNOWN";
152328
+ const stateStatus = state.status;
152329
+ const chainState = asString3(stateStatus?.state) ?? "UNKNOWN";
152067
152330
  const block = asString3(state.processedBlockNumber) ?? "?";
152068
152331
  lines.push(` chain ${chainId}: ${chainState} block=${block}`);
152332
+ const errorRecord = stateStatus?.errorRecord;
152333
+ const chainError = asString3(errorRecord?.message);
152334
+ if (chainError) {
152335
+ const createdAt = asString3(errorRecord?.createdAt);
152336
+ const prefix = createdAt ? `[${createdAt}] ` : "";
152337
+ lines.push(` error: ${prefix}${chainError}`);
152338
+ }
152069
152339
  }
152070
152340
  if (states.length > 5) {
152071
152341
  lines.push(` ... ${states.length - 5} more chains`);
@@ -152090,6 +152360,18 @@ function formatOutput2(data) {
152090
152360
  const objectData = data;
152091
152361
  return `Pending processor version activated for ${asString3(objectData.project) ?? "<project>"}.`;
152092
152362
  }
152363
+ if (data && typeof data === "object" && "action" in data && "processorId" in data) {
152364
+ const objectData = data;
152365
+ return `Processor ${asString3(objectData.processorId)} successfully ${asString3(objectData.action)}.`;
152366
+ }
152367
+ if (data && typeof data === "object" && "logs" in data) {
152368
+ const logsData = data;
152369
+ const entries2 = Array.isArray(logsData.logs) ? logsData.logs : [];
152370
+ if (entries2.length === 0) {
152371
+ return "No logs found.";
152372
+ }
152373
+ return entries2.map((entry) => formatLogEntry(entry)).join("\n");
152374
+ }
152093
152375
  return JSON.stringify(data, null, 2);
152094
152376
  }
152095
152377
  function normalizeVersionSelector(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentio/cli",
3
- "version": "3.4.1",
3
+ "version": "3.5.0-rc.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
package/src/api.ts CHANGED
@@ -420,6 +420,37 @@ export async function postApiJson<T>(
420
420
  return (await response.json()) as T
421
421
  }
422
422
 
423
+ export async function putApiJson<T>(
424
+ apiPath: string,
425
+ context: ApiContext,
426
+ body?: unknown,
427
+ query?: Record<string, string | number | boolean | undefined>
428
+ ): Promise<T> {
429
+ const url = getApiUrl(apiPath, context.host)
430
+ if (query) {
431
+ for (const [key, value] of Object.entries(query)) {
432
+ if (value !== undefined) {
433
+ url.searchParams.set(key, String(value))
434
+ }
435
+ }
436
+ }
437
+ const response = await fetch(url.href, {
438
+ method: 'PUT',
439
+ headers: {
440
+ ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
441
+ ...context.headers
442
+ },
443
+ body: body !== undefined ? JSON.stringify(body) : undefined
444
+ })
445
+ if (!response.ok) {
446
+ const text = await response.text()
447
+ throw new CliError(
448
+ `Sentio API request failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ''}`
449
+ )
450
+ }
451
+ return (await response.json()) as T
452
+ }
453
+
423
454
  function parseStructuredInput(content: string, source: string, filename?: string) {
424
455
  if (!content.trim()) {
425
456
  throw new CliError(`Expected JSON or YAML in ${source}, but it was empty.`)
@@ -1,8 +1,18 @@
1
1
  import { ProcessorExtService, ProcessorService } from '@sentio/api'
2
2
  import { Command, InvalidArgumentError } from '@commander-js/extra-typings'
3
+ import chalk from 'chalk'
3
4
  import process from 'process'
4
5
  import yaml from 'yaml'
5
- import { CliError, createApiContext, handleCommandError, resolveProjectRef, unwrapApiResult } from '../api.js'
6
+ import {
7
+ CliError,
8
+ createApiContext,
9
+ handleCommandError,
10
+ resolveProjectRef,
11
+ unwrapApiResult,
12
+ postApiJson,
13
+ putApiJson
14
+ } from '../api.js'
15
+ import { confirm } from './upload.js'
6
16
 
7
17
  interface ProcessorOptions {
8
18
  host?: string
@@ -25,11 +35,23 @@ interface ProcessorSourceOptions extends ProcessorOptions {
25
35
  path?: string
26
36
  }
27
37
 
38
+ interface ProcessorLogsOptions extends ProcessorOptions {
39
+ limit?: number
40
+ follow?: boolean
41
+ logType?: string
42
+ level?: string
43
+ query?: string
44
+ }
45
+
28
46
  export function createProcessorCommand() {
29
47
  const processorCommand = new Command('processor').description('Manage Sentio processor versions')
30
48
  processorCommand.addCommand(createProcessorStatusCommand())
31
49
  processorCommand.addCommand(createProcessorSourceCommand())
32
50
  processorCommand.addCommand(createProcessorActivatePendingCommand())
51
+ processorCommand.addCommand(createProcessorPauseCommand())
52
+ processorCommand.addCommand(createProcessorResumeCommand())
53
+ processorCommand.addCommand(createProcessorStopCommand())
54
+ processorCommand.addCommand(createProcessorLogsCommand())
33
55
  return processorCommand
34
56
  }
35
57
 
@@ -71,6 +93,7 @@ function createProcessorActivatePendingCommand() {
71
93
  )
72
94
  )
73
95
  .showHelpAfterError()
96
+ .option('-y, --yes', 'Bypass confirmation')
74
97
  .action(async (options, command) => {
75
98
  try {
76
99
  await runActivatePending(options)
@@ -80,6 +103,75 @@ function createProcessorActivatePendingCommand() {
80
103
  })
81
104
  }
82
105
 
106
+ function createProcessorPauseCommand() {
107
+ return withOutputOptions(
108
+ withSharedProjectOptions(withAuthOptions(new Command('pause').description('Pause a processor')))
109
+ )
110
+ .showHelpAfterError()
111
+ .argument('[processorId]', 'ID of the processor')
112
+ .option('--reason <reason>', 'Reason for pausing')
113
+ .option('-y, --yes', 'Bypass confirmation')
114
+ .action(async (processorId, options, command) => {
115
+ try {
116
+ await runProcessorPause(processorId, options)
117
+ } catch (error) {
118
+ handleProcessorCommandError(error, command)
119
+ }
120
+ })
121
+ }
122
+
123
+ function createProcessorResumeCommand() {
124
+ return withOutputOptions(
125
+ withSharedProjectOptions(withAuthOptions(new Command('resume').description('Resume a processor')))
126
+ )
127
+ .showHelpAfterError()
128
+ .argument('[processorId]', 'ID of the processor')
129
+ .option('-y, --yes', 'Bypass confirmation')
130
+ .action(async (processorId, options, command) => {
131
+ try {
132
+ await runProcessorResume(processorId, options)
133
+ } catch (error) {
134
+ handleProcessorCommandError(error, command)
135
+ }
136
+ })
137
+ }
138
+
139
+ function createProcessorStopCommand() {
140
+ return withOutputOptions(
141
+ withSharedProjectOptions(withAuthOptions(new Command('stop').description('Stop a processor')))
142
+ )
143
+ .showHelpAfterError()
144
+ .argument('[processorId]', 'ID of the processor')
145
+ .option('-y, --yes', 'Bypass confirmation')
146
+ .action(async (processorId, options, command) => {
147
+ try {
148
+ await runProcessorStop(processorId, options)
149
+ } catch (error) {
150
+ handleProcessorCommandError(error, command)
151
+ }
152
+ })
153
+ }
154
+
155
+ function createProcessorLogsCommand() {
156
+ return withOutputOptions(
157
+ withSharedProjectOptions(withAuthOptions(new Command('logs').description('View processor logs')))
158
+ )
159
+ .showHelpAfterError()
160
+ .argument('[processorId]', 'ID of the processor (defaults to active processor)')
161
+ .option('--limit <count>', 'Maximum number of log entries to fetch', parseInteger, 100)
162
+ .option('-f, --follow', 'Poll for new log entries continuously')
163
+ .option('--log-type <type>', 'Filter by log type (e.g. execution, system)')
164
+ .option('--level <level>', 'Filter by log level: DEBUG, INFO, WARNING, ERROR')
165
+ .option('--query <query>', 'Free-text filter query')
166
+ .action(async (processorId, options, command) => {
167
+ try {
168
+ await runProcessorLogs(processorId, options)
169
+ } catch (error) {
170
+ handleProcessorCommandError(error, command)
171
+ }
172
+ })
173
+ }
174
+
83
175
  async function runProcessorStatus(options: ProcessorStatusOptions) {
84
176
  const context = createApiContext(options)
85
177
  const project = await resolveProjectRef(options, context, { ownerSlug: true })
@@ -127,9 +219,41 @@ async function runProcessorSource(options: ProcessorSourceOptions) {
127
219
  printOutput(options, data)
128
220
  }
129
221
 
130
- async function runActivatePending(options: ProcessorOptions) {
222
+ async function runActivatePending(options: ProcessorOptions & { yes?: boolean }) {
131
223
  const context = createApiContext(options)
132
224
  const project = await resolveProjectRef(options, context, { ownerSlug: true })
225
+
226
+ const statusResponse = await ProcessorService.getProcessorStatusV2({
227
+ path: {
228
+ owner: project.owner,
229
+ slug: project.slug
230
+ },
231
+ query: { version: 'ALL' },
232
+ headers: context.headers
233
+ })
234
+ const data = unwrapApiResult(statusResponse)
235
+ const processors = Array.isArray(data.processors) ? data.processors : []
236
+
237
+ const activeProcessor = processors.find((p) => asString(p.versionState) === 'ACTIVE')
238
+ const pendingProcessor = processors.find((p) => asString(p.versionState) === 'PENDING')
239
+
240
+ if (!pendingProcessor) {
241
+ throw new CliError(`No pending version found for project ${project.owner}/${project.slug}.`)
242
+ }
243
+
244
+ if (!options.yes) {
245
+ let message = `Activate the pending version ${asNumber(pendingProcessor.version)}. Are you sure you want to proceed?`
246
+ if (activeProcessor) {
247
+ message = `Activate the pending version ${asNumber(pendingProcessor.version)} may obsolete the active version ${asNumber(activeProcessor.version)}. Are you sure you want to proceed?`
248
+ }
249
+
250
+ const isConfirmed = await confirm(message)
251
+ if (!isConfirmed) {
252
+ console.log('Activation cancelled.')
253
+ return
254
+ }
255
+ }
256
+
133
257
  const response = await ProcessorService.activatePendingVersion({
134
258
  path: {
135
259
  owner: project.owner,
@@ -143,6 +267,216 @@ async function runActivatePending(options: ProcessorOptions) {
143
267
  })
144
268
  }
145
269
 
270
+ async function resolveAndConfirmProcessor(
271
+ actionName: string,
272
+ processorId: string | undefined,
273
+ options: ProcessorOptions & { yes?: boolean }
274
+ ): Promise<string | undefined> {
275
+ const context = createApiContext(options)
276
+ let resolvedProcessorId = processorId
277
+ let versionToConfirm = ''
278
+
279
+ if (!resolvedProcessorId) {
280
+ const project = await resolveProjectRef(options, context, { ownerSlug: true })
281
+ const statusResponse = await ProcessorService.getProcessorStatusV2({
282
+ path: {
283
+ owner: project.owner,
284
+ slug: project.slug
285
+ },
286
+ query: { version: 'ACTIVE' },
287
+ headers: context.headers
288
+ })
289
+ const data = unwrapApiResult(statusResponse)
290
+ const processors = Array.isArray(data.processors) ? data.processors : []
291
+ const activeProcessor = processors.find((p) => asString(p.versionState) === 'ACTIVE')
292
+
293
+ if (!activeProcessor || !activeProcessor.processorId) {
294
+ throw new CliError(
295
+ `No active processor found for project ${project.owner}/${project.slug}. Please specify a processorId.`
296
+ )
297
+ }
298
+ resolvedProcessorId = asString(activeProcessor.processorId)!
299
+ versionToConfirm = `version ${asNumber(activeProcessor.version)} of project ${project.owner}/${project.slug}`
300
+ } else {
301
+ versionToConfirm = `processor ${resolvedProcessorId}`
302
+ }
303
+
304
+ if (!options.yes) {
305
+ const isConfirmed = await confirm(`Are you sure you want to ${actionName} ${versionToConfirm}?`)
306
+ if (!isConfirmed) {
307
+ console.log(`${actionName.charAt(0).toUpperCase() + actionName.slice(1)} cancelled.`)
308
+ return undefined
309
+ }
310
+ }
311
+
312
+ return resolvedProcessorId
313
+ }
314
+
315
+ async function runProcessorPause(
316
+ processorId: string | undefined,
317
+ options: ProcessorOptions & { reason?: string; yes?: boolean }
318
+ ) {
319
+ const resolvedProcessorId = await resolveAndConfirmProcessor('pause', processorId, options)
320
+ if (!resolvedProcessorId) return
321
+ const context = createApiContext(options)
322
+ const response = await putApiJson(`/api/v1/processors/${resolvedProcessorId}/pause`, context, {
323
+ reason: options.reason
324
+ })
325
+ printOutput(options, { processorId: resolvedProcessorId, action: 'paused', ...(response as Record<string, unknown>) })
326
+ }
327
+
328
+ async function runProcessorResume(processorId: string | undefined, options: ProcessorOptions & { yes?: boolean }) {
329
+ const resolvedProcessorId = await resolveAndConfirmProcessor('resume', processorId, options)
330
+ if (!resolvedProcessorId) return
331
+ const context = createApiContext(options)
332
+ const response = await putApiJson(`/api/v1/processors/${resolvedProcessorId}/resume`, context)
333
+ printOutput(options, {
334
+ processorId: resolvedProcessorId,
335
+ action: 'resumed',
336
+ ...(response as Record<string, unknown>)
337
+ })
338
+ }
339
+
340
+ async function runProcessorStop(processorId: string | undefined, options: ProcessorOptions & { yes?: boolean }) {
341
+ const resolvedProcessorId = await resolveAndConfirmProcessor('stop', processorId, options)
342
+ if (!resolvedProcessorId) return
343
+ const context = createApiContext(options)
344
+ const response = await postApiJson(`/api/v1/processors/stop`, context, { processorId: resolvedProcessorId })
345
+ printOutput(options, {
346
+ processorId: resolvedProcessorId,
347
+ action: 'stopped',
348
+ ...(response as Record<string, unknown>)
349
+ })
350
+ }
351
+
352
+ async function resolveProcessorId(processorId: string | undefined, options: ProcessorOptions): Promise<string> {
353
+ if (processorId) return processorId
354
+ const context = createApiContext(options)
355
+ const project = await resolveProjectRef(options, context, { ownerSlug: true })
356
+ const statusResponse = await ProcessorService.getProcessorStatusV2({
357
+ path: { owner: project.owner, slug: project.slug },
358
+ query: { version: 'ACTIVE' },
359
+ headers: context.headers
360
+ })
361
+ const data = unwrapApiResult(statusResponse)
362
+ const processors = Array.isArray(data.processors) ? data.processors : []
363
+ const activeProcessor = processors.find((p) => asString(p.versionState) === 'ACTIVE')
364
+ if (!activeProcessor || !activeProcessor.processorId) {
365
+ throw new CliError(
366
+ `No active processor found for project ${project.owner}/${project.slug}. Please specify a processorId.`
367
+ )
368
+ }
369
+ return asString(activeProcessor.processorId)!
370
+ }
371
+
372
+ async function runProcessorLogs(processorId: string | undefined, options: ProcessorLogsOptions) {
373
+ const resolvedProcessorId = await resolveProcessorId(processorId, options)
374
+ const context = createApiContext(options)
375
+
376
+ if (options.follow && !options.json && !options.yaml) {
377
+ await followProcessorLogs(resolvedProcessorId, context, options)
378
+ return
379
+ }
380
+
381
+ const response = await postApiJson<ProcessorLogsResponse>(
382
+ `/api/v1/processors/${resolvedProcessorId}/logs`,
383
+ context,
384
+ buildLogsRequestBody(resolvedProcessorId, options)
385
+ )
386
+ printOutput(options, response)
387
+ }
388
+
389
+ async function followProcessorLogs(
390
+ processorId: string,
391
+ context: ReturnType<typeof createApiContext>,
392
+ options: ProcessorLogsOptions
393
+ ) {
394
+ let until: unknown[] | undefined
395
+ let running = true
396
+ const seenIds = new Set<string>()
397
+
398
+ process.on('SIGINT', () => {
399
+ running = false
400
+ })
401
+
402
+ while (running) {
403
+ try {
404
+ const body = { ...buildLogsRequestBody(processorId, options), until }
405
+ const response = await postApiJson<ProcessorLogsResponse>(`/api/v1/processors/${processorId}/logs`, context, body)
406
+ const entries = Array.isArray(response.logs) ? response.logs : []
407
+ for (const entry of entries) {
408
+ const e = entry as ProcessorLog
409
+ const id = e.id ?? ''
410
+ if (id && seenIds.has(id)) continue
411
+ if (id) seenIds.add(id)
412
+ process.stdout.write(formatLogEntry(e) + '\n')
413
+ }
414
+ if (response.until) {
415
+ until = response.until
416
+ }
417
+ } catch {
418
+ // Ignore transient fetch errors during follow mode
419
+ }
420
+ await new Promise((resolve) => setTimeout(resolve, 2000))
421
+ }
422
+ }
423
+
424
+ function buildLogsRequestBody(processorId: string, options: ProcessorLogsOptions): Record<string, unknown> {
425
+ const body: Record<string, unknown> = { processorId, limit: options.limit }
426
+ if (options.logType) {
427
+ body.logTypeFilters = [options.logType]
428
+ }
429
+ if (options.level || options.query) {
430
+ const parts: string[] = []
431
+ if (options.level) parts.push(options.level.toUpperCase())
432
+ if (options.query) parts.push(options.query)
433
+ body.query = parts.join(' ')
434
+ }
435
+ return body
436
+ }
437
+
438
+ interface ProcessorLog {
439
+ id?: string
440
+ message?: string
441
+ timestamp?: string
442
+ attributes?: Record<string, unknown>
443
+ logType?: string
444
+ level?: string
445
+ highlightedMessage?: string
446
+ chainId?: string
447
+ }
448
+
449
+ interface ProcessorLogsResponse {
450
+ logs?: ProcessorLog[]
451
+ until?: unknown[]
452
+ total?: string
453
+ }
454
+
455
+ function formatLogEntry(entry: ProcessorLog): string {
456
+ const formattedTime = entry.timestamp ? chalk.gray(entry.timestamp.replace('T', ' ').replace('Z', '')) : ''
457
+ const level = (entry.level ?? 'INFO').toUpperCase()
458
+ const logType = entry.logType ? chalk.gray(`[${entry.logType}]`) : ''
459
+ const coloredLevel = colorSeverity(level)
460
+ const message = entry.message ?? ''
461
+ const chain = entry.chainId ? chalk.gray(`(chain=${entry.chainId})`) : ''
462
+
463
+ return [formattedTime, coloredLevel, logType, message, chain].filter(Boolean).join(' ')
464
+ }
465
+
466
+ function colorSeverity(severity: string): string {
467
+ switch (severity) {
468
+ case 'ERROR':
469
+ return chalk.red(`[${severity}]`)
470
+ case 'WARNING':
471
+ case 'WARN':
472
+ return chalk.yellow(`[${severity}]`)
473
+ case 'DEBUG':
474
+ return chalk.gray(`[${severity}]`)
475
+ default:
476
+ return chalk.cyan(`[${severity}]`)
477
+ }
478
+ }
479
+
146
480
  function withAuthOptions<T extends Command<any, any, any>>(command: T) {
147
481
  return command
148
482
  .option('--host <host>', 'Override Sentio host')
@@ -204,8 +538,8 @@ function formatOutput(data: unknown) {
204
538
  lines.push(`${group.versionState} (${group.processors.length})`)
205
539
  for (const processor of group.processors) {
206
540
  const version = asNumber(processor.version)
207
- const statusState =
208
- asString((processor.processorStatus as Record<string, unknown> | undefined)?.state) ?? 'UNKNOWN'
541
+ const processorStatus = processor.processorStatus as Record<string, unknown> | undefined
542
+ const statusState = asString(processorStatus?.state) ?? 'UNKNOWN'
209
543
  const uploadedAt = asString(processor.uploadedAt)
210
544
  lines.push(`- v${version ?? '?'} status=${statusState}${uploadedAt ? ` uploaded=${uploadedAt}` : ''}`)
211
545
  if (asString(processor.processorId)) {
@@ -215,9 +549,17 @@ function formatOutput(data: unknown) {
215
549
  for (const stateEntry of states.slice(0, 5)) {
216
550
  const state = stateEntry as Record<string, unknown>
217
551
  const chainId = asString(state.chainId) ?? '?'
218
- const chainState = asString((state.status as Record<string, unknown> | undefined)?.state) ?? 'UNKNOWN'
552
+ const stateStatus = state.status as Record<string, unknown> | undefined
553
+ const chainState = asString(stateStatus?.state) ?? 'UNKNOWN'
219
554
  const block = asString(state.processedBlockNumber) ?? '?'
220
555
  lines.push(` chain ${chainId}: ${chainState} block=${block}`)
556
+ const errorRecord = stateStatus?.errorRecord as Record<string, unknown> | undefined
557
+ const chainError = asString(errorRecord?.message)
558
+ if (chainError) {
559
+ const createdAt = asString(errorRecord?.createdAt)
560
+ const prefix = createdAt ? `[${createdAt}] ` : ''
561
+ lines.push(` error: ${prefix}${chainError}`)
562
+ }
221
563
  }
222
564
  if (states.length > 5) {
223
565
  lines.push(` ... ${states.length - 5} more chains`)
@@ -246,6 +588,25 @@ function formatOutput(data: unknown) {
246
588
  return `Pending processor version activated for ${asString(objectData.project) ?? '<project>'}.`
247
589
  }
248
590
 
591
+ if (
592
+ data &&
593
+ typeof data === 'object' &&
594
+ 'action' in (data as Record<string, unknown>) &&
595
+ 'processorId' in (data as Record<string, unknown>)
596
+ ) {
597
+ const objectData = data as Record<string, unknown>
598
+ return `Processor ${asString(objectData.processorId)} successfully ${asString(objectData.action)}.`
599
+ }
600
+
601
+ if (data && typeof data === 'object' && 'logs' in (data as Record<string, unknown>)) {
602
+ const logsData = data as ProcessorLogsResponse
603
+ const entries = Array.isArray(logsData.logs) ? logsData.logs : []
604
+ if (entries.length === 0) {
605
+ return 'No logs found.'
606
+ }
607
+ return entries.map((entry) => formatLogEntry(entry)).join('\n')
608
+ }
609
+
249
610
  return JSON.stringify(data, null, 2)
250
611
  }
251
612