@devflow-one/cli 0.1.15 → 0.1.17

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
@@ -182,9 +182,17 @@ devflow mr create "Title" feat/x --base release/2026.4 --project ABC
182
182
  # stamped with. Omitted or unrecognized, it is auto-selected by matching configured
183
183
  # type names against the title's conventional prefix, branch prefix, Jira issue type
184
184
  # and title keywords, falling back to the first type. Pass its id or label to pick one.
185
+ # The template (its own {prMedia} uploads media) writes the description; --body is the
186
+ # fallback, where {prMedia} is filled or removed.
185
187
  devflow mr create "Title" feat/x --type Bugfix --project ABC
186
188
  devflow mr types --project ABC # configured types: id, label, default flag
187
189
 
190
+ # ready for review: also applies the PR type's labels, assigns you when unassigned and
191
+ # requests the type's missing reviewers (additive, best-effort). --type switches the
192
+ # type by swapping its labels; to refresh this later, run `mr draft` then `mr ready` again
193
+ devflow mr ready 42 --project ABC
194
+ devflow mr ready 42 --type Bugfix --project ABC
195
+
188
196
  # issue key: parsed from the head branch by default; --issue sets it explicitly
189
197
  # ({parentWithChildIssueKeys}/{issueKey}/{issueTitle} template tokens) for a branch without one.
190
198
  # {issueTitle} needs the Jira API token set in Settings, else it renders empty.
@@ -198,51 +206,77 @@ devflow mr find ABC-123
198
206
  devflow pipeline status 42 deadbeef --issue ABC-9
199
207
 
200
208
  # commit a managed worktree with the project's commit-message template; it stages tracked
201
- # changes only (add --include-untracked for new files) and pushing needs --force
202
- devflow commit template /wt/ABC-123 --values '{"summary":"fix login"}' --project ABC --force
209
+ # changes only (add --include-untracked for new files); pushing is destructive
210
+ devflow commit template /wt/ABC-123 --values '{"summary":"fix login"}' --project ABC
203
211
  devflow commit template /wt/ABC-123 --no-push --project ABC # commit locally, skip push
204
212
 
205
213
  # read a PR/MR's title + description, then edit it: --title/--description replace,
206
214
  # --append-description adds text after a `---` rule (skipped if already at the end);
207
- # --base retargets the PR/MR to an existing remote branch
215
+ # --base retargets the PR/MR to an existing remote branch; `{prMedia}` in the description
216
+ # uploads .devflow/media/<ISSUE-KEY>[-n].{mp4,png,jpg,jpeg} and is replaced with the links
217
+ # (removed when the upload fails or the head branch names no issue; media.error says why)
208
218
  devflow mr get 42 --project ABC
209
219
  devflow mr update 42 --append-description "## Test plan" --project ABC
210
220
  devflow mr update 42 --title "Fix login redirect" --project ABC
211
221
  devflow mr update 42 --base release/2026.4 --project ABC
222
+ devflow mr update 42 --description "$(cat body.md)" --project ABC # body.md has {prMedia}
212
223
 
213
224
  # assembled fix-review-comments prompt (read-only; runs no AI, makes no writes)
214
225
  devflow mr fix-prompt 42 # issue key inferred from the branch
215
226
 
227
+ # issue status and the repo card's branches and PRs/MRs (read-only; the issue key is
228
+ # inferred from the branch). Status needs the Jira API token set in Settings.
229
+ devflow issue status # the issue's and its parent's status
230
+ devflow issue branches ABC-123 --project ABC
231
+ devflow issue prs # every state, merged and closed included
232
+
233
+ # review comments: counts, then "Reply as Fixed" or an AI "Fix Comments" run, which
234
+ # commits and pushes (so it is destructive). It returns at once: `started`, `prNumber` and
235
+ # `queued` (true when an automation run is busy, so the fix starts on its next tick), or
236
+ # `started: false` with a `reason` — e.g. while the panel or the timer is fixing that PR
237
+ devflow mr comments 42 # unfixed / fixed / resolved / prComments
238
+ devflow mr reply-fixed 42 # success / failed / resolved counts
239
+ devflow mr fix-comments 42 # then re-check the counts
240
+
216
241
  # notify the project's configured Slack channel(s)
217
242
  devflow slack notify "Review fixes ready on {pr}" --values '{"pr":"#42"}' --project ABC
218
243
  devflow slack channels --project ABC # configured channels (read-only, no credentials)
219
244
 
