conduyt 1.81.0 → 1.82.0

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/README.md CHANGED
@@ -56,8 +56,9 @@ conduyt deals health --pipeline <id> --check needs_action # GET /deals/inspect
56
56
  conduyt deals health-fix <dealId> --check stale --expected-updated-at <iso> --stage-id <uuid> # Pipeline health quick fix (PATCH /deals/{id} with checkOrigin): stale = --stage-id, unassigned = --assigned, missing_close_date = --close-date, missing_contact = --contact; 409 deal_changed / deal_check_cleared and 400 health_fix_mismatch exit 1 verbatim, never retried
57
57
  conduyt deals health-next-action <dealId> --check needs_action --expected-updated-at <iso> --title "Call back" --idempotency-key <key> # "Set next action" (POST /tasks with checkOrigin): one non-done quick-fix task per deal, 409 deal_has_next_action / deal_changed / deal_check_cleared exit 1 verbatim
58
58
  conduyt deals card-fields set value,contact,custom:next_contact_at # the kanban card fields (1-8, built-ins or custom:<fieldKey>); `card-fields get` reads them
59
- conduyt pipelines stage-rules set <pipelineId> <stageId> --required-fields contactId,custom:loan_amount --required-documents 2 [--lost-reason-allow-other true] # what a deal needs to enter the stage; other stages untouched; `stage-rules get [pipelineId] [stageId]` reads
59
+ conduyt pipelines stage-rules set <pipelineId> <stageId> --required-fields contactId,custom:loan_amount --required-documents 2 [--lost-reason-allow-other true] [--entry-by-automation-only true] # what a deal needs to enter the stage (--entry-by-automation-only true: only an automation can move or create a deal into it, every other entry is refused 422 stage_automation_only; `pipelines list` shows it on each stage); other stages untouched; `stage-rules get [pipelineId] [stageId]` reads
60
60
  conduyt deals bulk-update --file updates.json # update up to 100 deals at once ([{id, stageId, value, ...}]); per-item results, exit 1 if any failed
61
+ conduyt deals bulk-edit --ids <id>,<id> --stage <stage> # the SAME change on up to 500 deals (POST /bulk/deals/edit { ids, updates }); failed deals are listed with their code (e.g. stage_automation_only), exit 1 if any failed
61
62
  conduyt pipelines # list pipelines and stages (each carries color, automationOnly, sortOrder)
62
63
  conduyt pipelines list --archived # the archived pipelines (owner / admin key): name, archivedName, openDeals, stageCount
63
64
  conduyt pipelines archive <id> --move-open-deals-to <pipelineId|none> [--expected-settings-revision N] # archive + move the open deals in one transaction; refusals print their code (stale_settings_revision, target_not_open, dpp_post_close, archive_too_large, target_archived, intake_target, pipeline_archived) and exit 1
@@ -11,7 +11,7 @@ export const MAX_STAGE_REQUIRED_FIELDS = 25;
11
11
  export const MAX_STAGE_REQUIRED_DOCUMENTS = 20;
12
12
  export const MAX_LOST_REASON_OPTIONS = 50;
13
13
  const CUSTOM_FIELD_RE = /^custom:[a-zA-Z_][a-zA-Z0-9_]*$/;
14
- export const RULE_KEYS = ["requiredFields", "requiredDocuments", "lostReasonLabel", "lostReasonOptions", "lostReasonAllowOther"];
14
+ export const RULE_KEYS = ["requiredFields", "requiredDocuments", "lostReasonLabel", "lostReasonOptions", "lostReasonAllowOther", "entryByAutomationOnly"];
15
15
  const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
16
16
  /** "a,b,custom:x" → ["a","b","custom:x"]; blank items are dropped, an all-blank value is an empty list */
