@devflow-one/cli 0.1.3 → 0.1.6

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
@@ -162,6 +162,10 @@ devflow branch create feat/x
162
162
  devflow branch create feat/x --project ABC
163
163
  devflow mr create "Title" feat/x --body "desc" --project ABC
164
164
 
165
+ # target branch: the repo's configured base branch by default (a {year}/{number}
166
+ # pattern is expanded to the newest matching remote branch), or pick one
167
+ devflow mr create "Title" feat/x --base release/2026.4 --project ABC
168
+
165
169
  # routed by issue key (project key = prefix before the last '-')
166
170
  devflow mr find ABC-123
167
171
  devflow pipeline status 42 deadbeef --issue ABC-9
@@ -182,13 +186,16 @@ devflow mr ready 12 --profile prof-1
182
186
 
183
187
  ### Destructive actions
184
188
 
185
- `branch delete` and `mr close` are destructive. They are **refused unless you
186
- confirm** with `--force` (or `--yes`); the gateway enforces the same gate
187
- server-side, so the confirmation can't be bypassed:
189
+ `branch delete`, `mr close` and `mr merge` are destructive. They are **refused
190
+ unless you confirm** with `--force` (or `--yes`); the gateway enforces the same
191
+ gate server-side, so the confirmation can't be bypassed:
188
192
 
189
193
  ```sh
190
194
  devflow branch delete old --project ABC # refused
191
195
  devflow branch delete old --project ABC --force # executes
196
+
197
+ # merge a PR/MR, squashing and cleaning up the source branch
198
+ devflow mr merge 42 --method squash --delete-source-branch --force
192
199
  ```
193
200
 
194
201
  Inference never softens this gate — a destructive command run with fully
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devflow-one/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
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/args.js CHANGED
@@ -4,7 +4,15 @@
4
4
  // positionals.
5
5
 
6
6
  /** Boolean flags never consume the following token. */
7
- const BOOLEAN_FLAGS = new Set(["json", "all", "help", "force", "yes", "no-push"]);
7
+ const BOOLEAN_FLAGS = new Set([
8
+ "json",
9
+ "all",
10
+ "help",
11
+ "force",
12
+ "yes",
13
+ "no-push",
14
+ "delete-source-branch",
15
+ ]);
8
16
 
9
17
  /**
10
18
  * @param {string[]} argv the args after the node/script path
package/src/commands.js CHANGED
@@ -20,6 +20,20 @@ function toInt(name, value) {
20
20
  return n;
21
21
  }
22
22
 
23
+ /** Valid `--method` values for `mr merge`, mirroring the provider merge strategies. */
24
+ const MERGE_METHODS = ["merge", "squash", "rebase"];
25
+
26
+ /** Parse a `--method` flag into a merge strategy, or fail with the allowed set. */
27
+ function toMergeMethod(value) {
28
+ const method = String(value);
29
+
30
+ if (!MERGE_METHODS.includes(method)) {
31
+ throw new UsageError(`"--method" must be one of ${MERGE_METHODS.join(", ")}, got "${method}".`);
32
+ }
33
+
34
+ return method;
35
+ }
36
+
23
37
  /** Parse a `--values` JSON-object flag into a plain object, or fail clearly. */