245
+ # reviewer notify: per channel, were reviewers already notified, and is the next step a
246
+ # send or a ping? `source` says who posted: manual (panel), auto (auto-send), gateway
247
+ # (`slack notify --pr`) or slack (found only in Slack history). `--pr` records the post
248
+ # and threads a ping under the earlier message
249
+ devflow mr notify-status 42
250
+ devflow slack notify "Please review #42" --channel C123 --pr 42
251
+
220
252
  # force a specific profile when a key is ambiguous
221
253
  devflow mr ready 12 --profile prof-1
222
254
  ```
223
255
 
224
256
  ### Destructive actions
225
257
 
226
- `branch delete`, `mr close` and `mr merge` are destructive. They are **refused
227
- unless you confirm** with `--force` (or `--yes`); the gateway enforces the same
228
- gate server-side, so the confirmation can't be bypassed:
258
+ `branch delete`, `mr close`, `mr merge`, `mr fix-comments` (its AI run commits and
259
+ pushes) and `commit template` (unless `--no-push`) are destructive. By default they
260
+ run without confirmation. Turn on **Require confirmation for destructive CLI/MCP
261
+ actions** in a repo's DevFlow Git settings, and that repo then refuses them with
262
+ `DESTRUCTIVE_BLOCKED` unless you confirm with `--force` (or `--yes`). The extension
263
+ enforces the setting, so it can't be bypassed:
229
264
 
230
265
  ```sh
231
- devflow branch delete old --project ABC # refused
232
- devflow branch delete old --project ABC --force # executes
266
+ devflow branch delete old --project ABC # runs; refused if confirmation is on
267
+ devflow branch delete old --project ABC --force # always runs
233
268
 
234
269
  # merge a PR/MR, squashing and cleaning up the source branch
235
270
  devflow mr merge 42 --method squash --delete-source-branch --force
236
271
  ```
237
272
 
238
- Inference never softens this gate — a destructive command run with fully
239
- inferred routing still refuses without `--force`. Confirmation is always yours
240
- to give explicitly, precisely because inference makes mis-targeting easier.
273
+ Inference never confirms — even with fully inferred routing, only an explicit
274
+ `--force` counts, precisely because inference makes mis-targeting easier.
241
275
 
242
276
  ## Verify
243
277
 
244
278
  ```sh