17
17
  export function splitList(raw) {
@@ -64,9 +64,9 @@ export function validateRuleChange(raw) {
64
64
  throw new Error("lostReasonLabel must be 1-60 characters.");
65
65
  out[k] = t;
66
66
  }
67
- else if (k === "lostReasonAllowOther") {
67
+ else if (k === "lostReasonAllowOther" || k === "entryByAutomationOnly") {
68
68
  if (typeof v !== "boolean")
69
- throw new Error("lostReasonAllowOther must be true or false.");
69
+ throw new Error(`${k} must be true or false.`);
70
70
  out[k] = v;
71
71
  }
72
72
  else {
package/dist/index.js CHANGED
@@ -38,6 +38,18 @@ function readVersion() {
38
38
  return "0.0.0";
39
39
  }
40
40
  }
41
+ /** (#289 r3) the bulk deal routes read a fixed field list and silently DROP any other key, so a typo (`titel`) would hide behind a
42
+ * partly applied call: refuse it locally. app/api/v1/bulk/deals/edit/route.ts:56 (MAX_BULK_SIZE 500, refused 422 at :139-140) and
43
+ * its ALLOWED_FIELDS; app/api/v1/bulk/deals/update/route.ts:49-60 (MAX_BULK_SIZE 100, ALLOWED_FIELDS plus customFields; each item also has id). */
44
+ const BULK_EDIT_MAX_IDS = 500;
45
+ const BULK_UPDATE_MAX_ITEMS = 100;
46
+ const BULK_EDIT_FIELDS = ["title", "stageId", "value", "currency", "status", "priority", "assignedTo", "expectedCloseDate", "lostReason"];
47
+ const BULK_UPDATE_FIELDS = ["id", ...BULK_EDIT_FIELDS, "customFields"];
48
+ function assertAllowedKeys(keys, allowed, where) {
49
+ const bad = keys.filter((k) => !allowed.includes(k));
50
+ if (bad.length > 0)
51
+ throw new Error(`${where}: unknown field ${bad.map((k) => `"${k}"`).join(", ")}. The API ignores unknown fields silently, so nothing was sent. Allowed: ${allowed.join(", ")}.`);
52
+ }
41
53
  const program = new Command();
42
54
  program
43
55
  .name("conduyt")
@@ -891,7 +903,7 @@ cardFields
891
903
  }));
892
904
  deals
893
905
  .command("create")
894
- .description("Create a deal (use `pipelines list` to get pipeline/stage ids)")
906
+ .description("Create a deal (use `pipelines list` to get pipeline/stage ids). A stage set to \"Entry by automation only\" refuses every entry: 422 code stage_automation_only, message \"Only an automation can move a deal into <stage>\" (admins and API keys included; moving out is free); the code and message are printed on stderr unchanged.")
895
907
  .option("--title <title>", "deal title")
896
908
  .option("--pipeline <id>", "pipeline UUID")
897
909
  .option("--stage <id>", "stage UUID")
@@ -958,7 +970,7 @@ function assertDealSource(body) {
958
970
  }
959
971
  deals
960
972
  .command("update <id>")
961
- .description("Update a deal (PATCH — only the fields you pass change). --stage moves the deal (won/lost status auto-derives from the target stage's flags)")
973
+ .description("Update a deal (PATCH — only the fields you pass change). --stage moves the deal (won/lost status auto-derives from the target stage's flags). A stage set to \"Entry by automation only\" refuses every entry: 422 code stage_automation_only, message \"Only an automation can move a deal into <stage>\" (admins and API keys included; moving out is free); the code and message are printed on stderr unchanged.")
962
974
  .option("--title <title>", "deal title")
963
975
  .option("--value <n>", "deal value (number)")
964
976
  .option("--currency <code>", "ISO currency, e.g. GBP")
@@ -1027,7 +1039,7 @@ deals
1027
1039
  }));
1028
1040
  deals
1029
1041
  .command("bulk-update")