24
38
  function toValues(flag, raw) {
25
39
  if (raw === undefined) return undefined;
@@ -39,6 +53,19 @@ function toValues(flag, raw) {
39
53
  return parsed;
40
54
  }
41
55
 
56
+ /** Parse a value-taking flag as a non-blank string, or fail clearly. */
57
+ function toBranch(flag, raw) {
58
+ if (raw === undefined) return undefined;
59
+
60
+ const branch = String(raw).trim();
61
+
62
+ if (raw === true || branch === "") {
63
+ throw new UsageError(`"--${flag}" requires a branch name.`);
64
+ }
65
+
66
+ return branch;
67
+ }
68
+
42
69
  /** A bad-invocation error (wrong/missing args) — printed with the command usage. */
43
70
  export class UsageError extends Error {
44
71
  constructor(message) {
@@ -102,7 +129,8 @@ const COMMANDS = {
102
129
  method: "POST",
103
130
  action: "commitByTemplate",
104
131
  usage:
105
- 'commit template <worktreePath> [--values \'{"key":"val"}\'] [--no-push] [--issue KEY] [--project KEY]',
132
+ 'commit template <worktreePath> [--values \'{"key":"val"}\'] [--no-push] [--issue KEY] [--project KEY]' +
133
+ " (--values keys must match the {key} tokens of the project's configured commit template)",
106
134
  buildArgs: ([worktreePath], flags) => {
107
135
  if (!worktreePath) throw new UsageError("commit template requires <worktreePath>.");
108
136
 
@@ -116,13 +144,21 @@ const COMMANDS = {
116
144
  "mr create": {
117
145
  method: "POST",
118
146
  action: "createPullRequest",
119
- usage: "mr create <title> <headBranch> [--body <text>] [--issue KEY] [--project KEY]",
147
+ usage:
148
+ "mr create <title> <headBranch> [--body <text>] [--base <branch>] [--issue KEY] [--project KEY]" +
149
+ " (--base defaults to the repo's configured base branch, expanding a {year}/{number} pattern)",
120
150
  buildArgs: ([title, headBranch], flags) => {
121
151
  if (!title || !headBranch) {
122
152
  throw new UsageError("mr create requires <title> and <headBranch>.");
123
153
  }
124
154
 
125
- return [title, flags.body ?? "", headBranch];
155
+ const baseBranch = toBranch("base", flags.base);
156
+
157
+ // Omitted rather than trailing-undefined so the gateway resolves the repo's
158
+ // own configured base branch (and expands a pattern) instead of seeing null.
159
+ return baseBranch
160
+ ? [title, flags.body ?? "", headBranch, baseBranch]
161
+ : [title, flags.body ?? "", headBranch];
126
162
  },
127
163
  },
128
164
  "mr ready": {
@@ -144,6 +180,20 @@ const COMMANDS = {
144
180
  usage: "mr close <prId> [--issue KEY] [--project KEY] --force",
145
181
  buildArgs: ([prId]) => [toInt("prId", prId)],
146
182
  },
183
+ "mr merge": {
184
+ method: "POST",
185
+ action: "mergePullRequest",
186
+ destructive: true,
187
+ usage:
188
+ "mr merge <prId> [--method merge|squash|rebase] [--delete-source-branch] " +
189
+ "[--issue KEY] [--project KEY] --force",
190
+ buildArgs: ([prId], flags) => {
191
+ const mergeMethod = flags.method === undefined ? undefined : toMergeMethod(flags.method);
192
+ const deleteSourceBranch = flags["delete-source-branch"] ? true : undefined;
193
+
194
+ return [toInt("prId", prId), { mergeMethod, deleteSourceBranch }];
195
+ },
196
+ },
147
197
  "mr assign": {
148
198
  method: "POST",
149
199
  action: "assignPr",
@@ -162,8 +162,8 @@ function fill(resolved, source, origin) {
162
162
  * @param {(args: string[]) => string | null} [options.runGit]
163
163
  * @param {(q: { cwd?: string, remote?: string, branch?: string }) => Promise<object | null>} [options.queryContext]
164
164
  * @returns {Promise<{
165
- * projectKey?: string, issueKey?: string, repo?: string, profile?: string,
166
- * profileAlive?: boolean | null, matchedBy: Record<string, string>,
165
+ * projectKey?: string, issueKey?: string, repo?: string, baseBranch?: string,
166
+ * profile?: string, profileAlive?: boolean | null, matchedBy: Record<string, string>,
167
167
  * candidates?: object[], ambiguous?: boolean, warnings: string[]
168
168
  * }>}
169
169
  */
@@ -259,6 +259,9 @@ export async function resolveContext({
259
259
  clean(context.repo?.id) ??
260
260
  clean(context.repo?.name) ??
261
261
  clean(context.repo?.remoteUrl),
262
+ // The selector `repo` flattens the digest to one string; the configured
263
+ // base branch rides alongside it so a caller can target it explicitly.
264
+ baseBranch: clean(context.repo?.baseBranch),
262
265
  profile: clean(context.profileId),
263
266
  }),
264
267
  },