automata-cli 0.2.0-develop.7 → 0.2.1
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 +57 -9
- package/dist/index.js +953 -23
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# automata-cli
|
|
2
2
|
|
|
3
|
-
A command-line interface tool.
|
|
3
|
+
A command-line interface tool for automating Git and project workflows.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,32 +8,80 @@ A command-line interface tool.
|
|
|
8
8
|
npm install -g automata-cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Quick start
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
automata --help
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
---
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
## `automata config`
|
|
20
20
|
|
|
21
|
-
Launch the interactive configuration wizard
|
|
21
|
+
Launch the interactive configuration wizard to select the remote environment type.
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
automata config
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
Configuration is saved to `.automata/config.json` in the current directory.
|
|
28
|
+
|
|
27
29
|
### `automata config set type <value>`
|
|
28
30
|
|
|
29
|
-
Set
|
|
31
|
+
Set the remote type non-interactively (useful in scripts or CI).
|
|
30
32
|
|
|
31
33
|
```bash
|
|
32
|
-
automata config set type gh # GitHub
|
|
34
|
+
automata config set type gh # GitHub (requires gh CLI)
|
|
33
35
|
automata config set type azdo # Azure DevOps
|
|
34
36
|
```
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## `automata git`
|
|
41
|
+
|
|
42
|
+
Git workflow helpers. Most commands require the [`gh` CLI](https://cli.github.com/) installed and authenticated. `publish-release` only requires `git`.
|
|
43
|
+
|
|
44
|
+
### `automata git get-pr-info`
|
|
45
|
+
|
|
46
|
+
Show the pull request for the current branch, including CI check results.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
automata git get-pr-info # human-readable output
|
|
50
|
+
automata git get-pr-info --json # full PR object as JSON
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `automata git get-pr-comments`
|
|
54
|
+
|
|
55
|
+
List open (unresolved) review thread comments on the current branch's PR.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
automata git get-pr-comments # human-readable output
|
|
59
|
+
automata git get-pr-comments --json # JSON array output
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### `automata git finish-feature`
|
|
63
|
+
|
|
64
|
+
Clean up a merged feature branch: checkout `develop`, pull latest, delete the local branch.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
automata git finish-feature
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Checks that the PR exists and is merged, the working tree is clean, and the remote tracking branch is gone before making any changes.
|
|
71
|
+
|
|
72
|
+
### `automata git publish-release`
|
|
73
|
+
|
|
74
|
+
Execute the full GitFlow release sequence and push to `origin`. Only requires `git`.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
automata git publish-release # auto-detect version from master tag
|
|
78
|
+
automata git publish-release 2.0.0 # explicit version
|
|
79
|
+
automata git publish-release --dry-run # preview without executing
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
When no version is given, the latest semver tag on `master` is detected and the minor segment is incremented (e.g. `1.2.0 → 1.3.0`).
|
|
83
|
+
|
|
84
|
+
---
|
|
37
85
|
|
|
38
86
|
## Development
|
|
39
87
|
|
|
@@ -53,7 +101,7 @@ npm install
|
|
|
53
101
|
### Scripts
|
|
54
102
|
|
|
55
103
|
| Command | Description |
|
|
56
|
-
|
|
104
|
+
|---|---|
|
|
57
105
|
| `npm run build` | Build the CLI with tsup |
|
|
58
106
|
| `npm test` | Build and run tests with vitest |
|
|
59
107
|
| `npm run lint` | Lint source files with ESLint |
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command4 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -44,43 +44,151 @@ function writeConfig(config) {
|
|
|
44
44
|
|
|
45
45
|
// src/config/ConfigWizard.tsx
|
|
46
46
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
47
|
-
var
|
|
47
|
+
var REMOTE_OPTIONS = [
|
|
48
48
|
{ label: "GitHub", value: "gh" },
|
|
49
49
|
{ label: "Azure DevOps", value: "azdo" }
|
|
50
50
|
];
|
|
51
|
+
var TECHNIQUE_OPTIONS = [
|
|
52
|
+
{ label: "By Label", value: "label" },
|
|
53
|
+
{ label: "By Assignee", value: "assignee" },
|
|
54
|
+
{ label: "By Title Contains", value: "title-contains" }
|
|
55
|
+
];
|
|
51
56
|
function ConfigWizard() {
|
|
52
57
|
const existing = readConfig();
|
|
53
|
-
const
|
|
54
|
-
const
|
|
58
|
+
const initialRemoteIndex = REMOTE_OPTIONS.findIndex((o) => o.value === existing.remoteType);
|
|
59
|
+
const initialTechIndex = TECHNIQUE_OPTIONS.findIndex((o) => o.value === existing.issueDiscoveryTechnique);
|
|
60
|
+
const [screen, setScreen] = useState("remote");
|
|
61
|
+
const [selectedRemoteIndex, setSelectedRemoteIndex] = useState(initialRemoteIndex >= 0 ? initialRemoteIndex : 0);
|
|
62
|
+
const [selectedTechIndex, setSelectedTechIndex] = useState(initialTechIndex >= 0 ? initialTechIndex : 0);
|
|
63
|
+
const [discoveryValue, setDiscoveryValue] = useState(existing.issueDiscoveryValue ?? "");
|
|
64
|
+
const [systemPrompt, setSystemPrompt] = useState(existing.claudeSystemPrompt ?? "");
|
|
65
|
+
const [pendingRemote, setPendingRemote] = useState(existing.remoteType ?? "gh");
|
|
66
|
+
const [pendingTechnique, setPendingTechnique] = useState(
|
|
67
|
+
existing.issueDiscoveryTechnique ?? "label"
|
|
68
|
+
);
|
|
55
69
|
const { exit } = useApp();
|
|
56
70
|
useInput((input, key) => {
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
if (screen === "remote") {
|
|
72
|
+
if (key.upArrow) {
|
|
73
|
+
setSelectedRemoteIndex((i) => i > 0 ? i - 1 : REMOTE_OPTIONS.length - 1);
|
|
74
|
+
} else if (key.downArrow) {
|
|
75
|
+
setSelectedRemoteIndex((i) => i < REMOTE_OPTIONS.length - 1 ? i + 1 : 0);
|
|
76
|
+
} else if (key.return) {
|
|
77
|
+
const chosen = REMOTE_OPTIONS[selectedRemoteIndex];
|
|
78
|
+
setPendingRemote(chosen.value);
|
|
79
|
+
if (chosen.value === "gh") {
|
|
80
|
+
setScreen("technique");
|
|
81
|
+
} else {
|
|
82
|
+
writeConfig({ ...existing, remoteType: chosen.value });
|
|
83
|
+
exit();
|
|
84
|
+
}
|
|
85
|
+
} else if (key.escape || key.ctrl && input === "c") {
|
|
86
|
+
exit();
|
|
87
|
+
}
|
|
88
|
+
} else if (screen === "technique") {
|
|
89
|
+
if (key.upArrow) {
|
|
90
|
+
setSelectedTechIndex((i) => i > 0 ? i - 1 : TECHNIQUE_OPTIONS.length - 1);
|
|
91
|
+
} else if (key.downArrow) {
|
|
92
|
+
setSelectedTechIndex((i) => i < TECHNIQUE_OPTIONS.length - 1 ? i + 1 : 0);
|
|
93
|
+
} else if (key.return) {
|
|
94
|
+
const chosen = TECHNIQUE_OPTIONS[selectedTechIndex];
|
|
95
|
+
setPendingTechnique(chosen.value);
|
|
96
|
+
setScreen("value");
|
|
97
|
+
} else if (key.escape || key.ctrl && input === "c") {
|
|
98
|
+
exit();
|
|
99
|
+
}
|
|
100
|
+
} else if (screen === "value") {
|
|
101
|
+
if (key.return) {
|
|
102
|
+
setScreen("system-prompt");
|
|
103
|
+
} else if (key.backspace || key.delete) {
|
|
104
|
+
setDiscoveryValue((v) => v.slice(0, -1));
|
|
105
|
+
} else if (key.escape || key.ctrl && input === "c") {
|
|
106
|
+
exit();
|
|
107
|
+
} else if (input && !key.ctrl && !key.meta) {
|
|
108
|
+
setDiscoveryValue((v) => v + input);
|
|
109
|
+
}
|
|
110
|
+
} else if (screen === "system-prompt") {
|
|
111
|
+
if (key.return) {
|
|
112
|
+
writeConfig({
|
|
113
|
+
...existing,
|
|
114
|
+
remoteType: pendingRemote,
|
|
115
|
+
issueDiscoveryTechnique: pendingTechnique,
|
|
116
|
+
issueDiscoveryValue: discoveryValue || void 0,
|
|
117
|
+
claudeSystemPrompt: systemPrompt || void 0
|
|
118
|
+
});
|
|
119
|
+
exit();
|
|
120
|
+
} else if (key.backspace || key.delete) {
|
|
121
|
+
setSystemPrompt((v) => v.slice(0, -1));
|
|
122
|
+
} else if (key.escape || key.ctrl && input === "c") {
|
|
123
|
+
exit();
|
|
124
|
+
} else if (input && !key.ctrl && !key.meta) {
|
|
125
|
+
setSystemPrompt((v) => v + input);
|
|
126
|
+
}
|
|
67
127
|
}
|
|
68
128
|
});
|
|
129
|
+
if (screen === "remote") {
|
|
130
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, children: [
|
|
131
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Configure Automata" }),
|
|
132
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
133
|
+
/* @__PURE__ */ jsx(Text, { children: "Remote environment type:" }),
|
|
134
|
+
REMOTE_OPTIONS.map((option, index) => /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: index === selectedRemoteIndex ? "cyan" : void 0, children: [
|
|
135
|
+
index === selectedRemoteIndex ? "\u276F " : " ",
|
|
136
|
+
option.label
|
|
137
|
+
] }) }, option.value)),
|
|
138
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
139
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 to move \xB7 Enter to confirm \xB7 Ctrl+C to cancel" })
|
|
140
|
+
] });
|
|
141
|
+
}
|
|
142
|
+
if (screen === "technique") {
|
|
143
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, children: [
|
|
144
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Configure Issue Discovery Technique" }),
|
|
145
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
146
|
+
/* @__PURE__ */ jsx(Text, { children: "How to find the next issue to work on:" }),
|
|
147
|
+
TECHNIQUE_OPTIONS.map((option, index) => /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: index === selectedTechIndex ? "cyan" : void 0, children: [
|
|
148
|
+
index === selectedTechIndex ? "\u276F " : " ",
|
|
149
|
+
option.label
|
|
150
|
+
] }) }, option.value)),
|
|
151
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
152
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 to move \xB7 Enter to confirm \xB7 Ctrl+C to cancel" })
|
|
153
|
+
] });
|
|
154
|
+
}
|
|
155
|
+
if (screen === "value") {
|
|
156
|
+
const techLabel = TECHNIQUE_OPTIONS.find((t) => t.value === pendingTechnique)?.label ?? pendingTechnique;
|
|
157
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, children: [
|
|
158
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Configure Issue Discovery Value" }),
|
|
159
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
160
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
161
|
+
techLabel,
|
|
162
|
+
" value:",
|
|
163
|
+
" ",
|
|
164
|
+
/* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
|
|
165
|
+
discoveryValue,
|
|
166
|
+
/* @__PURE__ */ jsx(Text, { children: "_" })
|
|
167
|
+
] })
|
|
168
|
+
] }),
|
|
169
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
170
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Type value \xB7 Enter to confirm \xB7 Ctrl+C to cancel" })
|
|
171
|
+
] });
|
|
172
|
+
}
|
|
69
173
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, children: [
|
|
70
|
-
/* @__PURE__ */ jsx(Text, { bold: true, children: "Configure
|
|
174
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Configure Claude System Prompt" }),
|
|
71
175
|
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
72
|
-
/* @__PURE__ */
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
176
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
177
|
+
"System prompt (optional):",
|
|
178
|
+
" ",
|
|
179
|
+
/* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
|
|
180
|
+
systemPrompt,
|
|
181
|
+
/* @__PURE__ */ jsx(Text, { children: "_" })
|
|
182
|
+
] })
|
|
183
|
+
] }),
|
|
77
184
|
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
78
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "
|
|
185
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Type prompt \xB7 Enter to save and exit \xB7 Ctrl+C to cancel" })
|
|
79
186
|
] });
|
|
80
187
|
}
|
|
81
188
|
|
|
82
189
|
// src/commands/config.ts
|
|
83
190
|
var VALID_TYPES = ["gh", "azdo"];
|
|
191
|
+
var VALID_TECHNIQUES = ["label", "assignee", "title-contains"];
|
|
84
192
|
var configSetType = new Command("type").description("Set the remote environment type").argument("<value>", "Remote type: gh (GitHub) or azdo (Azure DevOps)").action((value) => {
|
|
85
193
|
if (!VALID_TYPES.includes(value)) {
|
|
86
194
|
process.stderr.write(`Error: invalid type "${value}". Must be one of: ${VALID_TYPES.join(", ")}
|
|
@@ -92,16 +200,838 @@ var configSetType = new Command("type").description("Set the remote environment
|
|
|
92
200
|
process.stdout.write(`Remote type set to: ${value}
|
|
93
201
|
`);
|
|
94
202
|
});
|
|
95
|
-
var
|
|
203
|
+
var configSetIssueDiscoveryTechnique = new Command("issue-discovery-technique").description("Set the issue discovery technique (GitHub mode only)").argument("<value>", `Technique: ${VALID_TECHNIQUES.join(", ")}`).action((value) => {
|
|
204
|
+
if (!VALID_TECHNIQUES.includes(value)) {
|
|
205
|
+
process.stderr.write(`Error: invalid technique "${value}". Must be one of: ${VALID_TECHNIQUES.join(", ")}
|
|
206
|
+
`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
const current = readConfig();
|
|
210
|
+
writeConfig({ ...current, issueDiscoveryTechnique: value });
|
|
211
|
+
process.stdout.write(`Issue discovery technique set to: ${value}
|
|
212
|
+
`);
|
|
213
|
+
});
|
|
214
|
+
var configSetIssueDiscoveryValue = new Command("issue-discovery-value").description("Set the value for the issue discovery technique (label name, username, or search string)").argument("<value>", "The filter value").action((value) => {
|
|
215
|
+
const current = readConfig();
|
|
216
|
+
writeConfig({ ...current, issueDiscoveryValue: value });
|
|
217
|
+
process.stdout.write(`Issue discovery value set to: ${value}
|
|
218
|
+
`);
|
|
219
|
+
});
|
|
220
|
+
var configSetClaudeSystemPrompt = new Command("claude-system-prompt").description("Set the system prompt used when invoking Claude Code").argument("<value>", "System prompt text").action((value) => {
|
|
221
|
+
const current = readConfig();
|
|
222
|
+
writeConfig({ ...current, claudeSystemPrompt: value });
|
|
223
|
+
process.stdout.write(`Claude system prompt set.
|
|
224
|
+
`);
|
|
225
|
+
});
|
|
226
|
+
var configSet = new Command("set").description("Set a configuration value").addCommand(configSetType).addCommand(configSetIssueDiscoveryTechnique).addCommand(configSetIssueDiscoveryValue).addCommand(configSetClaudeSystemPrompt);
|
|
96
227
|
var configCommand = new Command("config").description("Configure automata settings").addCommand(configSet).action(async () => {
|
|
97
228
|
const { waitUntilExit } = render(React2.createElement(ConfigWizard));
|
|
98
229
|
await waitUntilExit();
|
|
99
230
|
});
|
|
100
231
|
|
|
232
|
+
// src/commands/git.ts
|
|
233
|
+
import { Command as Command2 } from "commander";
|
|
234
|
+
|
|
235
|
+
// src/git/gitService.ts
|
|
236
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
237
|
+
|
|
238
|
+
// src/config/azdoService.ts
|
|
239
|
+
import { spawnSync } from "child_process";
|
|
240
|
+
function run(cmd, args) {
|
|
241
|
+
const result = spawnSync(cmd, args, { encoding: "utf8" });
|
|
242
|
+
if (result.error) {
|
|
243
|
+
const err = result.error;
|
|
244
|
+
if (err.code === "ENOENT") {
|
|
245
|
+
throw new Error("`azdo` CLI is not installed or not on PATH.");
|
|
246
|
+
}
|
|
247
|
+
throw new Error(err.message);
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
stdout: result.stdout ?? "",
|
|
251
|
+
stderr: result.stderr ?? "",
|
|
252
|
+
status: result.status ?? 1
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function mapStatus(azdoStatus) {
|
|
256
|
+
switch (azdoStatus) {
|
|
257
|
+
case "active":
|
|
258
|
+
return "OPEN";
|
|
259
|
+
case "completed":
|
|
260
|
+
return "MERGED";
|
|
261
|
+
case "abandoned":
|
|
262
|
+
return "CLOSED";
|
|
263
|
+
default:
|
|
264
|
+
return azdoStatus.toUpperCase();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function getPrInfo() {
|
|
268
|
+
const { stdout, stderr, status } = run("azdo", ["pr", "status", "--json"]);
|
|
269
|
+
if (status !== 0) {
|
|
270
|
+
throw new Error(stderr.trim() || "Failed to query Azure DevOps PR status. Is `azdo` installed and authenticated?");
|
|
271
|
+
}
|
|
272
|
+
const parsed = JSON.parse(stdout);
|
|
273
|
+
if (parsed.pullRequests.length === 0) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const pr = parsed.pullRequests[0];
|
|
277
|
+
return {
|
|
278
|
+
number: pr.id,
|
|
279
|
+
title: pr.title,
|
|
280
|
+
state: mapStatus(pr.status),
|
|
281
|
+
url: pr.url,
|
|
282
|
+
checks: []
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/git/gitService.ts
|
|
287
|
+
function run2(cmd, args) {
|
|
288
|
+
const result = spawnSync2(cmd, args, { encoding: "utf8" });
|
|
289
|
+
return {
|
|
290
|
+
stdout: result.stdout ?? "",
|
|
291
|
+
stderr: result.stderr ?? "",
|
|
292
|
+
status: result.status ?? 1
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function getCurrentBranch() {
|
|
296
|
+
const { stdout, status } = run2("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
297
|
+
if (status !== 0) {
|
|
298
|
+
throw new Error("Failed to determine current branch. Are you inside a git repository?");
|
|
299
|
+
}
|
|
300
|
+
return stdout.trim();
|
|
301
|
+
}
|
|
302
|
+
function parseOwnerRepo() {
|
|
303
|
+
const { stdout, status } = run2("git", ["remote", "get-url", "origin"]);
|
|
304
|
+
if (status !== 0) return null;
|
|
305
|
+
const url = stdout.trim();
|
|
306
|
+
const https = url.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
307
|
+
if (https) return https[1];
|
|
308
|
+
const ssh = url.match(/github\.com:([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
309
|
+
if (ssh) return ssh[1];
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
function extractLastMarkdownUrl(markdown) {
|
|
313
|
+
const matches = [...markdown.matchAll(/\]\((https?:\/\/[^)]+)\)/g)];
|
|
314
|
+
return matches.length > 0 ? matches[matches.length - 1][1] ?? null : null;
|
|
315
|
+
}
|
|
316
|
+
function fetchCheckRunOutputs(ownerRepo, sha) {
|
|
317
|
+
const { stdout, status } = run2("gh", [
|
|
318
|
+
"api",
|
|
319
|
+
`repos/${ownerRepo}/commits/${sha}/check-runs`,
|
|
320
|
+
"--jq",
|
|
321
|
+
".check_runs[] | {name, html_url, details_url, output}"
|
|
322
|
+
]);
|
|
323
|
+
const map = /* @__PURE__ */ new Map();
|
|
324
|
+
if (status !== 0) return map;
|
|
325
|
+
for (const line of stdout.trim().split("\n")) {
|
|
326
|
+
if (!line) continue;
|
|
327
|
+
try {
|
|
328
|
+
const item = JSON.parse(line);
|
|
329
|
+
const title = item.output?.title ?? "";
|
|
330
|
+
const summaryUrl = item.output?.summary ? extractLastMarkdownUrl(item.output.summary) : null;
|
|
331
|
+
const detailsUrl = summaryUrl ?? (item.details_url !== item.html_url ? item.details_url : "") ?? item.html_url;
|
|
332
|
+
map.set(item.name, { title, detailsUrl });
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return map;
|
|
337
|
+
}
|
|
338
|
+
function getPrInfoGh(branch) {
|
|
339
|
+
const { stdout, stderr, status } = run2("gh", [
|
|
340
|
+
"pr",
|
|
341
|
+
"view",
|
|
342
|
+
branch,
|
|
343
|
+
"--json",
|
|
344
|
+
"number,title,state,url,headRefOid,statusCheckRollup"
|
|
345
|
+
]);
|
|
346
|
+
if (status !== 0) {
|
|
347
|
+
if (stderr.includes("no pull requests found") || stderr.includes("Could not resolve")) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
throw new Error(stderr.trim() || "Failed to query GitHub. Is `gh` installed and authenticated?");
|
|
351
|
+
}
|
|
352
|
+
const raw = JSON.parse(stdout);
|
|
353
|
+
const failedChecks = (raw.statusCheckRollup ?? []).filter(
|
|
354
|
+
(c) => c.conclusion !== null && ["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED"].includes(c.conclusion)
|
|
355
|
+
);
|
|
356
|
+
const ownerRepo = failedChecks.length > 0 ? parseOwnerRepo() : null;
|
|
357
|
+
const checkOutputs = ownerRepo ? fetchCheckRunOutputs(ownerRepo, raw.headRefOid) : /* @__PURE__ */ new Map();
|
|
358
|
+
const checks = (raw.statusCheckRollup ?? []).map((c) => {
|
|
359
|
+
const enriched = checkOutputs.get(c.name);
|
|
360
|
+
return {
|
|
361
|
+
name: c.name,
|
|
362
|
+
status: c.status,
|
|
363
|
+
conclusion: c.conclusion,
|
|
364
|
+
description: enriched?.title || c.description || "",
|
|
365
|
+
detailsUrl: enriched?.detailsUrl || c.detailsUrl || ""
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
return { number: raw.number, title: raw.title, state: raw.state, url: raw.url, checks };
|
|
369
|
+
}
|
|
370
|
+
function getPrInfo2(branch) {
|
|
371
|
+
const config = readConfig();
|
|
372
|
+
if (config.remoteType === "azdo") {
|
|
373
|
+
return getPrInfo();
|
|
374
|
+
}
|
|
375
|
+
return getPrInfoGh(branch);
|
|
376
|
+
}
|
|
377
|
+
function isUpstreamGone(branch) {
|
|
378
|
+
const { status } = run2("git", ["ls-remote", "--exit-code", "--heads", "origin", branch]);
|
|
379
|
+
return status !== 0;
|
|
380
|
+
}
|
|
381
|
+
function hasUncommittedChanges() {
|
|
382
|
+
const { stdout } = run2("git", ["status", "--porcelain"]);
|
|
383
|
+
return stdout.trim().length > 0;
|
|
384
|
+
}
|
|
385
|
+
function checkoutAndPull(targetBranch) {
|
|
386
|
+
const checkout = run2("git", ["checkout", targetBranch]);
|
|
387
|
+
if (checkout.status !== 0) {
|
|
388
|
+
throw new Error(`Failed to checkout ${targetBranch}: ${checkout.stderr.trim()}`);
|
|
389
|
+
}
|
|
390
|
+
const pull = run2("git", ["pull"]);
|
|
391
|
+
if (pull.status !== 0) {
|
|
392
|
+
throw new Error(`Failed to pull ${targetBranch}: ${pull.stderr.trim()}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function fetchPrune() {
|
|
396
|
+
const result = run2("git", ["fetch", "--prune"]);
|
|
397
|
+
if (result.status !== 0) {
|
|
398
|
+
throw new Error(`Failed to fetch --prune: ${result.stderr.trim()}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function deleteLocalBranch(branch) {
|
|
402
|
+
const result = run2("git", ["branch", "-D", branch]);
|
|
403
|
+
if (result.status !== 0) {
|
|
404
|
+
throw new Error(`Failed to delete branch ${branch}: ${result.stderr.trim()}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
var REVIEW_THREADS_QUERY = `
|
|
408
|
+
query($owner:String!,$repo:String!,$prNumber:Int!){
|
|
409
|
+
repository(owner:$owner,name:$repo){
|
|
410
|
+
pullRequest(number:$prNumber){
|
|
411
|
+
reviewThreads(first:100){
|
|
412
|
+
nodes{
|
|
413
|
+
isResolved
|
|
414
|
+
isOutdated
|
|
415
|
+
comments(first:1){
|
|
416
|
+
nodes{ author{login} body path line createdAt }
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}`.trim();
|
|
423
|
+
function getPrCommentsGh(branch) {
|
|
424
|
+
const prView = run2("gh", ["pr", "view", branch, "--json", "number"]);
|
|
425
|
+
if (prView.status !== 0) {
|
|
426
|
+
if (prView.stderr.includes("no pull requests found") || prView.stderr.includes("Could not resolve")) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
throw new Error(prView.stderr.trim() || "Failed to query GitHub. Is `gh` installed and authenticated?");
|
|
430
|
+
}
|
|
431
|
+
const { number: prNumber } = JSON.parse(prView.stdout);
|
|
432
|
+
const ownerRepo = parseOwnerRepo();
|
|
433
|
+
if (!ownerRepo) {
|
|
434
|
+
throw new Error("Could not determine GitHub owner/repo from git remote. Is 'origin' set to a GitHub URL?");
|
|
435
|
+
}
|
|
436
|
+
const slashIdx = ownerRepo.indexOf("/");
|
|
437
|
+
const owner = ownerRepo.slice(0, slashIdx);
|
|
438
|
+
const repo = ownerRepo.slice(slashIdx + 1);
|
|
439
|
+
const gql = run2("gh", [
|
|
440
|
+
"api",
|
|
441
|
+
"graphql",
|
|
442
|
+
"-f",
|
|
443
|
+
`query=${REVIEW_THREADS_QUERY}`,
|
|
444
|
+
"-f",
|
|
445
|
+
`owner=${owner}`,
|
|
446
|
+
"-f",
|
|
447
|
+
`repo=${repo}`,
|
|
448
|
+
"-F",
|
|
449
|
+
`prNumber=${String(prNumber)}`
|
|
450
|
+
]);
|
|
451
|
+
if (gql.status !== 0) {
|
|
452
|
+
throw new Error(gql.stderr.trim() || "Failed to query GitHub GraphQL API.");
|
|
453
|
+
}
|
|
454
|
+
const response = JSON.parse(gql.stdout);
|
|
455
|
+
const threads = response.data.repository.pullRequest.reviewThreads.nodes;
|
|
456
|
+
return threads.filter((t) => !t.isResolved && t.comments.nodes.length > 0).map((t) => {
|
|
457
|
+
const c = t.comments.nodes[0];
|
|
458
|
+
return {
|
|
459
|
+
author: c.author.login,
|
|
460
|
+
body: c.body,
|
|
461
|
+
path: c.path,
|
|
462
|
+
line: c.line ?? null,
|
|
463
|
+
createdAt: c.createdAt
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
function getPrComments(branch) {
|
|
468
|
+
const config = readConfig();
|
|
469
|
+
if (config.remoteType === "azdo") {
|
|
470
|
+
return "unsupported";
|
|
471
|
+
}
|
|
472
|
+
return getPrCommentsGh(branch);
|
|
473
|
+
}
|
|
474
|
+
var SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
|
|
475
|
+
function getLatestTagOnMaster() {
|
|
476
|
+
const { stdout, status } = run2("git", [
|
|
477
|
+
"describe",
|
|
478
|
+
"--tags",
|
|
479
|
+
"--abbrev=0",
|
|
480
|
+
"--match",
|
|
481
|
+
"[0-9]*.[0-9]*.[0-9]*",
|
|
482
|
+
"--match",
|
|
483
|
+
"v[0-9]*.[0-9]*.[0-9]*",
|
|
484
|
+
"master"
|
|
485
|
+
]);
|
|
486
|
+
if (status !== 0) return null;
|
|
487
|
+
const tag = stdout.trim();
|
|
488
|
+
const m = SEMVER_RE.exec(tag);
|
|
489
|
+
if (!m) return null;
|
|
490
|
+
return `${m[1]}.${m[2]}.${m[3]}`;
|
|
491
|
+
}
|
|
492
|
+
function bumpMinorVersion(version2) {
|
|
493
|
+
const m = SEMVER_RE.exec(version2);
|
|
494
|
+
if (!m) throw new Error(`Invalid semver: ${version2}`);
|
|
495
|
+
return `${m[1]}.${String(Number(m[2]) + 1)}.0`;
|
|
496
|
+
}
|
|
497
|
+
function tagExists(version2) {
|
|
498
|
+
const { stdout, status, stderr } = run2("git", ["tag", "-l", version2]);
|
|
499
|
+
if (status !== 0) {
|
|
500
|
+
throw new Error(`Command failed: git tag -l ${version2}
|
|
501
|
+
${stderr.trim()}`);
|
|
502
|
+
}
|
|
503
|
+
return stdout.trim().length > 0;
|
|
504
|
+
}
|
|
505
|
+
function publishRelease(version2, dryRun) {
|
|
506
|
+
const releaseBranch = `release/${version2}`;
|
|
507
|
+
const steps = [
|
|
508
|
+
{ args: ["checkout", "-b", releaseBranch], desc: `git checkout -b ${releaseBranch}` },
|
|
509
|
+
{ args: ["checkout", "master"], desc: `git checkout master` },
|
|
510
|
+
{ args: ["merge", "--no-ff", releaseBranch], desc: `git merge --no-ff ${releaseBranch}` },
|
|
511
|
+
{ args: ["tag", version2], desc: `git tag ${version2}` },
|
|
512
|
+
{ args: ["checkout", "develop"], desc: `git checkout develop` },
|
|
513
|
+
{ args: ["merge", "--no-ff", releaseBranch], desc: `git merge --no-ff ${releaseBranch}` },
|
|
514
|
+
{ args: ["branch", "-d", releaseBranch], desc: `git branch -d ${releaseBranch}` },
|
|
515
|
+
{ args: ["push", "origin", "develop", "master", version2], desc: `git push origin develop master ${version2}` }
|
|
516
|
+
];
|
|
517
|
+
for (const step of steps) {
|
|
518
|
+
if (dryRun) {
|
|
519
|
+
process.stdout.write(`[dry-run] ${step.desc}
|
|
520
|
+
`);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const { status, stderr } = run2("git", step.args);
|
|
524
|
+
if (status !== 0) {
|
|
525
|
+
throw new Error(`Command failed: ${step.desc}
|
|
526
|
+
${stderr.trim()}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/commands/git.ts
|
|
532
|
+
var FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED"]);
|
|
533
|
+
var SKIP_CONCLUSIONS = /* @__PURE__ */ new Set(["SKIPPED", "NEUTRAL"]);
|
|
534
|
+
function checkSymbol(check) {
|
|
535
|
+
if (check.status !== "COMPLETED") return "\u25CF";
|
|
536
|
+
if (check.conclusion === "SUCCESS") return "\u2713";
|
|
537
|
+
if (check.conclusion !== null && SKIP_CONCLUSIONS.has(check.conclusion)) return "\u25CB";
|
|
538
|
+
if (check.conclusion !== null && FAIL_CONCLUSIONS.has(check.conclusion)) return "\u2717";
|
|
539
|
+
return "\u25CF";
|
|
540
|
+
}
|
|
541
|
+
function formatCheckSummary(checks) {
|
|
542
|
+
const running = checks.some((c) => c.status !== "COMPLETED");
|
|
543
|
+
const failed = checks.filter((c) => c.conclusion !== null && FAIL_CONCLUSIONS.has(c.conclusion));
|
|
544
|
+
const errors = failed.length === 0 ? "none" : failed.map((c) => `${c.name}: ${c.description.trim() || c.detailsUrl || "no details available"}`).join("; ");
|
|
545
|
+
return `Checks Running: ${String(running)}
|
|
546
|
+
Check Errors: ${errors}
|
|
547
|
+
`;
|
|
548
|
+
}
|
|
549
|
+
function formatChecks(checks) {
|
|
550
|
+
if (checks.length === 0) return "Checks: none\n";
|
|
551
|
+
const lines = ["Checks:"];
|
|
552
|
+
for (const check of checks) {
|
|
553
|
+
const sym = checkSymbol(check);
|
|
554
|
+
const pending = check.status !== "COMPLETED" ? " (pending)" : "";
|
|
555
|
+
lines.push(` ${sym} ${check.name}${pending}`);
|
|
556
|
+
if (check.conclusion !== null && FAIL_CONCLUSIONS.has(check.conclusion)) {
|
|
557
|
+
const desc = check.description.trim();
|
|
558
|
+
const url = check.detailsUrl.trim();
|
|
559
|
+
if (desc) lines.push(` Details: ${desc}`);
|
|
560
|
+
if (url) lines.push(` URL: ${url}`);
|
|
561
|
+
if (!desc && !url) lines.push(` Details: (no details available)`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return lines.join("\n") + "\n";
|
|
565
|
+
}
|
|
566
|
+
function sleep(ms) {
|
|
567
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
568
|
+
}
|
|
569
|
+
function formatFailedChecks(failed) {
|
|
570
|
+
const lines = [];
|
|
571
|
+
for (const check of failed) {
|
|
572
|
+
lines.push(` \u2717 ${check.name}`);
|
|
573
|
+
const desc = check.description.trim();
|
|
574
|
+
const url = check.detailsUrl.trim();
|
|
575
|
+
if (desc) lines.push(` Details: ${desc}`);
|
|
576
|
+
if (url) lines.push(` URL: ${url}`);
|
|
577
|
+
if (!desc && !url) lines.push(` Details: (no details available)`);
|
|
578
|
+
}
|
|
579
|
+
return lines.join("\n") + "\n";
|
|
580
|
+
}
|
|
581
|
+
var POLL_INTERVAL_MS = 1e4;
|
|
582
|
+
var getPrInfoCmd = new Command2("get-pr-info").description("Show pull request info for the current branch").option("--json", "Output as JSON").option("--wait-finish-checks", "Poll until all checks complete, then report pass/fail (exit 1 on failure)").addHelpText(
|
|
583
|
+
"after",
|
|
584
|
+
`
|
|
585
|
+
Check status symbols:
|
|
586
|
+
\u2713 Passed (conclusion: SUCCESS)
|
|
587
|
+
\u2717 Failed (conclusion: FAILURE / TIMED_OUT / ACTION_REQUIRED / CANCELLED)
|
|
588
|
+
\u25CF Pending (status: QUEUED or IN_PROGRESS)
|
|
589
|
+
\u25CB Skipped (conclusion: SKIPPED or NEUTRAL)
|
|
590
|
+
|
|
591
|
+
Failure details are printed beneath each \u2717 check.
|
|
592
|
+
See docs/git.md for full output reference.`
|
|
593
|
+
).action(async (options) => {
|
|
594
|
+
let branch;
|
|
595
|
+
try {
|
|
596
|
+
branch = getCurrentBranch();
|
|
597
|
+
} catch (err) {
|
|
598
|
+
process.stderr.write(`Error: ${err.message}
|
|
599
|
+
`);
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
if (options.waitFinishChecks) {
|
|
603
|
+
let pr2;
|
|
604
|
+
while (true) {
|
|
605
|
+
try {
|
|
606
|
+
pr2 = getPrInfo2(branch);
|
|
607
|
+
} catch (err) {
|
|
608
|
+
process.stderr.write(`Error: ${err.message}
|
|
609
|
+
`);
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
if (pr2 === null) {
|
|
613
|
+
process.stderr.write(`Error: No pull request found for branch: ${branch}
|
|
614
|
+
`);
|
|
615
|
+
process.exit(1);
|
|
616
|
+
}
|
|
617
|
+
const running = pr2.checks.filter((c) => c.status !== "COMPLETED");
|
|
618
|
+
if (running.length === 0) break;
|
|
619
|
+
process.stdout.write(`Waiting for ${running.length} check(s) to complete...
|
|
620
|
+
`);
|
|
621
|
+
await sleep(POLL_INTERVAL_MS);
|
|
622
|
+
}
|
|
623
|
+
const failed = pr2.checks.filter((c) => c.conclusion !== null && FAIL_CONCLUSIONS.has(c.conclusion));
|
|
624
|
+
if (failed.length === 0) {
|
|
625
|
+
if (options.json) {
|
|
626
|
+
process.stdout.write(JSON.stringify({ result: "passed" }, null, 2) + "\n");
|
|
627
|
+
} else {
|
|
628
|
+
process.stdout.write(`All checks passed. \u2713
|
|
629
|
+
`);
|
|
630
|
+
}
|
|
631
|
+
process.exit(0);
|
|
632
|
+
} else {
|
|
633
|
+
if (options.json) {
|
|
634
|
+
process.stdout.write(JSON.stringify({ result: "failed", failed }, null, 2) + "\n");
|
|
635
|
+
} else {
|
|
636
|
+
process.stdout.write(`${failed.length} check(s) failed:
|
|
637
|
+
`);
|
|
638
|
+
process.stdout.write(formatFailedChecks(failed));
|
|
639
|
+
}
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
let pr;
|
|
645
|
+
try {
|
|
646
|
+
pr = getPrInfo2(branch);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
process.stderr.write(`Error: ${err.message}
|
|
649
|
+
`);
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
if (pr === null) {
|
|
653
|
+
process.stdout.write(`No pull request found for branch: ${branch}
|
|
654
|
+
`);
|
|
655
|
+
process.exit(0);
|
|
656
|
+
}
|
|
657
|
+
if (options.json) {
|
|
658
|
+
process.stdout.write(JSON.stringify(pr, null, 2) + "\n");
|
|
659
|
+
} else {
|
|
660
|
+
process.stdout.write(`PR: #${pr.number}
|
|
661
|
+
Title: ${pr.title}
|
|
662
|
+
State: ${pr.state}
|
|
663
|
+
URL: ${pr.url}
|
|
664
|
+
`);
|
|
665
|
+
process.stdout.write(formatCheckSummary(pr.checks));
|
|
666
|
+
process.stdout.write(formatChecks(pr.checks));
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
var ANSI_ESCAPE_RE = new RegExp("\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])", "g");
|
|
670
|
+
var CONTROL_CHARS_RE = new RegExp("[\0-\b\v\f-\x7F]", "g");
|
|
671
|
+
function sanitizeText(text) {
|
|
672
|
+
return text.replace(ANSI_ESCAPE_RE, "").replace(CONTROL_CHARS_RE, "");
|
|
673
|
+
}
|
|
674
|
+
var getPrCommentsCmd = new Command2("get-pr-comments").description("List open (unresolved) review comments on the pull request for the current branch (GitHub only)").option("--json", "Output as JSON array").addHelpText(
|
|
675
|
+
"after",
|
|
676
|
+
`
|
|
677
|
+
Only GitHub (remoteType: gh) is supported. Azure DevOps is not supported.
|
|
678
|
+
See docs/azdo-gap.md for details.`
|
|
679
|
+
).action((options) => {
|
|
680
|
+
let branch;
|
|
681
|
+
try {
|
|
682
|
+
branch = getCurrentBranch();
|
|
683
|
+
} catch (err) {
|
|
684
|
+
process.stderr.write(`Error: ${err.message}
|
|
685
|
+
`);
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
let comments;
|
|
689
|
+
try {
|
|
690
|
+
comments = getPrComments(branch);
|
|
691
|
+
} catch (err) {
|
|
692
|
+
process.stderr.write(`Error: ${err.message}
|
|
693
|
+
`);
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
if (comments === "unsupported") {
|
|
697
|
+
process.stderr.write(
|
|
698
|
+
`Error: get-pr-comments is not supported for Azure DevOps. See docs/azdo-gap.md for details.
|
|
699
|
+
`
|
|
700
|
+
);
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
if (comments === null) {
|
|
704
|
+
process.stderr.write(`Error: No pull request found for branch: ${branch}
|
|
705
|
+
`);
|
|
706
|
+
process.exit(1);
|
|
707
|
+
}
|
|
708
|
+
if (options.json) {
|
|
709
|
+
process.stdout.write(JSON.stringify(comments, null, 2) + "\n");
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (comments.length === 0) {
|
|
713
|
+
process.stdout.write(`No open comments.
|
|
714
|
+
`);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const lines = [];
|
|
718
|
+
for (const c of comments) {
|
|
719
|
+
const loc = c.line !== null ? `${c.path}:${String(c.line)}` : `${c.path}:(file)`;
|
|
720
|
+
const safeBody = sanitizeText(c.body);
|
|
721
|
+
const safeAuthor = sanitizeText(c.author);
|
|
722
|
+
lines.push(`[${safeAuthor}] on ${loc}
|
|
723
|
+
${safeBody}`);
|
|
724
|
+
}
|
|
725
|
+
process.stdout.write(lines.join("\n\n") + "\n");
|
|
726
|
+
});
|
|
727
|
+
var finishFeatureCmd = new Command2("finish-feature").description("Clean up a merged feature branch: checkout develop, pull, and delete local branch").action(() => {
|
|
728
|
+
let branch;
|
|
729
|
+
try {
|
|
730
|
+
branch = getCurrentBranch();
|
|
731
|
+
} catch (err) {
|
|
732
|
+
process.stderr.write(`Error: ${err.message}
|
|
733
|
+
`);
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
if (branch === "develop") {
|
|
737
|
+
process.stderr.write("Error: finish-feature cannot be run from the develop branch.\n");
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
if (hasUncommittedChanges()) {
|
|
741
|
+
process.stderr.write(
|
|
742
|
+
"Error: You have uncommitted changes. Commit or stash them before running finish-feature.\n"
|
|
743
|
+
);
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
let pr;
|
|
747
|
+
try {
|
|
748
|
+
pr = getPrInfo2(branch);
|
|
749
|
+
} catch (err) {
|
|
750
|
+
process.stderr.write(`Error: ${err.message}
|
|
751
|
+
`);
|
|
752
|
+
process.exit(1);
|
|
753
|
+
}
|
|
754
|
+
if (pr === null) {
|
|
755
|
+
process.stderr.write(`Error: No pull request found for branch: ${branch}
|
|
756
|
+
`);
|
|
757
|
+
process.exit(1);
|
|
758
|
+
}
|
|
759
|
+
if (pr.state === "OPEN") {
|
|
760
|
+
process.stderr.write(`Error: Pull request #${pr.number} is still open. Merge it before finishing the feature.
|
|
761
|
+
`);
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
if (pr.state === "CLOSED") {
|
|
765
|
+
process.stderr.write(
|
|
766
|
+
`Error: Pull request #${pr.number} was closed without merging. finish-feature only proceeds for merged PRs.
|
|
767
|
+
`
|
|
768
|
+
);
|
|
769
|
+
process.exit(1);
|
|
770
|
+
}
|
|
771
|
+
if (!isUpstreamGone(branch)) {
|
|
772
|
+
process.stderr.write(
|
|
773
|
+
`Error: Remote tracking branch 'origin/${branch}' still exists. Push or delete it remotely before finishing.
|
|
774
|
+
`
|
|
775
|
+
);
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
try {
|
|
779
|
+
process.stdout.write(`Fetching and pruning remote refs...
|
|
780
|
+
`);
|
|
781
|
+
fetchPrune();
|
|
782
|
+
process.stdout.write(`Checking out develop and pulling latest...
|
|
783
|
+
`);
|
|
784
|
+
checkoutAndPull("develop");
|
|
785
|
+
process.stdout.write(`Deleting local branch: ${branch}
|
|
786
|
+
`);
|
|
787
|
+
deleteLocalBranch(branch);
|
|
788
|
+
process.stdout.write(`Done. Branch '${branch}' has been removed.
|
|
789
|
+
`);
|
|
790
|
+
} catch (err) {
|
|
791
|
+
process.stderr.write(`Error: ${err.message}
|
|
792
|
+
`);
|
|
793
|
+
process.exit(1);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
var SEMVER_ARG_RE = /^\d+\.\d+\.\d+$/;
|
|
797
|
+
var publishReleaseCmd = new Command2("publish-release").description("Execute the full GitFlow release sequence and push to origin").argument("[version]", "Release version in X.Y.Z format (auto-detected from master tag if omitted)").option("--dry-run", "Print git commands without executing them").addHelpText(
|
|
798
|
+
"after",
|
|
799
|
+
`
|
|
800
|
+
Release sequence:
|
|
801
|
+
1. git checkout -b release/<version>
|
|
802
|
+
2. git checkout master && git merge --no-ff release/<version>
|
|
803
|
+
3. git tag <version>
|
|
804
|
+
4. git checkout develop && git merge --no-ff release/<version>
|
|
805
|
+
5. git branch -d release/<version>
|
|
806
|
+
6. git push origin develop master <version>
|
|
807
|
+
|
|
808
|
+
When [version] is omitted the latest semver tag on master is detected and the
|
|
809
|
+
minor segment is incremented (e.g. 1.2.0 \u2192 1.3.0).`
|
|
810
|
+
).action((version2, options) => {
|
|
811
|
+
const dryRun = options.dryRun ?? false;
|
|
812
|
+
let branch;
|
|
813
|
+
try {
|
|
814
|
+
branch = getCurrentBranch();
|
|
815
|
+
} catch (err) {
|
|
816
|
+
process.stderr.write(`Error: ${err.message}
|
|
817
|
+
`);
|
|
818
|
+
process.exit(1);
|
|
819
|
+
}
|
|
820
|
+
if (branch !== "develop") {
|
|
821
|
+
process.stderr.write(
|
|
822
|
+
`Error: publish-release must be run from the 'develop' branch (currently on '${branch}').
|
|
823
|
+
`
|
|
824
|
+
);
|
|
825
|
+
process.exit(1);
|
|
826
|
+
}
|
|
827
|
+
if (hasUncommittedChanges()) {
|
|
828
|
+
process.stderr.write("Error: You have uncommitted changes. Commit or stash them before publishing a release.\n");
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
let resolvedVersion;
|
|
832
|
+
if (version2 !== void 0) {
|
|
833
|
+
if (!SEMVER_ARG_RE.test(version2)) {
|
|
834
|
+
process.stderr.write(`Error: Version '${version2}' is not valid semver. Use X.Y.Z format (e.g. 1.2.0).
|
|
835
|
+
`);
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
resolvedVersion = version2;
|
|
839
|
+
} else {
|
|
840
|
+
const latest = getLatestTagOnMaster();
|
|
841
|
+
if (latest === null) {
|
|
842
|
+
process.stderr.write(
|
|
843
|
+
"Error: No semver tag found on master. Pass a version explicitly: automata git publish-release <X.Y.Z>\n"
|
|
844
|
+
);
|
|
845
|
+
process.exit(1);
|
|
846
|
+
}
|
|
847
|
+
resolvedVersion = bumpMinorVersion(latest);
|
|
848
|
+
process.stdout.write(`Auto-detected version: ${latest} \u2192 ${resolvedVersion}
|
|
849
|
+
`);
|
|
850
|
+
}
|
|
851
|
+
if (tagExists(resolvedVersion)) {
|
|
852
|
+
process.stderr.write(`Error: Tag '${resolvedVersion}' already exists.
|
|
853
|
+
`);
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
856
|
+
if (dryRun) {
|
|
857
|
+
process.stdout.write(`Dry-run: release ${resolvedVersion}
|
|
858
|
+
`);
|
|
859
|
+
} else {
|
|
860
|
+
process.stdout.write(`Publishing release ${resolvedVersion}...
|
|
861
|
+
`);
|
|
862
|
+
}
|
|
863
|
+
try {
|
|
864
|
+
publishRelease(resolvedVersion, dryRun);
|
|
865
|
+
} catch (err) {
|
|
866
|
+
process.stderr.write(`Error: ${err.message}
|
|
867
|
+
`);
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
870
|
+
if (!dryRun) {
|
|
871
|
+
process.stdout.write(`Release ${resolvedVersion} published successfully.
|
|
872
|
+
`);
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
var gitCommand = new Command2("git").description("Git workflow commands (some require gh CLI)").addCommand(getPrInfoCmd).addCommand(getPrCommentsCmd).addCommand(finishFeatureCmd).addCommand(publishReleaseCmd);
|
|
876
|
+
|
|
877
|
+
// src/commands/getReady.ts
|
|
878
|
+
import { Command as Command3 } from "commander";
|
|
879
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
880
|
+
import { existsSync } from "fs";
|
|
881
|
+
import { delimiter, join as join2 } from "path";
|
|
882
|
+
|
|
883
|
+
// src/config/githubService.ts
|
|
884
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
885
|
+
function run3(cmd, args) {
|
|
886
|
+
const result = spawnSync3(cmd, args, { encoding: "utf8" });
|
|
887
|
+
if (result.error) {
|
|
888
|
+
const err = result.error;
|
|
889
|
+
if (err.code === "ENOENT") {
|
|
890
|
+
throw new Error("`gh` CLI is not installed or not on PATH.");
|
|
891
|
+
}
|
|
892
|
+
throw new Error(err.message);
|
|
893
|
+
}
|
|
894
|
+
return {
|
|
895
|
+
stdout: result.stdout ?? "",
|
|
896
|
+
stderr: result.stderr ?? "",
|
|
897
|
+
status: result.status ?? 1
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
function listIssues(technique, value) {
|
|
901
|
+
const baseArgs = [
|
|
902
|
+
"issue",
|
|
903
|
+
"list",
|
|
904
|
+
"--state",
|
|
905
|
+
"open",
|
|
906
|
+
"--sort",
|
|
907
|
+
"created",
|
|
908
|
+
"--order",
|
|
909
|
+
"asc",
|
|
910
|
+
"--limit",
|
|
911
|
+
"1",
|
|
912
|
+
"--json",
|
|
913
|
+
"number,title,body,url"
|
|
914
|
+
];
|
|
915
|
+
let filterArgs;
|
|
916
|
+
switch (technique) {
|
|
917
|
+
case "label":
|
|
918
|
+
filterArgs = ["--label", value];
|
|
919
|
+
break;
|
|
920
|
+
case "assignee":
|
|
921
|
+
filterArgs = ["--assignee", value];
|
|
922
|
+
break;
|
|
923
|
+
case "title-contains":
|
|
924
|
+
filterArgs = ["--search", `${value} in:title`];
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
const { stdout, stderr, status } = run3("gh", [...baseArgs, ...filterArgs]);
|
|
928
|
+
if (status !== 0) {
|
|
929
|
+
throw new Error(stderr.trim() || "Failed to query GitHub issues. Is `gh` installed and authenticated?");
|
|
930
|
+
}
|
|
931
|
+
const issues = JSON.parse(stdout);
|
|
932
|
+
if (issues.length === 0) {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
return issues[0];
|
|
936
|
+
}
|
|
937
|
+
function postComment(issueNumber, body) {
|
|
938
|
+
const { stderr, status } = run3("gh", ["issue", "comment", String(issueNumber), "--body", body]);
|
|
939
|
+
if (status !== 0) {
|
|
940
|
+
throw new Error(stderr.trim() || `Failed to post comment on issue #${issueNumber}.`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/commands/getReady.ts
|
|
945
|
+
function resolveCommand(name) {
|
|
946
|
+
const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
|
|
947
|
+
for (const dir of pathDirs) {
|
|
948
|
+
const candidate = join2(dir, name);
|
|
949
|
+
if (existsSync(candidate)) return candidate;
|
|
950
|
+
}
|
|
951
|
+
return name;
|
|
952
|
+
}
|
|
953
|
+
function invokeClaudeCode(issue, systemPrompt) {
|
|
954
|
+
const prompt = systemPrompt ? `${systemPrompt}
|
|
955
|
+
|
|
956
|
+
${issue.body}` : issue.body;
|
|
957
|
+
const claudeBin = resolveCommand("claude");
|
|
958
|
+
const result = spawnSync4(claudeBin, ["-p", prompt], { encoding: "utf8", stdio: "inherit" });
|
|
959
|
+
if (result.error) {
|
|
960
|
+
const err = result.error;
|
|
961
|
+
if (err.code === "ENOENT") {
|
|
962
|
+
process.stderr.write("Error: `claude` CLI is not installed or not on PATH.\n");
|
|
963
|
+
process.exit(1);
|
|
964
|
+
}
|
|
965
|
+
process.stderr.write(`Error: ${err.message}
|
|
966
|
+
`);
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
if (result.status !== 0) {
|
|
970
|
+
process.stderr.write(`Error: Claude Code exited with code ${result.status ?? "unknown"}.
|
|
971
|
+
`);
|
|
972
|
+
process.exit(result.status ?? 1);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
var getReadyCommand = new Command3("get-ready").description("Find the next open GitHub issue matching the configured filter, claim it, and invoke Claude Code").option("--json", "Output issue details as JSON").option("--no-claude", "Skip Claude Code invocation after claiming the issue").action((options) => {
|
|
976
|
+
const config = readConfig();
|
|
977
|
+
if (config.remoteType !== "gh") {
|
|
978
|
+
process.stderr.write(
|
|
979
|
+
"Error: get-ready is not supported in Azure DevOps mode. Work item discovery is not available in azdo-cli. See docs/azdo-gap.md for details.\n"
|
|
980
|
+
);
|
|
981
|
+
process.exit(1);
|
|
982
|
+
}
|
|
983
|
+
if (!config.issueDiscoveryTechnique) {
|
|
984
|
+
process.stderr.write(
|
|
985
|
+
"Error: No issue discovery technique configured. Run `automata config` to set one.\n"
|
|
986
|
+
);
|
|
987
|
+
process.exit(1);
|
|
988
|
+
}
|
|
989
|
+
if (!config.issueDiscoveryValue) {
|
|
990
|
+
process.stderr.write(
|
|
991
|
+
"Error: No issue discovery value configured. Run `automata config` to set one.\n"
|
|
992
|
+
);
|
|
993
|
+
process.exit(1);
|
|
994
|
+
}
|
|
995
|
+
let issue;
|
|
996
|
+
try {
|
|
997
|
+
issue = listIssues(config.issueDiscoveryTechnique, config.issueDiscoveryValue);
|
|
998
|
+
} catch (err) {
|
|
999
|
+
process.stderr.write(`Error: ${err.message}
|
|
1000
|
+
`);
|
|
1001
|
+
process.exit(1);
|
|
1002
|
+
}
|
|
1003
|
+
if (issue === null) {
|
|
1004
|
+
process.stdout.write("No issues found matching the configured filter.\n");
|
|
1005
|
+
process.exit(0);
|
|
1006
|
+
}
|
|
1007
|
+
if (options.json) {
|
|
1008
|
+
process.stdout.write(JSON.stringify({ number: issue.number, title: issue.title, body: issue.body, url: issue.url }, null, 2) + "\n");
|
|
1009
|
+
} else {
|
|
1010
|
+
process.stdout.write(`Issue: #${issue.number}
|
|
1011
|
+
Title: ${issue.title}
|
|
1012
|
+
URL: ${issue.url}
|
|
1013
|
+
|
|
1014
|
+
${issue.body}
|
|
1015
|
+
`);
|
|
1016
|
+
}
|
|
1017
|
+
try {
|
|
1018
|
+
postComment(issue.number, "working");
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
process.stderr.write(`Error: ${err.message}
|
|
1021
|
+
`);
|
|
1022
|
+
process.exit(1);
|
|
1023
|
+
}
|
|
1024
|
+
if (options.claude !== false) {
|
|
1025
|
+
invokeClaudeCode(issue, config.claudeSystemPrompt);
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
|
|
101
1029
|
// src/index.ts
|
|
102
|
-
var program = new
|
|
1030
|
+
var program = new Command4();
|
|
103
1031
|
program.name("automata").description("Automata CLI tool").version(version, "-v, --version");
|
|
104
1032
|
program.addCommand(configCommand);
|
|
1033
|
+
program.addCommand(gitCommand);
|
|
1034
|
+
program.addCommand(getReadyCommand);
|
|
105
1035
|
program.showHelpAfterError();
|
|
106
1036
|
program.parse();
|
|
107
1037
|
if (process.argv.length <= 2) {
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "automata-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Automata CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"automata": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
13
14
|
"build": "tsup",
|