1030
- .description("Update up to 100 deals in one request (POST /bulk/deals/update). Pass a JSON array of { id, ...editable fields (title, stageId, value, currency, status, priority, assignedTo, expectedCloseDate, lostReason, customFields) } via --updates or --file. Each item is applied on its own; prints the summary and one result per item, and exits 1 when any item failed (the full result is still printed). Rate limited to 5 requests a minute")
1042
+ .description("Update up to 100 deals in one request (POST /bulk/deals/update). Pass a JSON array of { id, ...editable fields (title, stageId, value, currency, status, priority, assignedTo, expectedCloseDate, lostReason, customFields) } via --updates or --file. Each item is applied on its own; prints the summary and one result per item, and exits 1 when any item failed (the full result is still printed). Rate limited to 5 requests a minute. A stage change into an \"Entry by automation only\" stage fails that item with code stage_automation_only; the code stays on its result row in the printed output and is repeated on its `Failed <id>` stderr line")
1031
1043
  .option("--updates <json>", "the updates as a JSON array (or @file)")
1032
1044
  .option("--file <path>", "read the updates JSON array from a file")
1033
1045
  .action(async (opts) => {
@@ -1060,6 +1072,10 @@ deals
1060
1072
  }
1061
1073
  if (!Array.isArray(updates))
1062
1074
  throw new Error("The updates must be a JSON array of { id, ...fields } objects.");
1075
+ if (updates.length > BULK_UPDATE_MAX_ITEMS)
1076
+ throw new Error(`The updates take at most ${BULK_UPDATE_MAX_ITEMS} items (got ${updates.length}); the API refuses more with 422.`);
1077
+ updates.forEach((u, i) => { if (u && typeof u === "object" && !Array.isArray(u))
1078
+ assertAllowedKeys(Object.keys(u), BULK_UPDATE_FIELDS, `updates[${i}]`); });
1063
1079
  const client = new ConduytClient();
1064
1080
  const res = await client.post("/api/v1/bulk/deals/update", { updates });
1065
1081
  print(res);
@@ -1083,7 +1099,7 @@ deals
1083
1099
  const failedItems = results.filter((r) => r.success === false);
1084
1100
  if (failedItems.length > 0 || Number(sm.failed) > 0) {
1085
1101
  for (const r of failedItems)
1086
- process.stderr.write(`Failed ${r.id}: ${r.error ?? "unknown error"}\n`);
1102
+ process.stderr.write(`Failed ${r.id}: ${r.error ?? "unknown error"}${typeof r.code === "string" ? ` [code: ${r.code}]` : ""}\n`);
1087
1103
  process.stderr.write(`Error: ${failedItems.length || sm.failed} of ${sm.total} updates failed.\n`);
1088
1104
  process.exitCode = 1;
1089
1105
  }
@@ -1092,6 +1108,83 @@ deals
1092
1108
  fail(err);
1093
1109
  }
1094
1110
  });
