@devflow-one/cli 0.1.6 → 0.1.8

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
@@ -166,6 +166,17 @@ devflow mr create "Title" feat/x --body "desc" --project ABC
166
166
  # pattern is expanded to the newest matching remote branch), or pick one
167
167
  devflow mr create "Title" feat/x --base release/2026.4 --project ABC
168
168
 
169
+ # PR type: the project's first configured type by default — it supplies the
170
+ # description/title templates and the labels the PR is stamped with. Pass its id
171
+ # or its label to pick another; an unrecognized value falls back to that default.
172
+ devflow mr create "Title" feat/x --type Bugfix --project ABC
173
+ devflow mr types --project ABC # configured types: id, label, default flag
174
+
175
+ # issue key: parsed from the head branch by default; --issue sets it explicitly
176
+ # ({parentWithChildIssueKeys}/{issueKey}/{issueTitle} template tokens) for a branch without one.
177
+ # {issueTitle} needs the Jira API token set in Settings, else it renders empty.
178
+ devflow mr create "Title" feat/x --issue ABC-123
179
+
169
180
  # routed by issue key (project key = prefix before the last '-')
170
181
  devflow mr find ABC-123
171
182
  devflow pipeline status 42 deadbeef --issue ABC-9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devflow-one/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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
@@ -77,6 +77,15 @@ function printResult(result, json, stdout) {
77
77
  }
78
78
  }
79
79
 
80
+ /** Echo a result's `warnings` to stderr so they stay visible next to --json stdout. */
81
+ function printWarnings(result, stderr) {
82
+ const warnings = Array.isArray(result?.warnings) ? result.warnings : [];
83
+
84
+ for (const warning of warnings) {
85
+ stderr(`warning: ${warning}`);
86
+ }
87
+ }
88
+
80
89
  /** Print an error, honoring --json, to stderr. */