245
- npm test # unit: arg parsing, command resolution, routing, destructive gate
279
+ npm test # unit: arg parsing, command resolution, routing, destructive confirmation
246
280
  ```
247
281
 
248
282
  ## Releasing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devflow-one/cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Terminal CLI wrapping the local DevFlow gateway so shell scripts can trigger repo actions.",
5
5
  "keywords": [
6
6
  "devflow",
package/src/cli.js CHANGED
@@ -35,9 +35,9 @@ below is an override, applied ahead of anything inferred:
35
35
 
36
36
  Run \`devflow where\` to see what the current directory resolves to.
37
37
 
38
- Destructive actions (branch delete, mr close):
39
- --force, --yes Confirm a destructive action; required or it is refused.
40
- Never inferred — inference makes mis-targeting easier.
38
+ Destructive actions (branch delete, mr close, mr merge, pushing commits):
39
+ --force, --yes Confirm a destructive action. Needed only when confirmation
40
+ is enabled for the repo in DevFlow settings. Never inferred.
41
41
 
42
42
  Output:
43
43
  --json Print machine-readable JSON (envelope) for scripts.
@@ -199,16 +199,8 @@ export async function run(argv, io = {}) {
199
199
 
200
200
  assertRoutable(key, command, context, flags);
201
201
 
202
- // Destructive gate: refuse before touching the gateway unless confirmed.
203
- // Deliberately independent of inference — a confirmed target is never guessed.
204
- const confirmed = Boolean(flags.force || flags.yes);
205
- const destructive = isDestructiveCommand(command, flags);
206
-
207
- if (destructive && !confirmed) {
208
- throw new UsageError(
209
- `"${key}" is destructive — re-run with --force (or --yes) to confirm. Usage: ${command.usage}`,
210
- );
211
- }
202
+ // Only an explicit --force/--yes confirms; inference makes mis-targeting easier.
203
+ const confirmed = Boolean(flags.force || flags.yes) && isDestructiveCommand(command, flags);
212
204
 
213
205
  // The owning profile is known but closed: say so by name rather than letting
214
206
  // the gateway answer with a bare NO_PROFILE.
@@ -220,7 +212,7 @@ export async function run(argv, io = {}) {
220
212
  const args = command.buildArgs ? command.buildArgs(rest, flags, context) : [];
221
213
  const body = {
222
214
  ...buildRoutingBody(context),
223
- ...(destructive ? { allowDestructive: true } : {}),
215
+ ...(confirmed ? { allowDestructive: true } : {}),
224
216
  payload: {
225
217
  projectKey: projectKeyFrom(context),
226
218
  ...(context.repo ? { repo: context.repo } : {}),
package/src/client.js CHANGED
@@ -27,13 +27,18 @@ export class GatewayError extends Error {
27
27
  export const RETRY_HINT =
28
28
  "The action may have completed — verify (e.g. findPrByIssueKey) before retrying.";
29
29
 
30
+ export const DESTRUCTIVE_HINT =
31
+ "Confirmation is enabled in DevFlow settings for this repo — re-run with --force (or --yes).";
32
+
30
33
  /**
31
- * The "verify before retrying" hint for a TIMEOUT of a retry-unsafe action
32
- * that may already have run; undefined when a retry is safe.
34
+ * How to retry a failed call: confirm a DESTRUCTIVE_BLOCKED action, or verify a
35
+ * retry-unsafe TIMEOUT that may already have run; undefined when a retry is safe.
33
36
  * @param {unknown} err
34
37
  * @param {boolean} [retryUnsafe]
35
38
  */
36
39
  export function retryHint(err, retryUnsafe) {
40
+ if (err instanceof GatewayError && err.code === "DESTRUCTIVE_BLOCKED") return DESTRUCTIVE_HINT;
41
+
37
42
  const mayHaveRun =
38
43
  err instanceof GatewayError && err.code === "TIMEOUT" && err.delivered !== false;
39
44
 
package/src/commands.js CHANGED
@@ -123,8 +123,8 @@ export class UsageError extends Error {
123
123
  * @property {"GET" | "POST" | "LOCAL"} method `LOCAL` runs in-process, no action call.
124
124
  * @property {string} [path] For GET commands.
125
125
  * @property {string} [action] Gateway action name for POST commands.
126
- * @property {boolean | ((flags: Record<string, unknown>) => boolean)} [destructive] Requires
127
- * --force/--yes (gated by cli.js); a predicate when that depends on the flags.
126
+ * @property {boolean | ((flags: Record<string, unknown>) => boolean)} [destructive] Accepts
127
+ * --force/--yes as confirmation; a predicate when that depends on the flags.
128
128
  * @property {boolean} [retryUnsafe] A retry may repeat its side effect; a TIMEOUT warns to verify first.
129
129
  * @property {(pos: string[], flags: Record<string, unknown>, context?: object) => unknown[]} [buildArgs]
130
130
  * @property {string} usage One-line usage string.
@@ -182,7 +182,7 @@ const COMMANDS = {
182
182
  retryUnsafe: true,
183
183
  action: "deleteBranch",
184
184
  destructive: true,
185
- usage: "branch delete <branchName> [--issue KEY] [--project KEY] --force",
185
+ usage: "branch delete <branchName> [--issue KEY] [--project KEY] [--force]",
186
186
  buildArgs: ([branchName]) => {
187
187
  if (!branchName) throw new UsageError("branch delete requires <branchName>.");
188
188
 
@@ -198,7 +198,7 @@ const COMMANDS = {
198
198
  destructive: (flags) => !flags["no-push"],
199
199
  usage:
200
200
  'commit template <worktreePath> [--values \'{"key":"val"}\'] [--include-untracked]' +
201
- " [--issue KEY] [--project KEY] (--no-push | --force)" +
201
+ " [--issue KEY] [--project KEY] [--no-push | --force]" +
202
202
  " (stages every modified/deleted tracked file and pushes, refused from the base branch;" +
203
203
  " untracked files are refused unless --include-untracked;" +
204
204
  " --values keys fill the project's commit template, default" +
@@ -218,6 +218,32 @@ const COMMANDS = {
218
218
  },
219
219
  },
220
220
 
221
+ "issue status": {
222
+ method: "POST",
223
+ action: "getIssueStatus",
224
+ usage:
225
+ "issue status [issueKey] [--issue KEY] [--project KEY]" +
226
+ " (the issue's and its parent's Jira status; needs the Jira API token source; read-only)",
227
+ buildArgs: issueKeyArgs("issue status"),
228
+ },
229
+ "issue branches": {
230
+ method: "POST",
231
+ action: "listIssueBranches",
232
+ usage:
233
+ "issue branches [issueKey] [--issue KEY] [--project KEY]" +
234
+ " (each repo's remote branches for the issue and its parent, as the repo card lists them; read-only)",
235
+ buildArgs: issueKeyArgs("issue branches"),
236
+ },
237
+ "issue prs": {
238
+ method: "POST",
239
+ action: "listIssuePullRequests",
240
+ usage:
241
+ "issue prs [issueKey] [--issue KEY] [--project KEY]" +
242
+ " (each repo's PRs/MRs for the issue in every state, merged and closed included, with" +
243
+ " approvers on open ones; read-only)",
244
+ buildArgs: issueKeyArgs("issue prs"),
245
+ },
246
+
221
247
  "mr create": {
222
248
  method: "POST",
223
249
  retryUnsafe: true,
@@ -228,7 +254,8 @@ const COMMANDS = {
228
254
  " (a headBranch that exists on the remote is used as is; otherwise the repo's branch" +
229
255
  " template picks the head branch from the issue; --base defaults to the repo's configured base branch, expanding a {year}/{number} pattern;" +
230
256
  " --type is auto-selected by matching PR type names against the title prefix, branch prefix," +
231
- " Jira issue type and title keywords, falling back to the first configured type)",
257
+ " Jira issue type and title keywords, falling back to the first configured type;" +
258
+ " the description comes from the PR type template, --body is the fallback where {prMedia} is filled or removed)",
232
259
  buildArgs: ([title, headBranch], flags, context = {}) => {
233
260
  const issueKey = templateIssueKey(headBranch, flags, context);
234
261
 
@@ -253,8 +280,18 @@ const COMMANDS = {
253
280
  "mr ready": {
254
281
  method: "POST",
255
282
  action: "markPrReadyForReview",
256
- usage: "mr ready <prId> [--issue KEY] [--project KEY]",
257
- buildArgs: ([prId]) => [toInt("prId", prId)],
283
+ usage:
284
+ "mr ready <prId> [--type <idOrLabel>] [--issue KEY] [--project KEY]" +
285
+ " (also applies the PR type's labels, assigns you when unassigned and requests the" +
286
+ " type's missing reviewers; --type switches the type by swapping its labels;" +
287
+ " run `mr draft` then `mr ready` again to refresh them)",
288
+ buildArgs: ([prId], flags) => {
289
+ const id = toInt("prId", prId);
290
+ const prType = toText("type", flags.type);
291
+
292
+ // An omitted prType is left out (not sent as null) so the gateway keeps the current type.
293
+ return prType === undefined ? [id] : [id, prType];
294
+ },
258
295
  },
259
296
  "mr draft": {
260
297
  method: "POST",
@@ -267,7 +304,7 @@ const COMMANDS = {
267
304
  retryUnsafe: true,
268
305
  action: "closePullRequest",
269
306
  destructive: true,
270
- usage: "mr close <prId> [--issue KEY] [--project KEY] --force",
307
+ usage: "mr close <prId> [--issue KEY] [--project KEY] [--force]",
271
308
  buildArgs: ([prId]) => [toInt("prId", prId)],
272
309
  },
273
310
  "mr merge": {
@@ -277,7 +314,7 @@ const COMMANDS = {
277
314
  destructive: true,
278
315
  usage:
279
316
  "mr merge <prId> [--method merge|squash|rebase] [--delete-source-branch] " +
280
- "[--issue KEY] [--project KEY] --force",
317
+ "[--issue KEY] [--project KEY] [--force]",
281
318
  buildArgs: ([prId], flags) => {
282
319
  const mergeMethod = flags.method === undefined ? undefined : toMergeMethod(flags.method);
283
320
  const deleteSourceBranch = flags["delete-source-branch"] ? true : undefined;
@@ -295,30 +332,6 @@ const COMMANDS = {
295
332
  return [toInt("prId", prId), assignee];
296
333
  },
297
334
  },
298
- "mr labels": {
299
- method: "POST",
300
- action: "addPrLabels",
301
- usage: "mr labels <prId> <label...> [--issue KEY] [--project KEY]",
302
- buildArgs: ([prId, ...labels]) => {
303
- if (labels.length === 0) {
304
- throw new UsageError("mr labels requires <prId> and at least one <label>.");
305
- }
306
-
307
- return [toInt("prId", prId), labels];
308
- },
309
- },
310
- "mr reviewers": {
311
- method: "POST",
312
- action: "requestPrReviewers",
313
- usage: "mr reviewers <prId> <reviewer...> [--issue KEY] [--project KEY]",
314
- buildArgs: ([prId, ...reviewers]) => {
315
- if (reviewers.length === 0) {
316
- throw new UsageError("mr reviewers requires <prId> and at least one <reviewer>.");
317
- }
318
-
319
- return [toInt("prId", prId), reviewers];
320
- },
321
- },
322
335
  "mr find": {
323
336
  method: "POST",
324
337
  action: "findPrByIssueKey",
@@ -341,7 +354,10 @@ const COMMANDS = {
341
354
  " [--base <branch>] [--issue KEY] [--project KEY]" +
342
355
  " (--title/--description replace; --append-description adds text after a `---` rule" +
343
356
  " and is skipped if it already ends the description after that rule; --base retargets" +
344
- " the PR/MR to an existing remote branch)",
357
+ " the PR/MR to an existing remote branch; `{prMedia}` in --description or" +
358
+ " --append-description uploads .devflow/media/<ISSUE-KEY>[-n].{mp4,png,jpg,jpeg} and is" +
359
+ " replaced with the links, so swap old media links for `{prMedia}` to refresh them." +
360
+ " It is removed if the upload fails or the head branch names no issue; media.error says why)",
345
361
  buildArgs: ([prId], flags) => {
346
362
  const title = toText("title", flags.title);
347
363
  const body = toRawText("description", flags.description);
@@ -377,6 +393,46 @@ const COMMANDS = {
377
393
  return [parsedPrId, context.issueKey];
378
394
  },
379
395
  },
396
+ "mr comments": {
397
+ method: "POST",
398
+ action: "getPrReviewCommentCounts",
399
+ usage:
400
+ "mr comments <prId> [--issue KEY] [--project KEY]" +
401
+ " (unfixed / fixed / resolved review-thread and PR-level comment counts; read-only)",
402
+ buildArgs: ([prId]) => [toInt("prId", prId)],
403
+ },
404
+ "mr notify-status": {
405
+ method: "POST",
406
+ action: "getReviewerNotifyStatus",
407
+ usage:
408
+ "mr notify-status <prId> [--issue KEY] [--project KEY]" +
409
+ " (per reviewer channel: whether reviewers were already notified on Slack and whether the" +
410
+ " next step is a send or a ping, and who posted; follow up with `slack notify` for that" +
411
+ " PR; read-only)",
412
+ buildArgs: ([prId]) => [toInt("prId", prId)],
413
+ },
414
+ "mr fix-comments": {
415
+ method: "POST",
416
+ retryUnsafe: true,
417
+ action: "fixPrComments",
418
+ destructive: true,
419
+ usage:
420
+ "mr fix-comments <prId> [--issue KEY] [--project KEY] [--force]" +
421
+ " (starts an AI fix of the PR's unresolved review comments, which commits and pushes;" +
422
+ " returns at once: started / prNumber / queued, or a reason it was refused, e.g. that PR" +
423
+ " is already being fixed; re-check with `mr comments`)",
424
+ buildArgs: ([prId]) => [toInt("prId", prId)],
425
+ },
426
+ "mr reply-fixed": {
427
+ method: "POST",
428
+ retryUnsafe: true,
429
+ action: "replyPrCommentsFixed",
430
+ usage:
431
+ "mr reply-fixed <prId> [--issue KEY] [--project KEY]" +
432
+ ' (replies "Fixed" to every unanswered review thread, resolving it when the repo is set to;' +
433
+ " prints success / failed counts)",
434
+ buildArgs: ([prId]) => [toInt("prId", prId)],
435
+ },
380
436
  "pipeline status": {
381
437
  method: "POST",
382
438
  action: "getPipelineStatus",
@@ -423,14 +479,19 @@ const COMMANDS = {
423
479
  retryUnsafe: true,
424
480
  action: "notifySlack",
425
481
  usage:
426
- 'slack notify <message> [--channel ID] [--values \'{"key":"val"}\'] [--issue KEY] [--project KEY]',
482
+ 'slack notify <message> [--channel ID] [--values \'{"key":"val"}\'] [--pr <prId>]' +
483
+ " [--issue KEY] [--project KEY]" +
484
+ " (--pr records the post as that PR's reviewer notify; a channel already notified for it" +
485
+ " gets a ping threaded under the earlier message)",
427
486
  buildArgs: ([message], flags) => {
428
487
  if (!message) throw new UsageError("slack notify requires <message>.");
429
488
 
430
489
  const channel = flags.channel ? String(flags.channel) : undefined;
431
490
  const values = toValues("values", flags.values);
432
491
 
433
- return [message, channel, values];
492
+ if (flags.pr === undefined) return [message, channel, values];
493
+
494
+ return [message, channel, values, { prNumber: toInt("pr", String(flags.pr)) }];
434
495
  },
435
496
  },
436
497
  "slack channels": {
@@ -442,7 +503,7 @@ const COMMANDS = {
442
503
  };
443
504
 
444
505
  /**
445
- * Does this invocation of `command` need --force/--yes?
506
+ * Is this invocation of `command` destructive?
446
507
  * @param {Command} command
447
508
  * @param {Record<string, unknown>} flags
448
509
  */