1111
+ deals
1112
+ .command("bulk-edit")
1113
+ .description("Apply the SAME field changes to many deals in one request (POST /bulk/deals/edit, body { ids, updates }; up to 500 ids). --ids is a comma-separated list of deal UUIDs; the update flags (--stage, --title, --value, --currency, --status, --priority, --assigned-to, --expected-close-date, --lost-reason) or --json build the one updates object. Each deal is applied on its own; prints { updated, skippedNoOp, requested, errors } and exits 1 when any deal failed (the full result is still printed). A stage change into an \"Entry by automation only\" stage fails that deal with code stage_automation_only and the message \"Only an automation can move a deal into <stage>\": the code stays on its row in the printed errors and is repeated on its `Failed <id>` stderr line. For different changes per deal use `deals bulk-update`")
1114
+ .requiredOption("--ids <list>", "comma-separated deal UUIDs")
1115
+ .option("--stage <name-or-uuid>", "move every deal to this stage (a stage name is resolved via /automations/resolve)")
1116
+ .option("--title <title>", "deal title")
1117
+ .option("--value <n>", "deal value (number)")
1118
+ .option("--currency <code>", "ISO currency, e.g. GBP")
1119
+ .option("--status <status>", "explicit status (open|won|lost)")
1120
+ .option("--priority <priority>", "priority, e.g. low|medium|high")
1121
+ .option("--assigned-to <id>", "owner user UUID")
1122
+ .option("--expected-close-date <date>", "expected close date (ISO 8601)")
1123
+ .option("--lost-reason <reason>", "reason for a lost/disqualified deal")
1124
+ .option("--json <json>", "the updates object as JSON, merged over the flags")
1125
+ .action(async (opts) => {
1126
+ try {
1127
+ // Preflight: everything the CLI can check is checked BEFORE the stage name is looked up and before any request.
1128
+ const ids = uuidList(opts.ids, "--ids");
1129
+ if (ids.length > BULK_EDIT_MAX_IDS)
1130
+ throw new Error(`--ids takes at most ${BULK_EDIT_MAX_IDS} deal ids (got ${ids.length}); the API refuses more with 422.`);
1131
+ const updates = {};
1132
+ if (opts.title !== undefined)
1133
+ updates.title = opts.title;
1134
+ if (opts.value !== undefined) {
1135
+ const t = opts.value.trim();
1136
+ const n = Number(t);
1137
+ if (t === "" || !Number.isFinite(n))
1138
+ throw new Error("--value must be a finite number.");
1139
+ updates.value = n;
1140
+ }
1141
+ if (opts.currency !== undefined)
1142
+ updates.currency = opts.currency;
1143
+ if (opts.status !== undefined)
1144
+ updates.status = opts.status;
1145
+ if (opts.priority !== undefined)
1146
+ updates.priority = opts.priority;
1147
+ if (opts.assignedTo !== undefined)
1148
+ updates.assignedTo = uuidOf(opts.assignedTo, "--assigned-to");
1149
+ if (opts.expectedCloseDate !== undefined)
1150
+ updates.expectedCloseDate = opts.expectedCloseDate;
1151
+ if (opts.lostReason !== undefined) {
1152
+ if (opts.lostReason.trim() === "")
1153
+ throw new Error("--lost-reason was provided but is blank. Omit it or pass a reason.");
1154
+ updates.lostReason = opts.lostReason;
1155
+ }
1156
+ if (opts.stage !== undefined && opts.stage.trim() === "")
1157
+ throw new Error("--stage was provided but is blank. Omit it or pass a stage name/UUID.");
1158
+ if (opts.json !== undefined)
1159
+ Object.assign(updates, jsonArg(opts.json, "--json"));
1160
+ assertAllowedKeys(Object.keys(updates), BULK_EDIT_FIELDS, "--json updates");
1161
+ assertFiniteValue(updates.value);
1162
+ const stageFromFlag = opts.stage !== undefined && updates.stageId === undefined;
1163
+ if (Object.keys(updates).length === 0 && !stageFromFlag)
1164
+ throw new Error("Nothing to apply. Pass at least one update flag or --json.");
1165
+ const client = new ConduytClient();
1166
+ if (stageFromFlag)
1167
+ updates.stageId = await resolveStageId(client, opts.stage);
1168
+ const res = await client.post("/api/v1/bulk/deals/edit", { ids, updates });
1169
+ print(res);
1170
+ const data = unwrap(res);
1171
+ const errors = data && typeof data === "object" && Array.isArray(data.errors) ? data.errors : null;
1172
+ if (!errors || typeof data.updated !== "number" || typeof data.requested !== "number") {
1173
+ process.stderr.write("Error: Unexpected response shape from the API: expected data.updated, data.requested and data.errors.\n");
1174
+ process.exitCode = 1;
1175
+ return;
1176
+ }
1177
+ if (errors.length > 0) {
1178
+ for (const e of errors)
1179
+ process.stderr.write(`Failed ${String(e?.id ?? "?")}: ${e?.error ?? "unknown error"}${typeof e?.code === "string" ? ` [code: ${e.code}]` : ""}\n`);
1180
+ process.stderr.write(`Error: ${errors.length} of ${data.requested} deals failed.\n`);
1181
+ process.exitCode = 1;
1182
+ }
1183
+ }
1184
+ catch (err) {
1185
+ fail(err);
1186
+ }
1187
+ });
1095
1188
  // ---- pipelines ----