81
90
  function printError(err, json, stderr) {
82
91
  const code = err instanceof GatewayError ? err.code : err.name;
@@ -217,6 +226,7 @@ export async function run(argv, io = {}) {
217
226
  const result = await request(config, "POST", `/actions/${action}`, body);
218
227
 
219
228
  printResult(result, Boolean(flags.json), stdout);
229
+ printWarnings(result, stderr);
220
230
 
221
231
  return 0;
222
232
  } catch (err) {
package/src/commands.js CHANGED
@@ -66,6 +66,19 @@ function toBranch(flag, raw) {
66
66
  return branch;
67
67
  }
68
68
 
69
+ /** Parse a value-taking flag as a non-blank string, or fail clearly. */
70
+ function toText(flag, raw) {
71
+ if (raw === undefined) return undefined;
72
+
73
+ const text = String(raw).trim();
74
+
75
+ if (raw === true || text === "") {
76
+ throw new UsageError(`"--${flag}" requires a value.`);
77
+ }
78
+
79
+ return text;
80
+ }
81
+
69
82
  /** A bad-invocation error (wrong/missing args) — printed with the command usage. */
70
83
  export class UsageError extends Error {
71
84
  constructor(message) {
@@ -130,7 +143,10 @@ const COMMANDS = {
130
143
  action: "commitByTemplate",
131
144
  usage:
132
145
  '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)",
146
+ " (--values keys fill the project's commit template, default" +
147
+ ' "{summary}\\n\\n{changesList}\\n\\n{issueSlug}": summary, changesList (string array,' +
148
+ " rendered as bullets), issueSlug, prNumber; missing keys are left empty and reported" +
149
+ " as warnings on stderr)",
134
150
  buildArgs: ([worktreePath], flags) => {
135
151
  if (!worktreePath) throw new UsageError("commit template requires <worktreePath>.");
136
152
 
@@ -145,20 +161,27 @@ const COMMANDS = {
145
161
  method: "POST",
146
162
  action: "createPullRequest",
147
163
  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)",
164
+ "mr create <title> <headBranch> [--body <text>] [--base <branch>] [--type <idOrLabel>]" +
165
+ " [--issue KEY] [--project KEY]" +
166
+ " (--base defaults to the repo's configured base branch, expanding a {year}/{number} pattern;" +
167
+ " --type defaults to the project's first configured PR type)",
150
168
  buildArgs: ([title, headBranch], flags) => {
151
169
  if (!title || !headBranch) {
152
170
  throw new UsageError("mr create requires <title> and <headBranch>.");
153
171
  }
154
172
 
155
173
  const baseBranch = toBranch("base", flags.base);
174
+ const prType = toText("type", flags.type);
175
+ // Only an explicit --issue is forwarded; the gateway otherwise parses the head branch.
176
+ const issueKey = toText("issue", flags.issue);
177
+
178
+ // Trailing undefined is trimmed so the gateway resolves the repo's own
179
+ // configured base branch (and expands a pattern) instead of seeing null.
180
+ const args = [title, flags.body ?? "", headBranch, baseBranch, prType, issueKey];
156
181
 
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];
182
+ while (args.length > 3 && args[args.length - 1] === undefined) args.pop();
183
+
184
+ return args;
162
185
  },
163
186
  },
164
187
  "mr ready": {
@@ -247,6 +270,12 @@ const COMMANDS = {
247
270
  return [key];
248
271
  },
249
272
  },
273
+ "mr types": {
274
+ method: "POST",
275
+ action: "listPrTypes",
276
+ usage: "mr types [--issue KEY] [--project KEY]",
277
+ buildArgs: () => [],
278
+ },
250
279
  "mr fix-prompt": {
251
280
  method: "POST",
252
281
  action: "getFixCommentsPrompt",
package/src/context.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // The resolver itself (contextResolver.js) is generated from shared/ and must not
6
6
  // be edited here; everything CLI-specific lives in this file.
7
7
 
8
- import { resolveContext } from "./contextResolver.js";
8
+ import { issueKeyToProjectKey, resolveContext } from "./contextResolver.js";
9
9
  import { createGitRunner } from "./gitFacts.js";
10
10
  import { GatewayError } from "./client.js";
11
11
  import { UsageError } from "./commands.js";
@@ -84,13 +84,7 @@ export function resolveCliContext(
84
84
  export function projectKeyFrom(context) {
85
85
  if (context.projectKey) return context.projectKey.toUpperCase();
86
86
 
87
- if (context.issueKey) {
88
- const lastDash = context.issueKey.lastIndexOf("-");
89
-
90
- return (lastDash === -1 ? context.issueKey : context.issueKey.slice(0, lastDash)).toUpperCase();
91
- }
92
-
93
- return undefined;
87
+ return issueKeyToProjectKey(context.issueKey);
94
88
  }
95
89
 
96
90
  /** Describe one ambiguity candidate as `PROJECT/repo (profile)`. */
@@ -203,10 +197,16 @@ export function offlineProfileError(context) {
203
197
 
204
198
  /**
205
199
  * How the resolved profile looks to the daemon. Shown inline on the profile row
206
- * so `devflow where` answers "would a call work?" without a second lookup.
200
+ * so `devflow where` answers "would a call work?" without a second lookup. A
201
+ * connected profile also names the extension bundle it is running, so a stale
202
+ * `dist/` can be spotted without guessing.
207
203
  */
208
204
  function describeLiveness(context) {
209
- if (context.profileAlive === true) return "connected";
205
+ if (context.profileAlive === true) {
206
+ return context.buildStamp
207
+ ? `connected; extension build ${context.buildStamp}`
208
+ : "connected; extension build unknown (this build predates the stamp)";
209
+ }
210
210
 
211
211
  if (context.profileAlive === false) {
212
212
  return "NOT connected; open this Chrome profile before running actions";
@@ -56,11 +56,11 @@ function clean(value) {
56
56
 
57
57
  /**
58
58
  * The project a key or issue key belongs to: split on the LAST `-` and uppercase
59
- * (`abc-123` → `ABC`). Mirrors `issueKeyToProjectKey` in the daemon's routing.js.
60
- * @param {string | undefined} value
61
- * @returns {string | undefined}
59
+ * (`abc-123` → `ABC`). A value with no `-` is already a project key.
60
+ * @param {unknown} value
61
+ * @returns {string | undefined} `undefined` for a non-string or blank value
62
62
  */
63
- function projectOf(value) {
63
+ export function issueKeyToProjectKey(value) {
64
64
  const trimmed = clean(value);
65
65
 
66
66
  if (!trimmed) return undefined;
@@ -163,7 +163,8 @@ function fill(resolved, source, origin) {
163
163
  * @param {(q: { cwd?: string, remote?: string, branch?: string }) => Promise<object | null>} [options.queryContext]
164
164
  * @returns {Promise<{
165
165
  * projectKey?: string, issueKey?: string, repo?: string, baseBranch?: string,
166
- * profile?: string, profileAlive?: boolean | null, matchedBy: Record<string, string>,
166
+ * profile?: string, profileAlive?: boolean | null, buildStamp?: string | null,
167
+ * matchedBy: Record<string, string>,
167
168
  * candidates?: object[], ambiguous?: boolean, warnings: string[]
168
169
  * }>}
169
170
  */
@@ -226,16 +227,10 @@ export async function resolveContext({
226
227
  // so the reported precedence step reflects the real reason, not a guess.
227
228
  const origin = context.matchedBy === "remote" ? "gitRemote" : "localPath";
228
229
 
229
- // The daemon answered "this CHECKOUT belongs to project P". Its repo, profile,
230
- // liveness and issue key all describe P — they are only meaningful together.
231
- // If the caller already pinned a different project, adopting them field-by-field
232
- // would aim the action at P's repository under the caller's key: silent
233
- // mis-targeting, and worse than a failure because nothing looks wrong. So the
234
- // whole group is discarded on disagreement. Note this compares the KEYS, not
235
- // merely where they came from — a caller who names the project the checkout
236
- // already matches is agreeing, and must still get its repo/profile.
237
- const pinned = projectOf(resolved.projectKey) ?? projectOf(resolved.issueKey);
238
- const matched = projectOf(context.projectKey);
230
+ // A pinned project that disagrees with the match discards its repo/profile/issue together.
231
+ const pinned =
232
+ issueKeyToProjectKey(resolved.projectKey) ?? issueKeyToProjectKey(resolved.issueKey);
233
+ const matched = issueKeyToProjectKey(context.projectKey);
239
234
  const conflict = Boolean(pinned && matched && pinned !== matched);
240
235
 
241
236
  if (conflict) {
@@ -272,8 +267,12 @@ export async function resolveContext({
272
267
 
273
268
  // Liveness is only meaningful for the profile we actually resolved to; it lets
274
269
  // consumers name an offline profile instead of printing a bare NO_PROFILE.
270
+ // The build stamp describes that same profile's running extension bundle, so
271
+ // it is gated identically — reporting one profile's bundle beside another's id
272
+ // is exactly the confusion the stamp exists to end.
275
273
  if (!conflict && resolved.profile && resolved.profile === clean(context.profileId)) {
276
274
  resolved.profileAlive = context.profileAlive ?? null;
275
+ resolved.buildStamp = context.buildStamp ?? null;
277
276
  }
278
277
 
279
278
  // The daemon's issue key describes its matched project too, so it is dropped on
@@ -20,7 +20,7 @@ export const APP_DIR_NAME = "devflow";
20
20
  /** Filename the daemon writes the gateway token to, under the data dir. */
21
21
  export const TOKEN_FILE = "gateway-token";
22
22
 
23
- /** Default gateway port — must match desktop/src/gateway/server.js. */
23
+ /** Default gateway port, shared by the daemon and every client. */
24
24
  export const DEFAULT_PORT = 8787;
25
25
 
26
26
  /** Loopback host; the gateway only ever binds 127.0.0.1. */