@formigio/fazemos-cli 0.10.7 → 0.10.10

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
@@ -19,6 +19,20 @@ function parseNumber(val) {
19
19
  throw new Error(`Invalid number: ${val}`);
20
20
  return n;
21
21
  }
22
+ function parseStreamSilenceAbortMs(val) {
23
+ const n = Number(val);
24
+ if (!Number.isInteger(n) || n < 30000 || n > 1800000) {
25
+ throw new Error(`stream_silence_abort_ms must be an integer between 30000 and 1800000 (got ${val})`);
26
+ }
27
+ return n;
28
+ }
29
+ function formatMsHuman(ms) {
30
+ if (ms >= 60000) {
31
+ const min = ms / 60000;
32
+ return Number.isInteger(min) ? `${min} min` : `${min.toFixed(1)} min`;
33
+ }
34
+ return `${ms / 1000}s`;
35
+ }
22
36
  function parseJson(val, flag) {
23
37
  try {
24
38
  return JSON.parse(val);
@@ -2809,6 +2823,9 @@ templates
2809
2823
  else if (step.outputs?.length) {
2810
2824
  console.log(' Inputs: none');
2811
2825
  }
2826
+ if (step.stream_silence_abort_ms) {
2827
+ console.log(` Stream Silence Abort: ${step.stream_silence_abort_ms.toLocaleString()}ms (${formatMsHuman(step.stream_silence_abort_ms)})`);
2828
+ }
2812
2829
  if (step.execution_config && Object.keys(step.execution_config).length) {
2813
2830
  const ec = step.execution_config;
2814
2831
  console.log(' Execution config:');
@@ -3161,6 +3178,7 @@ templates
3161
3178
  .option('--after <stepId>', 'Insert after this step within the same phase (sets sort_order to target + 1, shifts subsequent steps). Target must live in the phase named by --phase.')
3162
3179
  .option('--sort-order <n>', 'Set sort_order directly (lower-level escape hatch)', parseNumber)
3163
3180
  .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
+ .option('--stream-silence-abort-ms <ms>', 'Stream silence abort threshold in milliseconds (30000-1800000)', parseStreamSilenceAbortMs)
3164
3182
  .action(async (templateId, opts) => {
3165
3183
  try {
3166
3184
  if (!VALID_STEP_TYPES.includes(opts.type)) {
@@ -3220,6 +3238,8 @@ templates
3220
3238
  };
3221
3239
  if (opts.agentConfig)
3222
3240
  step.agent_config = parseJson(opts.agentConfig, '--agent-config');
3241
+ if (opts.streamSilenceAbortMs !== undefined)
3242
+ step.stream_silence_abort_ms = opts.streamSilenceAbortMs;
3223
3243
  // Positioning: --after or --sort-order
3224
3244
  if (opts.after && opts.sortOrder !== undefined) {
3225
3245
  console.error(chalk.red('Cannot use both --after and --sort-order'));
@@ -3245,6 +3265,9 @@ templates
3245
3265
  await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
3246
3266
  console.log(chalk.green(`Added step: ${opts.name} (${opts.type}) to phase ${phase.name}`));
3247
3267
  console.log(` ID: ${step.id}`);
3268
+ if (step.stream_silence_abort_ms) {
3269
+ console.log(` Stream Silence Abort: ${step.stream_silence_abort_ms.toLocaleString()}ms (${formatMsHuman(step.stream_silence_abort_ms)})`);
3270
+ }
3248
3271
  }
3249
3272
  catch (err) {
3250
3273
  console.error(chalk.red(err.message));
@@ -3313,12 +3336,13 @@ templates
3313
3336
  .option('--env <json>', 'Environment variables for script steps as JSON')
3314
3337
  .option('--sections <text>', 'Agent instructions / step content. Use @filepath to load from a file (e.g., --sections @steps/review.md)')
3315
3338
  .option('--agent-config <json>', 'Per-step agent config overrides as JSON (merges with existing)')
3339
+ .option('--stream-silence-abort-ms <ms>', 'Stream silence abort threshold in milliseconds (30000-1800000)', parseStreamSilenceAbortMs)
3316
3340
  .action(async (templateId, opts) => {
3317
3341
  try {
3318
3342
  const hasUpdate = opts.name || opts.type || opts.role || opts.description != null
3319
3343
  || opts.reviewer != null || opts.maxReviewCycles != null || opts.parallelGroup != null
3320
3344
  || opts.image || opts.command || opts.timeout !== undefined || opts.workingDir || opts.env
3321
- || opts.agentConfig || opts.sections != null;
3345
+ || opts.agentConfig || opts.sections != null || opts.streamSilenceAbortMs !== undefined;
3322
3346
  if (!hasUpdate) {
3323
3347
  console.error(chalk.red('Provide at least one field to update'));
3324
3348
  process.exit(1);
@@ -3387,6 +3411,10 @@ templates
3387
3411
  if (opts.agentConfig) {
3388
3412
  step.agent_config = { ...(step.agent_config || {}), ...parseJson(opts.agentConfig, '--agent-config') };
3389
3413
  }
3414
+ // stream silence abort threshold
3415
+ if (opts.streamSilenceAbortMs !== undefined) {
3416
+ step.stream_silence_abort_ms = opts.streamSilenceAbortMs;
3417
+ }
3390
3418
  await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
3391
3419
  console.log(chalk.green(`Updated step: ${step.name}`));
3392
3420
  }
@@ -3994,18 +4022,114 @@ pipelines
3994
4022
  process.exit(1);
3995
4023
  }
3996
4024
  });
4025
+ // I30 — `pl force-cancel` removed. Replaced by the generalised
4026
+ // `pl force-transition pipeline <id> --to cancelled` admin endpoint, which
4027
+ // covers the same emergency cancel path plus 12 more entity/state combos
4028
+ // across pipelines, steps, and executions. No shim: callers update or 404.
3997
4029
  pipelines
3998
- .command('force-cancel')
3999
- .description('Force-cancel a stuck pipeline instance from any state. Admin/owner only. Bypasses normal state transition rules.')
4000
- .argument('<id>', 'Instance ID')
4001
- .option('-r, --reason <reason>', 'Reason for force-cancel')
4002
- .action(async (id, opts) => {
4003
- try {
4004
- const body = {};
4030
+ .command('force-transition')
4031
+ .description('Force an entity (pipeline, step, or execution) into a target state. Admin / org owner / project member only. ' +
4032
+ 'Bypasses normal state transition rules and fires the appropriate subscriber chain (audit log, ECS stop, ' +
4033
+ 'agent reset, token revoke, etc.). Use only when the normal lifecycle is stuck.')
4034
+ .argument('<entity-type>', 'Entity type: pipeline | step | execution')
4035
+ .argument('<id>', 'Entity ID (pipeline_instance / step_instance / execution UUID)')
4036
+ .requiredOption('--to <state>', 'Target state (e.g., cancelled, failed, completed, in_progress, ...)')
4037
+ .option('-r, --reason <reason>', 'Free-form audit reason (recorded in audit_log)')
4038
+ .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".')
4039
+ .option('--expect-version <n>', 'Optimistic-concurrency guard: send If-Match: "<n>" so the API rejects the call (409) ' +
4040
+ 'if the current entity version differs. Omit to bypass version checking (default).')
4041
+ .option('--json', 'Print the raw API response as JSON (machine-readable)')
4042
+ .action(async (entityType, id, opts) => {
4043
+ try {
4044
+ const valid = ['pipeline', 'step', 'execution'];
4045
+ if (!valid.includes(entityType)) {
4046
+ console.error(chalk.red(`Invalid entity-type "${entityType}". Must be one of: ${valid.join(', ')}`));
4047
+ process.exit(1);
4048
+ }
4049
+ if (!opts.to || typeof opts.to !== 'string' || !opts.to.trim()) {
4050
+ console.error(chalk.red('--to <state> is required'));
4051
+ process.exit(1);
4052
+ }
4053
+ // Body field names match the API contract verbatim:
4054
+ // target_state — required
4055
+ // reason — optional, free-form audit string
4056
+ // output_data — required for step→completed when the step has an
4057
+ // output schema; carrier rejects with 400 OUTPUT_DATA_*
4058
+ // otherwise.
4059
+ const body = { target_state: opts.to };
4005
4060
  if (opts.reason)
4006
4061
  body.reason = opts.reason;
4007
- const data = await api('POST', `/api/pipeline-instances/${id}/force-cancel`, body);
4008
- console.log(chalk.green(`Force-cancelled: ${data.instance?.name || id}`));
4062
+ if (opts.outputDataFile) {
4063
+ if (entityType !== 'step') {
4064
+ console.error(chalk.red('--output-data-file is only valid when entity-type is "step"'));
4065
+ process.exit(1);
4066
+ }
4067
+ let raw;
4068
+ try {
4069
+ raw = readFileSync(resolve(opts.outputDataFile), 'utf-8');
4070
+ }
4071
+ catch (err) {
4072
+ console.error(chalk.red(`Cannot read --output-data-file "${opts.outputDataFile}": ${err.message}`));
4073
+ process.exit(1);
4074
+ return;
4075
+ }
4076
+ let parsed;
4077
+ try {
4078
+ parsed = JSON.parse(raw);
4079
+ }
4080
+ catch (err) {
4081
+ console.error(chalk.red(`Invalid JSON in --output-data-file "${opts.outputDataFile}": ${err.message}`));
4082
+ process.exit(1);
4083
+ return;
4084
+ }
4085
+ body.output_data = parsed;
4086
+ }
4087
+ // --expect-version <n> opts the caller in to optimistic concurrency.
4088
+ // Default is bypass (no header) per the I30 spec Decision #4.
4089
+ const apiOpts = {};
4090
+ if (opts.expectVersion !== undefined) {
4091
+ const n = Number(opts.expectVersion);
4092
+ if (!Number.isInteger(n)) {
4093
+ console.error(chalk.red(`--expect-version must be an integer; got "${opts.expectVersion}"`));
4094
+ process.exit(1);
4095
+ }
4096
+ apiOpts.headers = { 'If-Match': String(n) };
4097
+ }
4098
+ const path = `/api/admin/force-transition/${entityType}/${id}`;
4099
+ const data = (await api('POST', path, body, apiOpts));
4100
+ if (opts.json) {
4101
+ console.log(JSON.stringify(data, null, 2));
4102
+ return;
4103
+ }
4104
+ // Human-readable summary. Per-entity payloads diverge (instance / step /
4105
+ // execution) so we surface whichever is present and always echo the
4106
+ // forensic fields (audit_log_id, subscribers_fired, subscriber_failures,
4107
+ // orphan_executions_cancelled).
4108
+ const entityPayload = data?.instance || data?.step || data?.execution || null;
4109
+ const label = entityPayload?.name || entityPayload?.step_name || entityPayload?.id || id;
4110
+ console.log(chalk.green(`Force-transitioned ${entityType} ${label} → ${opts.to}`));
4111
+ if (data?.audit_log_id) {
4112
+ console.log(chalk.gray(` audit_log_id: ${data.audit_log_id}`));
4113
+ }
4114
+ const fired = Array.isArray(data?.subscribers_fired) ? data.subscribers_fired : [];
4115
+ if (fired.length) {
4116
+ console.log(chalk.gray(` subscribers_fired: ${fired.join(', ')}`));
4117
+ }
4118
+ const failures = Array.isArray(data?.subscriber_failures)
4119
+ ? data.subscriber_failures
4120
+ : [];
4121
+ if (failures.length) {
4122
+ console.log(chalk.yellow(` subscriber_failures: ${failures.length}`));
4123
+ for (const f of failures) {
4124
+ console.log(chalk.yellow(` - ${f.id || f.subscriber || '?'}: ${f.error}`));
4125
+ }
4126
+ }
4127
+ const orphans = Array.isArray(data?.orphan_executions_cancelled)
4128
+ ? data.orphan_executions_cancelled
4129
+ : [];
4130
+ if (orphans.length) {
4131
+ console.log(chalk.gray(` orphan_executions_cancelled: ${orphans.join(', ')}`));
4132
+ }
4009
4133
  }
4010
4134
  catch (err) {
4011
4135
  console.error(chalk.red(err.message));
@@ -4030,7 +4154,7 @@ pipelines
4030
4154
  });
4031
4155
  pipelines
4032
4156
  .command('set-status')
4033
- .description('Update instance status. Allowed: paused, cancelled. Use "pl force-cancel" for stuck instances.')
4157
+ .description('Update instance status. Allowed: paused, cancelled. Use "pl force-transition pipeline <id> --to cancelled" for stuck instances.')
4034
4158
  .argument('<id>', 'Instance ID')
4035
4159
  .requiredOption('--status <status>', 'New status (paused or cancelled)')
4036
4160
  .action(async (id, opts) => {
@@ -4038,7 +4162,7 @@ pipelines
4038
4162
  const valid = ['paused', 'cancelled'];
4039
4163
  if (!valid.includes(opts.status)) {
4040
4164
  console.error(chalk.red(`Invalid status. Must be one of: ${valid.join(', ')}`));
4041
- console.error(chalk.yellow('Tip: Use "pl force-cancel" to cancel stuck pipelines from any state.'));
4165
+ console.error(chalk.yellow('Tip: Use "pl force-transition pipeline <id> --to cancelled" to cancel stuck pipelines from any state.'));
4042
4166
  process.exit(1);
4043
4167
  }
4044
4168
  const data = await api('PATCH', `/api/pipeline-instances/${id}/status`, { status: opts.status });