1096
1189
  // Default action preserves the original published `conduyt pipelines` (list)
1097
1190
  // invocation now that pipelines is a group — without it, commander prints help
@@ -1103,7 +1196,7 @@ const pipelines = program
1103
1196
  .action(run(async (client) => client.get("/api/v1/pipelines")));
1104
1197
  pipelines
1105
1198
  .command("list")
1106
- .description("List pipelines and their stages (each pipeline carries color, automationOnly, sortOrder). --archived lists the ARCHIVED pipelines instead (owner / admin key): id, name (the pre-archive name), archivedName, color, archivedAt, openDeals, stageCount")
1199
+ .description("List pipelines and their stages (each pipeline carries color, automationOnly, sortOrder; each stage carries entryByAutomationOnly: true = only an automation can move or create a deal INTO it, every other entry is refused 422 stage_automation_only; set it with `pipelines stage-rules set --entry-by-automation-only`). --archived lists the ARCHIVED pipelines instead (owner / admin key): id, name (the pre-archive name), archivedName, color, archivedAt, openDeals, stageCount")
1107
1200
  .option("--archived", "list the archived pipelines (GET /pipelines?archived=true) instead of the active ones")
1108
1201
  .action(run(async (client, opts) => client.get(opts.archived ? "/api/v1/pipelines?archived=true" : "/api/v1/pipelines")));
1109
1202
  pipelines
@@ -1205,7 +1298,7 @@ const stageRules = pipelines
1205
1298
  .description("Per-stage rules (settings.pipelineStageRules): the fields and documents a deal needs before it may enter a stage");
1206
1299
  stageRules
1207
1300
  .command("get [pipelineId] [stageId]")
