@devflow-one/cli 0.1.10 → 0.1.12
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 +14 -1
- package/package.json +1 -1
- package/src/cli.js +9 -5
- package/src/client.js +21 -1
- package/src/commands.js +98 -20
package/README.md
CHANGED
|
@@ -156,7 +156,12 @@ devflow where # what this directory resolves to
|
|
|
156
156
|
|
|
157
157
|
# inside a configured repo, routing is inferred — no flags
|
|
158
158
|
devflow mr find # issue key from the branch
|
|
159
|
-
|
|
159
|
+
|
|
160
|
+
# the issue's branch, from the repo's branch template (incl. {parentIssueKey}: a
|
|
161
|
+
# sub-issue may resolve to its parent's branch). Check it out before `commit template`.
|
|
162
|
+
devflow branch resolve ABC-124 # read-only: branch name + whether it exists
|
|
163
|
+
devflow branch create --issue ABC-124 # creates it, or reuses an existing one
|
|
164
|
+
devflow branch create feat/x # a name is only a fallback when no issue key is known
|
|
160
165
|
|
|
161
166
|
# or route explicitly, overriding whatever the directory implies
|
|
162
167
|
devflow branch create feat/x --project ABC
|
|
@@ -177,6 +182,8 @@ devflow mr types --project ABC # configured types: id, label, def
|
|
|
177
182
|
# ({parentWithChildIssueKeys}/{issueKey}/{issueTitle} template tokens) for a branch without one.
|
|
178
183
|
# {issueTitle} needs the Jira API token set in Settings, else it renders empty.
|
|
179
184
|
devflow mr create "Title" feat/x --issue ABC-123
|
|
185
|
+
devflow mr create "Title" --issue ABC-123 # head branch from the branch template
|
|
186
|
+
# the template always picks the head branch; a different one is replaced (warning on stderr)
|
|
180
187
|
|
|
181
188
|
# routed by issue key (project key = prefix before the last '-')
|
|
182
189
|
devflow mr find ABC-123
|
|
@@ -186,6 +193,12 @@ devflow pipeline status 42 deadbeef --issue ABC-9
|
|
|
186
193
|
devflow commit template /wt/ABC-123 --values '{"summary":"fix login"}' --project ABC
|
|
187
194
|
devflow commit template /wt/ABC-123 --no-push --project ABC # commit locally, skip push
|
|
188
195
|
|
|
196
|
+
# read a PR/MR's title + description, then edit it: --title/--description replace,
|
|
197
|
+
# --append-description adds text after a `---` rule (skipped if already at the end)
|
|
198
|
+
devflow mr get 42 --project ABC
|
|
199
|
+
devflow mr update 42 --append-description "## Test plan" --project ABC
|
|
200
|
+
devflow mr update 42 --title "Fix login redirect" --project ABC
|
|
201
|
+
|
|
189
202
|
# assembled fix-review-comments prompt (read-only; runs no AI, makes no writes)
|
|
190
203
|
devflow mr fix-prompt 42 # issue key inferred from the branch
|
|
191
204
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { parseArgs } from "./args.js";
|
|
6
6
|
import { resolveConfig, ConfigError } from "./config.js";
|
|
7
|
-
import { gatewayRequest, GatewayError } from "./client.js";
|
|
7
|
+
import { gatewayRequest, GatewayError, retryHint } from "./client.js";
|
|
8
8
|
import { resolveCommand, listCommands, UsageError } from "./commands.js";
|
|
9
9
|
import {
|
|
10
10
|
createContextQuery,
|
|
@@ -86,17 +86,21 @@ function printWarnings(result, stderr) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/** Print an error, honoring --json, to stderr. */
|
|
90
|
-
function printError(err, json, stderr) {
|
|
89
|
+
/** Print an error (plus an optional retry hint), honoring --json, to stderr. */
|
|
90
|
+
function printError(err, json, stderr, hint) {
|
|
91
91
|
const code = err instanceof GatewayError ? err.code : err.name;
|
|
92
92
|
|
|
93
93
|
if (json) {
|
|
94
|
-
|
|
94
|
+
const error = { code, message: err.message, delivered: err.delivered, hint };
|
|
95
|
+
|
|
96
|
+
stderr(JSON.stringify({ ok: false, error }, null, 2));
|
|
95
97
|
|
|
96
98
|
return;
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
stderr(`error${code ? ` [${code}]` : ""}: ${err.message}`);
|
|
102
|
+
|
|
103
|
+
if (hint) stderr(`hint: ${hint}`);
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
/**
|
|
@@ -231,7 +235,7 @@ export async function run(argv, io = {}) {
|
|
|
231
235
|
return 0;
|
|
232
236
|
} catch (err) {
|
|
233
237
|
if (err instanceof UsageError || err instanceof GatewayError) {
|
|
234
|
-
printError(err, Boolean(flags.json), stderr);
|
|
238
|
+
printError(err, Boolean(flags.json), stderr, retryHint(err, command.retryUnsafe));
|
|
235
239
|
|
|
236
240
|
return 1;
|
|
237
241
|
}
|
package/src/client.js
CHANGED
|
@@ -9,15 +9,34 @@ export class GatewayError extends Error {
|
|
|
9
9
|
* @param {string} [code]
|
|
10
10
|
* @param {unknown} [result] the envelope's `result`, which some failures still
|
|
11
11
|
* carry — `/context` reports its ambiguity candidates there.
|
|
12
|
+
* @param {boolean} [delivered] on a TIMEOUT, whether the profile had already
|
|
13
|
+
* picked the action up (undefined from daemons that predate the flag).
|
|
12
14
|
*/
|
|
13
|
-
constructor(message, code, result) {
|
|
15
|
+
constructor(message, code, result, delivered) {
|
|
14
16
|
super(message);
|
|
15
17
|
this.name = "GatewayError";
|
|
16
18
|
this.code = code;
|
|
17
19
|
this.result = result;
|
|
20
|
+
this.delivered = delivered;
|
|
18
21
|
}
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
export const RETRY_HINT =
|
|
25
|
+
"The action may have completed — verify (e.g. findPrByIssueKey) before retrying.";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The "verify before retrying" hint for a TIMEOUT of a retry-unsafe action
|
|
29
|
+
* that may already have run; undefined when a retry is safe.
|
|
30
|
+
* @param {unknown} err
|
|
31
|
+
* @param {boolean} [retryUnsafe]
|
|
32
|
+
*/
|
|
33
|
+
export function retryHint(err, retryUnsafe) {
|
|
34
|
+
const mayHaveRun =
|
|
35
|
+
err instanceof GatewayError && err.code === "TIMEOUT" && err.delivered !== false;
|
|
36
|
+
|
|
37
|
+
return retryUnsafe && mayHaveRun ? RETRY_HINT : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
/**
|
|
22
41
|
* Turn a network-level failure into an actionable message. ECONNREFUSED almost
|
|
23
42
|
* always means the daemon isn't running.
|
|
@@ -83,5 +102,6 @@ export async function gatewayRequest(config, method, path, body) {
|
|
|
83
102
|
error.message ?? `Gateway error (HTTP ${response.status}).`,
|
|
84
103
|
error.code,
|
|
85
104
|
envelope?.result,
|
|
105
|
+
error.delivered,
|
|
86
106
|
);
|
|
87
107
|
}
|
package/src/commands.js
CHANGED
|
@@ -79,6 +79,37 @@ function toText(flag, raw) {
|
|
|
79
79
|
return text;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** Read a value-taking flag verbatim (no trimming), failing when it was given no value. */
|
|
83
|
+
function toRawText(flag, raw) {
|
|
84
|
+
if (raw === true) throw new UsageError(`"--${flag}" requires a value.`);
|
|
85
|
+
|
|
86
|
+
return raw === undefined ? undefined : String(raw);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The issue key whose branch template names `branch`: --issue, else — only when no
|
|
91
|
+
* branch was named — the inferred key, so an unrelated checkout never overrides it.
|
|
92
|
+
*/
|
|
93
|
+
function templateIssueKey(branch, flags, context) {
|
|
94
|
+
return toText("issue", flags.issue) ?? (branch ? undefined : context.issueKey);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** buildArgs for a command whose only arg is an issue key: positional, else --issue / inferred. */
|
|
98
|
+
function issueKeyArgs(name) {
|
|
99
|
+
return ([issueKey], _flags, context = {}) => {
|
|
100
|
+
const key = issueKey ?? context.issueKey;
|
|
101
|
+
|
|
102
|
+
if (!key) {
|
|
103
|
+
throw new UsageError(
|
|
104
|
+
`${name} needs an issue key: pass it positionally or with --issue ` +
|
|
105
|
+
"(none could be inferred from the current branch or directory).",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return [key];
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
82
113
|
/** A bad-invocation error (wrong/missing args) — printed with the command usage. */
|
|
83
114
|
export class UsageError extends Error {
|
|
84
115
|
constructor(message) {
|
|
@@ -93,6 +124,7 @@ export class UsageError extends Error {
|
|
|
93
124
|
* @property {string} [path] For GET commands.
|
|
94
125
|
* @property {string} [action] Gateway action name for POST commands.
|
|
95
126
|
* @property {boolean} [destructive] Requires --force/--yes (gated by cli.js).
|
|
127
|
+
* @property {boolean} [retryUnsafe] A retry may repeat its side effect; a TIMEOUT warns to verify first.
|
|
96
128
|
* @property {(pos: string[], flags: Record<string, unknown>, context?: object) => unknown[]} [buildArgs]
|
|
97
129
|
* @property {string} usage One-line usage string.
|
|
98
130
|
*/
|
|
@@ -118,16 +150,33 @@ const COMMANDS = {
|
|
|
118
150
|
|
|
119
151
|
"branch create": {
|
|
120
152
|
method: "POST",
|
|
153
|
+
retryUnsafe: true,
|
|
121
154
|
action: "createBranch",
|
|
122
|
-
usage:
|
|
123
|
-
|
|
124
|
-
|
|
155
|
+
usage:
|
|
156
|
+
"branch create [branchName] [--issue KEY] [--project KEY]" +
|
|
157
|
+
" (the repo's branch template names the branch from the issue, incl. {parentIssueKey};" +
|
|
158
|
+
" branchName is only a fallback; an existing branch is reused)",
|
|
159
|
+
buildArgs: ([branchName], flags, context = {}) => {
|
|
160
|
+
const issueKey = templateIssueKey(branchName, flags, context);
|
|
161
|
+
|
|
162
|
+
if (!branchName && !issueKey) {
|
|
163
|
+
throw new UsageError("branch create needs an issue key (--issue) or a <branchName>.");
|
|
164
|
+
}
|
|
125
165
|
|
|
126
|
-
return [branchName];
|
|
166
|
+
return [branchName, issueKey];
|
|
127
167
|
},
|
|
128
168
|
},
|
|
169
|
+
"branch resolve": {
|
|
170
|
+
method: "POST",
|
|
171
|
+
action: "resolveBranchName",
|
|
172
|
+
usage:
|
|
173
|
+
"branch resolve [issueKey] [--issue KEY] [--project KEY]" +
|
|
174
|
+
" (prints the issue's configured branch name and whether it exists; read-only)",
|
|
175
|
+
buildArgs: issueKeyArgs("branch resolve"),
|
|
176
|
+
},
|
|
129
177
|
"branch delete": {
|
|
130
178
|
method: "POST",
|
|
179
|
+
retryUnsafe: true,
|
|
131
180
|
action: "deleteBranch",
|
|
132
181
|
destructive: true,
|
|
133
182
|
usage: "branch delete <branchName> [--issue KEY] [--project KEY] --force",
|
|
@@ -140,13 +189,15 @@ const COMMANDS = {
|
|
|
140
189
|
|
|
141
190
|
"commit template": {
|
|
142
191
|
method: "POST",
|
|
192
|
+
retryUnsafe: true,
|
|
143
193
|
action: "commitByTemplate",
|
|
144
194
|
usage:
|
|
145
195
|
'commit template <worktreePath> [--values \'{"key":"val"}\'] [--no-push] [--issue KEY] [--project KEY]' +
|
|
146
196
|
" (--values keys fill the project's commit template, default" +
|
|
147
197
|
' "{summary}\\n\\n{changesList}\\n\\n{issueSlug}": summary, changesList (string array,' +
|
|
148
198
|
" rendered as bullets), issueSlug, prNumber; missing keys are left empty and reported" +
|
|
149
|
-
" as warnings on stderr
|
|
199
|
+
" as warnings on stderr; commits the worktree's checked-out branch, so check out the" +
|
|
200
|
+
" one `branch resolve` prints first)",
|
|
150
201
|
buildArgs: ([worktreePath], flags) => {
|
|
151
202
|
if (!worktreePath) throw new UsageError("commit template requires <worktreePath>.");
|
|
152
203
|
|
|
@@ -159,22 +210,26 @@ const COMMANDS = {
|
|
|
159
210
|
|
|
160
211
|
"mr create": {
|
|
161
212
|
method: "POST",
|
|
213
|
+
retryUnsafe: true,
|
|
162
214
|
action: "createPullRequest",
|
|
163
215
|
usage:
|
|
164
|
-
"mr create <title>
|
|
216
|
+
"mr create <title> [headBranch] [--body <text>] [--base <branch>] [--type <idOrLabel>]" +
|
|
165
217
|
" [--issue KEY] [--project KEY]" +
|
|
166
|
-
" (
|
|
218
|
+
" (the repo's branch template picks the head branch from the issue; headBranch is only" +
|
|
219
|
+
" a fallback; --base defaults to the repo's configured base branch, expanding a {year}/{number} pattern;" +
|
|
167
220
|
" --type is auto-selected by matching PR type names against the title prefix, branch prefix," +
|
|
168
221
|
" Jira issue type and title keywords, falling back to the first configured type)",
|
|
169
|
-
buildArgs: ([title, headBranch], flags) => {
|
|
170
|
-
|
|
171
|
-
|
|
222
|
+
buildArgs: ([title, headBranch], flags, context = {}) => {
|
|
223
|
+
const issueKey = templateIssueKey(headBranch, flags, context);
|
|
224
|
+
|
|
225
|
+
if (!title || (!headBranch && !issueKey)) {
|
|
226
|
+
throw new UsageError(
|
|
227
|
+
"mr create requires <title> and an issue key (--issue) or <headBranch>.",
|
|
228
|
+
);
|
|
172
229
|
}
|
|
173
230
|
|
|
174
231
|
const baseBranch = toBranch("base", flags.base);
|
|
175
232
|
const prType = toText("type", flags.type);
|
|
176
|
-
// Only an explicit --issue is forwarded; the gateway otherwise parses the head branch.
|
|
177
|
-
const issueKey = toText("issue", flags.issue);
|
|
178
233
|
|
|
179
234
|
// Trailing undefined is trimmed so the gateway resolves the repo's own
|
|
180
235
|
// configured base branch (and expands a pattern) instead of seeing null.
|
|
@@ -199,6 +254,7 @@ const COMMANDS = {
|
|
|
199
254
|
},
|
|
200
255
|
"mr close": {
|
|
201
256
|
method: "POST",
|
|
257
|
+
retryUnsafe: true,
|
|
202
258
|
action: "closePullRequest",
|
|
203
259
|
destructive: true,
|
|
204
260
|
usage: "mr close <prId> [--issue KEY] [--project KEY] --force",
|
|
@@ -206,6 +262,7 @@ const COMMANDS = {
|
|
|
206
262
|
},
|
|
207
263
|
"mr merge": {
|
|
208
264
|
method: "POST",
|
|
265
|
+
retryUnsafe: true,
|
|
209
266
|
action: "mergePullRequest",
|
|
210
267
|
destructive: true,
|
|
211
268
|
usage:
|
|
@@ -258,17 +315,36 @@ const COMMANDS = {
|
|
|
258
315
|
usage: "mr find [issueKey] [--issue KEY] [--project KEY]",
|
|
259
316
|
// The positional wins, then --issue, then the issue key inferred from the
|
|
260
317
|
// branch — so `devflow mr find` on `ABC-123-login` needs no argument at all.
|
|
261
|
-
buildArgs: (
|
|
262
|
-
|
|
318
|
+
buildArgs: issueKeyArgs("mr find"),
|
|
319
|
+
},
|
|
320
|
+
"mr get": {
|
|
321
|
+
method: "POST",
|
|
322
|
+
action: "getPullRequest",
|
|
323
|
+
usage: "mr get <prId> [--issue KEY] [--project KEY] (title, description, state; read-only)",
|
|
324
|
+
buildArgs: ([prId]) => [toInt("prId", prId)],
|
|
325
|
+
},
|
|
326
|
+
"mr update": {
|
|
327
|
+
method: "POST",
|
|
328
|
+
action: "updatePullRequest",
|
|
329
|
+
usage:
|
|
330
|
+
"mr update <prId> [--title <text>] [--description <text> | --append-description <text>]" +
|
|
331
|
+
" [--issue KEY] [--project KEY]" +
|
|
332
|
+
" (--title/--description replace; --append-description adds text after a `---` rule" +
|
|
333
|
+
" and is skipped if it already ends the description after that rule)",
|
|
334
|
+
buildArgs: ([prId], flags) => {
|
|
335
|
+
const title = toText("title", flags.title);
|
|
336
|
+
const body = toRawText("description", flags.description);
|
|
337
|
+
const appendBody = toRawText("append-description", flags["append-description"]);
|
|
263
338
|
|
|
264
|
-
if (
|
|
265
|
-
throw new UsageError(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
339
|
+
if (title === undefined && body === undefined && appendBody === undefined) {
|
|
340
|
+
throw new UsageError("mr update needs --title, --description or --append-description.");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (body !== undefined && appendBody !== undefined) {
|
|
344
|
+
throw new UsageError("mr update takes --description or --append-description, not both.");
|
|
269
345
|
}
|
|
270
346
|
|
|
271
|
-
return [
|
|
347
|
+
return [toInt("prId", prId), { title, body, appendBody }];
|
|
272
348
|
},
|
|
273
349
|
},
|
|
274
350
|
"mr types": {
|
|
@@ -309,6 +385,7 @@ const COMMANDS = {
|
|
|
309
385
|
},
|
|
310
386
|
"pipeline retry": {
|
|
311
387
|
method: "POST",
|
|
388
|
+
retryUnsafe: true,
|
|
312
389
|
action: "restartFailedJobs",
|
|
313
390
|
usage: "pipeline retry <pipelineOrRunId> [--all] [--job <jobId>] [--issue KEY] [--project KEY]",
|
|
314
391
|
buildArgs: ([id], flags) => {
|
|
@@ -329,6 +406,7 @@ const COMMANDS = {
|
|
|
329
406
|
|
|
330
407
|
"slack notify": {
|
|
331
408
|
method: "POST",
|
|
409
|
+
retryUnsafe: true,
|
|
332
410
|
action: "notifySlack",
|
|
333
411
|
usage:
|
|
334
412
|
'slack notify <message> [--channel ID] [--values \'{"key":"val"}\'] [--issue KEY] [--project KEY]',
|