@devflow-one/cli 0.1.2 → 0.1.5

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,13 +182,16 @@ devflow mr ready 12 --profile prof-1
182
182
 
183
183
  ### Destructive actions
184
184
 
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:
185
+ `branch delete`, `mr close` and `mr merge` are destructive. They are **refused
186
+ unless you confirm** with `--force` (or `--yes`); the gateway enforces the same
187
+ gate server-side, so the confirmation can't be bypassed:
188
188
 
189
189
  ```sh
190
190
  devflow branch delete old --project ABC # refused
191
191
  devflow branch delete old --project ABC --force # executes
192
+
193
+ # merge a PR/MR, squashing and cleaning up the source branch
194
+ devflow mr merge 42 --method squash --delete-source-branch --force
192
195
  ```
193
196
 
194
197
  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.2",
3
+ "version": "0.1.5",
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/cli.js CHANGED
@@ -125,12 +125,15 @@ export async function run(argv, io = {}) {
125
125
  const { key, command, rest } = match;
126
126
 
127
127
  let config;
128
+ let configError = null;
128
129
 
129
130
  try {
130
131
  config = resolveConfig();
131
132
  } catch (err) {
132
133
  if (!(err instanceof ConfigError)) throw err;
133
134
 
135
+ configError = err;
136
+
134
137
  // `where` is a diagnostic: it must still report what the checkout implies
135
138
  // when there is no daemon to ask, since that is exactly when it is consulted.
136
139
  if (command.method !== "LOCAL") {
@@ -149,7 +152,9 @@ export async function run(argv, io = {}) {
149
152
  queryContext: config
150
153
  ? createContextQuery(config, request)
151
154
  : async () => {
152
- throw new Error("the DevFlow desktop app is not running (no gateway token).");
155
+ // Report only what was actually checked — the token file. Whether the
156
+ // app is running is unknown here, since the gateway was never called.
157
+ throw new Error(configError?.message ?? "no gateway token available.");
153
158
  },
154
159
  });
155
160
 
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;
@@ -144,6 +158,20 @@ const COMMANDS = {
144
158
  usage: "mr close <prId> [--issue KEY] [--project KEY] --force",
145
159
  buildArgs: ([prId]) => [toInt("prId", prId)],
146
160
  },
161
+ "mr merge": {
162
+ method: "POST",
163
+ action: "mergePullRequest",
164
+ destructive: true,
165
+ usage:
166
+ "mr merge <prId> [--method merge|squash|rebase] [--delete-source-branch] " +
167
+ "[--issue KEY] [--project KEY] --force",
168
+ buildArgs: ([prId], flags) => {
169
+ const mergeMethod = flags.method === undefined ? undefined : toMergeMethod(flags.method);
170
+ const deleteSourceBranch = flags["delete-source-branch"] ? true : undefined;
171
+
172
+ return [toInt("prId", prId), { mergeMethod, deleteSourceBranch }];
173
+ },
174
+ },
147
175
  "mr assign": {
148
176
  method: "POST",
149
177
  action: "assignPr",
package/src/config.js CHANGED
@@ -1,18 +1,12 @@
1
1
  // Resolves how the CLI reaches the local gateway: base URL (port) and the Bearer
2
- // token. The token is read from the same userData dir the desktop daemon writes it
3
- // to (see desktop/src/gateway/token.js) so scripts "just work" with no setup. Env
4
- // vars override for tests / non-standard installs.
2
+ // token. Path/port resolution lives in the generated gatewayDataDir.js copy
3
+ // (authored at shared/gatewayDataDir.js) so the CLI, the MCP server and the
4
+ // desktop daemon can never disagree about where the token lives. Env vars
5
+ // override for tests / non-standard installs.
5
6
 
6
7
  import fs from "node:fs";
7
- import path from "node:path";
8
- import os from "node:os";
9
8
 
10
- /** Loopback host — the gateway only ever binds 127.0.0.1. */
11
- const HOST = "127.0.0.1";
12
- /** Default gateway port (matches desktop server DEFAULT_PORT). */
13
- const DEFAULT_PORT = 8787;
14
- /** Token filename under the app's userData dir (matches token.js TOKEN_FILE). */
15
- const TOKEN_FILE = "gateway-token";
9
+ import { describeMissingToken, resolveGatewayBaseUrl, resolveTokenPath } from "./gatewayDataDir.js";
16
10
 
17
11
  /** A tagged config-resolution failure, so the CLI can print a clear message. */
18
12
  export class ConfigError extends Error {
@@ -22,38 +16,9 @@ export class ConfigError extends Error {
22
16
  }
23
17
  }
24
18
 
25
- /**
26
- * The daemon's userData dir, matching desktop/src/gateway/token.js's fallback
27
- * (the CLI runs under plain node, never Electron, so it always uses the fallback).
28
- * @returns {string}
29
- */
30
- function resolveDataDir() {
31
- const home = os.homedir();
32
-
33
- if (process.platform === "darwin") {
34
- return path.join(home, "Library", "Application Support", "devflow");
35
- }
36
-
37
- if (process.platform === "win32") {
38
- const base = process.env.APPDATA || path.join(home, "AppData", "Roaming");
39
-
40
- return path.join(base, "devflow");
41
- }
42
-
43
- return path.join(home, ".config", "devflow");
44
- }
45
-
46
- /** Resolve the gateway port from `DEVFLOW_GATEWAY_PORT`, else the default. */
47
- function resolvePort() {
48
- const port = Number.parseInt(process.env.DEVFLOW_GATEWAY_PORT, 10);
49
-
50
- return Number.isInteger(port) && port > 0 && port < 65536 ? port : DEFAULT_PORT;
51
- }
52
-
53
19
  /**
54
20
  * Read the gateway token: `DEVFLOW_GATEWAY_TOKEN` wins, else the token file the
55
- * daemon persisted. Throws {@link ConfigError} with an actionable message when
56
- * neither is available.
21
+ * daemon persisted. Throws {@link ConfigError} naming the exact path probed.
57
22
  * @returns {string}
58
23
  */
59
24
  function resolveToken() {
@@ -61,23 +26,20 @@ function resolveToken() {
61
26
 
62
27
  if (fromEnv) return fromEnv;
63
28
 
64
- const tokenPath = path.join(resolveDataDir(), TOKEN_FILE);
29
+ const tokenPath = resolveTokenPath();
30
+ let contents;
65
31
 
66
32
  try {
67
- const token = fs.readFileSync(tokenPath, "utf8").trim();
33
+ contents = fs.readFileSync(tokenPath, "utf8");
34
+ } catch {
35
+ throw new ConfigError(describeMissingToken(tokenPath, "missing"));
36
+ }
68
37
 
69
- if (token) return token;
38
+ const token = contents.trim();
70
39
 
71
- throw new ConfigError(`Gateway token file is empty: ${tokenPath}`);
72
- } catch (err) {
73
- if (err instanceof ConfigError) throw err;
40
+ if (!token) throw new ConfigError(describeMissingToken(tokenPath, "empty"));
74
41
 
75
- throw new ConfigError(
76
- `Could not read the gateway token at ${tokenPath}. ` +
77
- `Is the DevFlow desktop app running? ` +
78
- `(Or set DEVFLOW_GATEWAY_TOKEN to override.)`,
79
- );
80
- }
42
+ return token;
81
43
  }
82
44
 
83
45
  /**
@@ -85,10 +47,8 @@ function resolveToken() {
85
47
  * @returns {{ baseUrl: string, token: string }}
86
48
  */
87
49
  export function resolveConfig() {
88
- const port = resolvePort();
89
-
90
50
  return {
91
- baseUrl: `http://${HOST}:${port}`,
51
+ baseUrl: resolveGatewayBaseUrl(),
92
52
  token: resolveToken(),
93
53
  };
94
54
  }
@@ -0,0 +1,86 @@
1
+ // GENERATED FILE — do not edit.
2
+ // Source of truth: shared/gatewayDataDir.js. Regenerate with:
3
+ // node scripts/sync-context-resolver.mjs
4
+
5
+ // Where the desktop daemon keeps its gateway state (bearer token, routing mirror),
6
+ // and where the CLI and MCP server read it back from. Authored once here because
7
+ // three independent copies of this logic is exactly how the clients ended up
8
+ // reading a directory the app never wrote to.
9
+ //
10
+ // The daemon itself resolves via Electron's `app.getPath("userData")`; this module
11
+ // is that path's plain-node equivalent. Electron derives userData from the app's
12
+ // package `name`, so APP_DIR_NAME must track `desktop/package.json`'s `name`.
13
+
14
+ import path from "node:path";
15
+ import os from "node:os";
16
+
17
+ /** Matches `desktop/package.json` `name` — Electron's userData dir is `<appData>/<name>`. */
18
+ export const APP_DIR_NAME = "devflow";
19
+
20
+ /** Filename the daemon writes the gateway token to, under the data dir. */
21
+ export const TOKEN_FILE = "gateway-token";
22
+
23
+ /** Default gateway port — must match desktop/src/gateway/server.js. */
24
+ export const DEFAULT_PORT = 8787;
25
+
26
+ /** Loopback host; the gateway only ever binds 127.0.0.1. */
27
+ export const HOST = "127.0.0.1";
28
+
29
+ /**
30
+ * Resolve the daemon's data dir for the current platform.
31
+ * `DEVFLOW_GATEWAY_DATA_DIR` overrides, so tests never touch real user data.
32
+ * @returns {string} absolute directory path
33
+ */
34
+ export function resolveGatewayDataDir() {
35
+ if (process.env.DEVFLOW_GATEWAY_DATA_DIR) return process.env.DEVFLOW_GATEWAY_DATA_DIR;
36
+
37
+ const home = os.homedir();
38
+
39
+ if (process.platform === "darwin") {
40
+ return path.join(home, "Library", "Application Support", APP_DIR_NAME);
41
+ }
42
+
43
+ if (process.platform === "win32") {
44
+ const base = process.env.APPDATA || path.join(home, "AppData", "Roaming");
45
+
46
+ return path.join(base, APP_DIR_NAME);
47
+ }
48
+
49
+ return path.join(home, ".config", APP_DIR_NAME);
50
+ }
51
+
52
+ /** Absolute path of the gateway token file. */
53
+ export function resolveTokenPath() {
54
+ return path.join(resolveGatewayDataDir(), TOKEN_FILE);
55
+ }
56
+
57
+ /** Resolve the gateway port from `DEVFLOW_GATEWAY_PORT`, else {@link DEFAULT_PORT}. */
58
+ export function resolveGatewayPort() {
59
+ const parsed = Number.parseInt(process.env.DEVFLOW_GATEWAY_PORT ?? "", 10);
60
+
61
+ return Number.isInteger(parsed) && parsed > 0 && parsed < 65536 ? parsed : DEFAULT_PORT;
62
+ }
63
+
64
+ /** Gateway base URL, honouring `DEVFLOW_GATEWAY_PORT`. */
65
+ export function resolveGatewayBaseUrl() {
66
+ return `http://${HOST}:${resolveGatewayPort()}`;
67
+ }
68
+
69
+ /**
70
+ * Message for "the token file isn't readable". States only what was checked —
71
+ * the probed path — and never asserts why, since nothing here touches the network.
72
+ * @param {string} tokenPath
73
+ * @param {"missing" | "empty"} [reason]
74
+ */
75
+ export function describeMissingToken(tokenPath, reason = "missing") {
76
+ const what =
77
+ reason === "empty"
78
+ ? `The gateway token file at ${tokenPath} is empty.`
79
+ : `No gateway token file at ${tokenPath}.`;
80
+
81
+ return (
82
+ `${what} That file is written by the DevFlow desktop app on first run. ` +
83
+ `Set DEVFLOW_GATEWAY_TOKEN to use a token from elsewhere, or ` +
84
+ `DEVFLOW_GATEWAY_DATA_DIR if the app stores its data in another location.`
85
+ );
86
+ }