1208
- .description("Read the stage rules as { pipelineId: { stageId: { requiredFields, requiredDocuments, lostReasonLabel, lostReasonOptions, lostReasonAllowOther } } }, for everything, one pipeline, or one stage")
1301
+ .description("Read the stage rules as { pipelineId: { stageId: { requiredFields, requiredDocuments, lostReasonLabel, lostReasonOptions, lostReasonAllowOther, entryByAutomationOnly } } }, for everything, one pipeline, or one stage")
1209
1302
  .action(run(async (client, pipelineId, stageId) => {
1210
1303
  if (pipelineId !== undefined)
1211
1304
  assertUuid(pipelineId, "pipeline id");
@@ -1229,7 +1322,8 @@ stageRules
1229
1322
  .option("--lost-reason-label <text>", "label of the stage's lost-reason prompt (1-60 chars)")
1230
1323
  .option("--lost-reason-options <list>", "comma-separated dropdown options of the lost-reason prompt (max 50; empty clears)")
1231
1324
  .option("--lost-reason-allow-other <bool>", "true|false: the rep may also pick \"Other\" and type the reason (stored as the reason text; takes effect while the stage has lost-reason options)")
1232
- .option("--json <json>", "the rule fields as a JSON object ({requiredFields, requiredDocuments, lostReasonLabel, lostReasonOptions, lostReasonAllowOther}), merged over the flags")
1325
+ .option("--entry-by-automation-only <bool>", "true|false: \"Entry by automation only\": only an automation run can move or create a deal INTO the stage; every other entry (admins and API keys included) is refused 422 stage_automation_only; moving out stays free (to correct by hand: set false, move the deal, set true; the starting stage of automatic intake deals cannot be one)")
1326
+ .option("--json <json>", "the rule fields as a JSON object ({requiredFields, requiredDocuments, lostReasonLabel, lostReasonOptions, lostReasonAllowOther, entryByAutomationOnly}), merged over the flags")
1233
1327
  .action(run(async (client, pipelineId, stageId, opts) => {
1234
1328
  assertUuid(pipelineId, "pipeline id");
1235
1329
  assertUuid(stageId, "stage id");
@@ -1246,18 +1340,20 @@ stageRules
1246
1340
  raw.lostReasonOptions = splitList(opts.lostReasonOptions);
1247
1341
  if (opts.lostReasonAllowOther !== undefined)
1248
1342
  raw.lostReasonAllowOther = boolFlag(opts.lostReasonAllowOther, "--lost-reason-allow-other");
1343
+ if (opts.entryByAutomationOnly !== undefined)
1344
+ raw.entryByAutomationOnly = boolFlag(opts.entryByAutomationOnly, "--entry-by-automation-only");
1249
1345
  if (opts.json !== undefined)
1250
1346
  Object.assign(raw, jsonArg(opts.json, "--json"));
1251
1347
  const change = validateRuleChange(raw);
1252
1348
  if (Object.keys(change).length === 0)
1253
- throw new Error("Nothing to set. Pass --required-fields, --required-documents, --lost-reason-label, --lost-reason-options, --lost-reason-allow-other or --json.");
1349
+ throw new Error("Nothing to set. Pass --required-fields, --required-documents, --lost-reason-label, --lost-reason-options, --lost-reason-allow-other, --entry-by-automation-only or --json.");
1254
1350
  return patchGeneralSettingsMerged(client, (settings) => stageRulePatch(settings, pipelineId, stageId, change));
1255
1351
  }));
1256
1352
  // (system stages 2026-09-24) tri-surface parity: the stage flags the API takes on PATCH /pipelines/:id/stages/:stageId,
1257
1353
  // isSystem included (a system holding stage automations route deals through; hidden from rep pickers, never won or lost)
1258
1354
  pipelines
1259
1355
  .command("update-stage <pipelineId> <stageId>")
1260
- .description("Update one stage (PATCH — only the fields you pass change). --is-system marks a system holding stage: automations route deals through it, reps never see it in a picker, and it can never be won or lost. A soft-deleted stage (`pipelines delete-stage`) answers 404")
1356
+ .description("Update one stage (PATCH — only the fields you pass change). --is-system marks a system holding stage: automations route deals through it, reps never see it in a picker, and it can never be won or lost. A soft-deleted stage (`pipelines delete-stage`) answers 404. The \"Entry by automation only\" flag is not set here (the API reads it from the stage rules): use `pipelines stage-rules set --entry-by-automation-only`")
1261
1357
  .option("--name <name>", "stage name")
1262
1358
  .option("--color <hex>", "stage colour")
1263
1359
  .option("--is-won <bool>", "true|false — entering this stage closes the deal as won")
@@ -1309,7 +1405,7 @@ pipelines
1309
1405
  // (pipeline settings 10-08) archive / restore / reorder, stage delete with a move, the stale-in-stage days.
1310
1406
  pipelines
1311
1407
  .command("archive <id>")
1312
- .description("Archive a pipeline (owner / admin key): renamed with the archived prefix, never deleted; its closed deals stay as history and `pipelines restore` brings it back. Every OPEN deal moves in the same transaction to --move-open-deals-to's first stage through the normal stage-entry rules; any refusal aborts all of it. Pass --move-open-deals-to none only when there are no open deals. Waits up to 75 s; if it times out the archive may have committed, so run `pipelines list` / `pipelines list --archived` before retrying. The default pipeline cannot be archived. Refusal codes are printed with the error and the command exits 1: stale_settings_revision, target_not_open, dpp_post_close, archive_too_large, target_archived, intake_target, pipeline_archived, open_deals, default_pipeline, stage_requirements, archive_raced, pipeline_automation_only. An already-archived pipeline answers { archived: true, alreadyArchived: true }")
1408
+ .description("Archive a pipeline (owner / admin key): renamed with the archived prefix, never deleted; its closed deals stay as history and `pipelines restore` brings it back. Every OPEN deal moves in the same transaction to --move-open-deals-to's first stage through the normal stage-entry rules; any refusal aborts all of it. Pass --move-open-deals-to none only when there are no open deals. Waits up to 75 s; if it times out the archive may have committed, so run `pipelines list` / `pipelines list --archived` before retrying. The default pipeline cannot be archived. Refusal codes are printed with the error and the command exits 1: stale_settings_revision, target_not_open, dpp_post_close, archive_too_large, target_archived, intake_target, pipeline_archived, open_deals, default_pipeline, stage_requirements, archive_raced, pipeline_automation_only, stage_automation_only (422, the destination is an \"Entry by automation only\" stage; nothing moved). An already-archived pipeline answers { archived: true, alreadyArchived: true }")
1313
1409
  .requiredOption("--move-open-deals-to <pipelineId|none>", "the active pipeline the open deals move to (its first stage must be open), or none when the pipeline has no open deals")
1314
1410
  .option("--expected-settings-revision <n>", "the settingsRevision from `pipelines settings-revision`; refused 409 stale_settings_revision when settings changed since")
1315
1411
  .action(run(async (client, id, opts) => {
@@ -1350,7 +1446,7 @@ pipelines
1350
1446
  }));
1351
1447
  pipelines
1352
1448
  .command("delete-stage <pipelineId> <stageId>")
1353
- .description("Soft-delete a stage (owner / admin key). An EMPTY stage can always be deleted, even if deals once used it: the row is kept so deal history and reports keep its name, but it is hidden from every pipeline / stage list, GET / PATCH on it answer 404, there is NO restore in v1, and its name may be reused by a new stage; its stage rule, outcome mappings and status metadata go with it. With --move-to the live deals move to another stage of the SAME pipeline (same won / lost setting, not a system stage) and the stage is deleted in one transaction. With --expected-settings-revision the answer is { id, deleted: true, settingsRevision }. A refusal exits 1 with the server's message printed verbatim and its code when it has one (never retried): deals currently in the stage and no --move-to; 'Cannot delete stage: it is used by N active automation(s) (names). Change their stage or pause them first.' (a published automation uses the stage in an action, trigger, condition or wait-until); the DPP status map selects it; stale_settings_revision, pipeline_archived, intake_target. A --move-to that is not a valid target is a 400. Accepted window: a run parked or in flight before the delete is not tracked; at the step that targets the deleted stage it fails or is skipped with a clear reason in the execution log")
1449
+ .description("Soft-delete a stage (owner / admin key). An EMPTY stage can always be deleted, even if deals once used it: the row is kept so deal history and reports keep its name, but it is hidden from every pipeline / stage list, GET / PATCH on it answer 404, there is NO restore in v1, and its name may be reused by a new stage; its stage rule, outcome mappings and status metadata go with it. With --move-to the live deals move to another stage of the SAME pipeline (same won / lost setting, not a system stage) and the stage is deleted in one transaction. With --expected-settings-revision the answer is { id, deleted: true, settingsRevision }. A refusal exits 1 with the server's message printed verbatim and its code when it has one (never retried): deals currently in the stage and no --move-to; 'Cannot delete stage: it is used by N active automation(s) (names). Change their stage or pause them first.' (a published automation uses the stage in an action, trigger, condition or wait-until); the DPP status map selects it; stale_settings_revision, pipeline_archived, intake_target. A --move-to that is not a valid target is a 400, and a --move-to stage that is \"Entry by automation only\" is refused 422 stage_automation_only (code and message unchanged). Accepted window: a run parked or in flight before the delete is not tracked; at the step that targets the deleted stage it fails or is skipped with a clear reason in the execution log")
1354
1450
  .option("--move-to <stageId>", "another stage of the same pipeline the live deals move to")
1355
1451
  .option("--expected-settings-revision <n>", "the settingsRevision from `pipelines settings-revision`; refused 409 stale_settings_revision when settings changed since")
1356
1452
  .action(run(async (client, pipelineId, stageId, opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt",
3
- "version": "1.81.0",
3
+ "version": "1.82.0",
4
4
  "description": "Command-line interface for Conduyt CRM — manage contacts, deals, pipelines, and run insight queries from your terminal.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",