@formigio/fazemos-cli 0.10.8 → 0.10.11

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/dist/api.d.ts CHANGED
@@ -29,6 +29,14 @@ export interface ApiOptions {
29
29
  projectSlug?: string;
30
30
  allProjects?: boolean;
31
31
  noProjectHeader?: boolean;
32
+ /**
33
+ * Extra request headers merged on top of the standard auth/org/project
34
+ * headers. Used by callers that need optimistic-concurrency or other
35
+ * out-of-band headers (e.g., I30 force-transition's `If-Match: <n>` opt-in).
36
+ * Caller-supplied keys win on conflict — this is intentional so a caller
37
+ * can override `Content-Type` if needed.
38
+ */
39
+ headers?: Record<string, string>;
32
40
  }
33
41
  /**
34
42
  * Resolve the project id to send as X-Fazemos-Project-Id on a scoped call.
package/dist/api.js CHANGED
@@ -119,6 +119,14 @@ export async function api(method, path, body, opts = {}) {
119
119
  headers['X-Fazemos-Project-Id'] = projectId;
120
120
  }
121
121
  }
122
+ // I30 — caller-supplied extra headers (e.g., If-Match for opt-in optimistic
123
+ // concurrency on force-transition). Applied last so callers can override
124
+ // anything we set above on purpose.
125
+ if (opts.headers) {
126
+ for (const [k, v] of Object.entries(opts.headers)) {
127
+ headers[k] = v;
128
+ }
129
+ }
122
130
  // Build URL with optional ?view=all append. We don't try to be clever
123
131
  // about merging with existing query strings here — callers that use
124
132
  // --all-projects know they're on GET list endpoints with no other query.
package/dist/api.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -26,6 +26,13 @@ function parseStreamSilenceAbortMs(val) {
26
26
  }
27
27
  return n;
28
28
  }
29
+ function parseEvalMaxTurns(val) {
30
+ const n = Number(val);
31
+ if (!Number.isInteger(n) || n < 8 || n > 30) {
32
+ throw new Error(`eval_max_turns must be an integer between 8 and 30 (got ${val})`);
33
+ }
34
+ return n;
35
+ }
29
36
  function formatMsHuman(ms) {
30
37
  if (ms >= 60000) {
31
38
  const min = ms / 60000;
@@ -2823,6 +2830,9 @@ templates
2823
2830
  else if (step.outputs?.length) {
2824
2831
  console.log(' Inputs: none');
2825
2832
  }
2833
+ if (step.eval_max_turns != null) {
2834
+ console.log(` Eval Max Turns: ${step.eval_max_turns}`);
2835
+ }
2826
2836
  if (step.stream_silence_abort_ms) {
2827
2837
  console.log(` Stream Silence Abort: ${step.stream_silence_abort_ms.toLocaleString()}ms (${formatMsHuman(step.stream_silence_abort_ms)})`);
2828
2838
  }
@@ -3179,6 +3189,7 @@ templates
3179
3189
  .option('--sort-order <n>', 'Set sort_order directly (lower-level escape hatch)', parseNumber)
3180
3190
  .option('--agent-config <json>', 'Per-step agent config overrides as JSON (e.g., \'{"model":"opus","maxBudgetUsd":20}\'). Overrides agent member defaults for: model, maxBudgetUsd, maxTurns, timeoutMs, cwd, repos')
3181
3191
  .option('--stream-silence-abort-ms <ms>', 'Stream silence abort threshold in milliseconds (30000-1800000)', parseStreamSilenceAbortMs)
3192
+ .option('--eval-max-turns <n>', 'Evaluator turn budget for tool-enabled (agent) evals on this step (8-30). Has no effect on script-typed steps (rejected by API).', parseEvalMaxTurns)
3182
3193
  .action(async (templateId, opts) => {
3183
3194
  try {
3184
3195
  if (!VALID_STEP_TYPES.includes(opts.type)) {
@@ -3240,6 +3251,8 @@ templates
3240
3251
  step.agent_config = parseJson(opts.agentConfig, '--agent-config');
3241
3252
  if (opts.streamSilenceAbortMs !== undefined)
3242
3253
  step.stream_silence_abort_ms = opts.streamSilenceAbortMs;
3254
+ if (opts.evalMaxTurns !== undefined)
3255
+ step.eval_max_turns = opts.evalMaxTurns;
3243
3256
  // Positioning: --after or --sort-order
3244
3257
  if (opts.after && opts.sortOrder !== undefined) {
3245
3258
  console.error(chalk.red('Cannot use both --after and --sort-order'));
@@ -3268,6 +3281,9 @@ templates
3268
3281
  if (step.stream_silence_abort_ms) {
3269
3282
  console.log(` Stream Silence Abort: ${step.stream_silence_abort_ms.toLocaleString()}ms (${formatMsHuman(step.stream_silence_abort_ms)})`);
3270
3283
  }
3284
+ if (step.eval_max_turns != null) {
3285
+ console.log(` Eval Max Turns: ${step.eval_max_turns}`);
3286
+ }
3271
3287
  }
3272
3288
  catch (err) {
3273
3289
  console.error(chalk.red(err.message));
@@ -3337,12 +3353,20 @@ templates
3337
3353
  .option('--sections <text>', 'Agent instructions / step content. Use @filepath to load from a file (e.g., --sections @steps/review.md)')
3338
3354
  .option('--agent-config <json>', 'Per-step agent config overrides as JSON (merges with existing)')
3339
3355
  .option('--stream-silence-abort-ms <ms>', 'Stream silence abort threshold in milliseconds (30000-1800000)', parseStreamSilenceAbortMs)
3356
+ .option('--eval-max-turns <n>', 'Evaluator turn budget for tool-enabled (agent) evals on this step (8-30). Mutually exclusive with --clear-eval-max-turns.', parseEvalMaxTurns)
3357
+ .option('--clear-eval-max-turns', 'Remove a previously-set eval_max_turns from this step, reverting to runner default. Mutually exclusive with --eval-max-turns.')
3340
3358
  .action(async (templateId, opts) => {
3341
3359
  try {
3360
+ if (opts.evalMaxTurns !== undefined && opts.clearEvalMaxTurns) {
3361
+ console.error(chalk.red('--eval-max-turns and --clear-eval-max-turns are mutually exclusive'));
3362
+ process.exit(1);
3363
+ return;
3364
+ }
3342
3365
  const hasUpdate = opts.name || opts.type || opts.role || opts.description != null
3343
3366
  || opts.reviewer != null || opts.maxReviewCycles != null || opts.parallelGroup != null
3344
3367
  || opts.image || opts.command || opts.timeout !== undefined || opts.workingDir || opts.env
3345
- || opts.agentConfig || opts.sections != null || opts.streamSilenceAbortMs !== undefined;
3368
+ || opts.agentConfig || opts.sections != null || opts.streamSilenceAbortMs !== undefined
3369
+ || opts.evalMaxTurns !== undefined || opts.clearEvalMaxTurns;
3346
3370
  if (!hasUpdate) {
3347
3371
  console.error(chalk.red('Provide at least one field to update'));
3348
3372
  process.exit(1);
@@ -3415,6 +3439,13 @@ templates
3415
3439
  if (opts.streamSilenceAbortMs !== undefined) {
3416
3440
  step.stream_silence_abort_ms = opts.streamSilenceAbortMs;
3417
3441
  }
3442
+ // eval_max_turns: set or clear (mutually exclusive — checked above)
3443
+ if (opts.evalMaxTurns !== undefined) {
3444
+ step.eval_max_turns = opts.evalMaxTurns;
3445
+ }
3446
+ else if (opts.clearEvalMaxTurns) {
3447
+ delete step.eval_max_turns;
3448
+ }
3418
3449
  await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
3419
3450
  console.log(chalk.green(`Updated step: ${step.name}`));
3420
3451
  }
@@ -4022,18 +4053,114 @@ pipelines
4022
4053
  process.exit(1);
4023
4054
  }
4024
4055
  });
4056
+ // I30 — `pl force-cancel` removed. Replaced by the generalised
4057
+ // `pl force-transition pipeline <id> --to cancelled` admin endpoint, which
4058
+ // covers the same emergency cancel path plus 12 more entity/state combos
4059
+ // across pipelines, steps, and executions. No shim: callers update or 404.
4025
4060
  pipelines
4026
- .command('force-cancel')
4027
- .description('Force-cancel a stuck pipeline instance from any state. Admin/owner only. Bypasses normal state transition rules.')
4028
- .argument('<id>', 'Instance ID')
4029
- .option('-r, --reason <reason>', 'Reason for force-cancel')
4030
- .action(async (id, opts) => {
4031
- try {
4032
- const body = {};
4061
+ .command('force-transition')
4062
+ .description('Force an entity (pipeline, step, or execution) into a target state. Admin / org owner / project member only. ' +
4063
+ 'Bypasses normal state transition rules and fires the appropriate subscriber chain (audit log, ECS stop, ' +
4064
+ 'agent reset, token revoke, etc.). Use only when the normal lifecycle is stuck.')
4065
+ .argument('<entity-type>', 'Entity type: pipeline | step | execution')
4066
+ .argument('<id>', 'Entity ID (pipeline_instance / step_instance / execution UUID)')
4067
+ .requiredOption('--to <state>', 'Target state (e.g., cancelled, failed, completed, in_progress, ...)')
4068
+ .option('-r, --reason <reason>', 'Free-form audit reason (recorded in audit_log)')
4069
+ .option('--output-data-file <path>', 'Path to a JSON file with output_data. Required when forcing a step with a structured output schema to "completed".')
4070
+ .option('--expect-version <n>', 'Optimistic-concurrency guard: send If-Match: "<n>" so the API rejects the call (409) ' +
4071
+ 'if the current entity version differs. Omit to bypass version checking (default).')
4072
+ .option('--json', 'Print the raw API response as JSON (machine-readable)')
4073
+ .action(async (entityType, id, opts) => {
4074
+ try {
4075
+ const valid = ['pipeline', 'step', 'execution'];
4076
+ if (!valid.includes(entityType)) {
4077
+ console.error(chalk.red(`Invalid entity-type "${entityType}". Must be one of: ${valid.join(', ')}`));
4078
+ process.exit(1);
4079
+ }
4080
+ if (!opts.to || typeof opts.to !== 'string' || !opts.to.trim()) {
4081
+ console.error(chalk.red('--to <state> is required'));
4082
+ process.exit(1);
4083
+ }
4084
+ // Body field names match the API contract verbatim:
4085
+ // target_state — required
4086
+ // reason — optional, free-form audit string
4087
+ // output_data — required for step→completed when the step has an
4088
+ // output schema; carrier rejects with 400 OUTPUT_DATA_*
4089
+ // otherwise.
4090
+ const body = { target_state: opts.to };
4033
4091
  if (opts.reason)
4034
4092
  body.reason = opts.reason;
4035
- const data = await api('POST', `/api/pipeline-instances/${id}/force-cancel`, body);
4036
- console.log(chalk.green(`Force-cancelled: ${data.instance?.name || id}`));
4093
+ if (opts.outputDataFile) {
4094
+ if (entityType !== 'step') {
4095
+ console.error(chalk.red('--output-data-file is only valid when entity-type is "step"'));
4096
+ process.exit(1);
4097
+ }
4098
+ let raw;
4099
+ try {
4100
+ raw = readFileSync(resolve(opts.outputDataFile), 'utf-8');
4101
+ }
4102
+ catch (err) {
4103
+ console.error(chalk.red(`Cannot read --output-data-file "${opts.outputDataFile}": ${err.message}`));
4104
+ process.exit(1);
4105
+ return;
4106
+ }
4107
+ let parsed;
4108
+ try {
4109
+ parsed = JSON.parse(raw);
4110
+ }
4111
+ catch (err) {
4112
+ console.error(chalk.red(`Invalid JSON in --output-data-file "${opts.outputDataFile}": ${err.message}`));
4113
+ process.exit(1);
4114
+ return;
4115
+ }
4116
+ body.output_data = parsed;
4117
+ }
4118
+ // --expect-version <n> opts the caller in to optimistic concurrency.
4119
+ // Default is bypass (no header) per the I30 spec Decision #4.
4120
+ const apiOpts = {};
4121
+ if (opts.expectVersion !== undefined) {
4122
+ const n = Number(opts.expectVersion);
4123
+ if (!Number.isInteger(n)) {
4124
+ console.error(chalk.red(`--expect-version must be an integer; got "${opts.expectVersion}"`));
4125
+ process.exit(1);
4126
+ }
4127
+ apiOpts.headers = { 'If-Match': String(n) };
4128
+ }
4129
+ const path = `/api/admin/force-transition/${entityType}/${id}`;
4130
+ const data = (await api('POST', path, body, apiOpts));
4131
+ if (opts.json) {
4132
+ console.log(JSON.stringify(data, null, 2));
4133
+ return;
4134
+ }
4135
+ // Human-readable summary. Per-entity payloads diverge (instance / step /
4136
+ // execution) so we surface whichever is present and always echo the
4137
+ // forensic fields (audit_log_id, subscribers_fired, subscriber_failures,
4138
+ // orphan_executions_cancelled).
4139
+ const entityPayload = data?.instance || data?.step || data?.execution || null;
4140
+ const label = entityPayload?.name || entityPayload?.step_name || entityPayload?.id || id;
4141
+ console.log(chalk.green(`Force-transitioned ${entityType} ${label} → ${opts.to}`));
4142
+ if (data?.audit_log_id) {
4143
+ console.log(chalk.gray(` audit_log_id: ${data.audit_log_id}`));
4144
+ }
4145
+ const fired = Array.isArray(data?.subscribers_fired) ? data.subscribers_fired : [];
4146
+ if (fired.length) {
4147
+ console.log(chalk.gray(` subscribers_fired: ${fired.join(', ')}`));
4148
+ }
4149
+ const failures = Array.isArray(data?.subscriber_failures)
4150
+ ? data.subscriber_failures
4151
+ : [];
4152
+ if (failures.length) {
4153
+ console.log(chalk.yellow(` subscriber_failures: ${failures.length}`));
4154
+ for (const f of failures) {
4155
+ console.log(chalk.yellow(` - ${f.id || f.subscriber || '?'}: ${f.error}`));
4156
+ }
4157
+ }
4158
+ const orphans = Array.isArray(data?.orphan_executions_cancelled)
4159
+ ? data.orphan_executions_cancelled
4160
+ : [];
4161
+ if (orphans.length) {
4162
+ console.log(chalk.gray(` orphan_executions_cancelled: ${orphans.join(', ')}`));
4163
+ }
4037
4164
  }
4038
4165
  catch (err) {
4039
4166
  console.error(chalk.red(err.message));
@@ -4058,7 +4185,7 @@ pipelines
4058
4185
  });
4059
4186
  pipelines
4060
4187
  .command('set-status')
4061
- .description('Update instance status. Allowed: paused, cancelled. Use "pl force-cancel" for stuck instances.')
4188
+ .description('Update instance status. Allowed: paused, cancelled. Use "pl force-transition pipeline <id> --to cancelled" for stuck instances.')
4062
4189
  .argument('<id>', 'Instance ID')
4063
4190
  .requiredOption('--status <status>', 'New status (paused or cancelled)')
4064
4191
  .action(async (id, opts) => {
@@ -4066,7 +4193,7 @@ pipelines
4066
4193
  const valid = ['paused', 'cancelled'];
4067
4194
  if (!valid.includes(opts.status)) {
4068
4195
  console.error(chalk.red(`Invalid status. Must be one of: ${valid.join(', ')}`));
4069
- console.error(chalk.yellow('Tip: Use "pl force-cancel" to cancel stuck pipelines from any state.'));
4196
+ console.error(chalk.yellow('Tip: Use "pl force-transition pipeline <id> --to cancelled" to cancel stuck pipelines from any state.'));
4070
4197
  process.exit(1);
4071
4198
  }
4072
4199
  const data = await api('PATCH', `/api/pipeline-instances/${id}/status`, { status: opts.status });