automata-cli 0.2.0-develop.2 → 0.2.0-develop.21
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 +59 -0
- package/dist/index.js +544 -2
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -14,6 +14,65 @@ npm install -g automata-cli
|
|
|
14
14
|
automata --help
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### `automata config`
|
|
20
|
+
|
|
21
|
+
Launch the interactive configuration wizard. Use arrow keys to select the remote environment type and press Enter to save.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
automata config
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### `automata config set type <value>`
|
|
28
|
+
|
|
29
|
+
Set a configuration value non-interactively (useful in scripts or CI).
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
automata config set type gh # GitHub
|
|
33
|
+
automata config set type azdo # Azure DevOps
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Configuration is saved to `.automata/config.json` in the current directory.
|
|
37
|
+
|
|
38
|
+
### `automata git get-pr-info`
|
|
39
|
+
|
|
40
|
+
Show the pull request associated with the current branch (requires [`gh` CLI](https://cli.github.com/) installed and authenticated).
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
automata git get-pr-info # human-readable output
|
|
44
|
+
automata git get-pr-info --json # JSON output
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Example output:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
PR: #42
|
|
51
|
+
Title: Fix authentication bug
|
|
52
|
+
State: MERGED
|
|
53
|
+
URL: https://github.com/org/repo/pull/42
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If no PR exists for the current branch, a friendly message is printed and the command exits with code 0.
|
|
57
|
+
|
|
58
|
+
### `automata git finish-feature`
|
|
59
|
+
|
|
60
|
+
Clean up a merged feature branch in one step: checkout `develop`, pull the latest, and delete the local branch.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
automata git finish-feature
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The command validates all preconditions before making any changes:
|
|
67
|
+
|
|
68
|
+
- Must **not** be on the `develop` branch
|
|
69
|
+
- Working tree must be clean (no uncommitted changes)
|
|
70
|
+
- A pull request for the branch must exist
|
|
71
|
+
- The PR must be in `merged` state (not open or closed-without-merge)
|
|
72
|
+
- The remote tracking branch must no longer exist (`origin/<branch>` is gone)
|
|
73
|
+
|
|
74
|
+
If any precondition fails, the command prints a descriptive error to stderr and exits with a non-zero code.
|
|
75
|
+
|
|
17
76
|
## Development
|
|
18
77
|
|
|
19
78
|
### Prerequisites
|
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 } from "commander";
|
|
4
|
+
import { Command as Command4 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -11,9 +11,551 @@ var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
11
11
|
var packageJson = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf8"));
|
|
12
12
|
var version = packageJson.version;
|
|
13
13
|
|
|
14
|
+
// src/commands/config.ts
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
import { render } from "ink";
|
|
17
|
+
import React2 from "react";
|
|
18
|
+
|
|
19
|
+
// src/config/ConfigWizard.tsx
|
|
20
|
+
import { useState } from "react";
|
|
21
|
+
import { Box, Text, useInput, useApp } from "ink";
|
|
22
|
+
|
|
23
|
+
// src/config/configStore.ts
|
|
24
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
|
|
25
|
+
import { join } from "path";
|
|
26
|
+
var CONFIG_DIR = ".automata";
|
|
27
|
+
var CONFIG_FILE = "config.json";
|
|
28
|
+
function configPath() {
|
|
29
|
+
return join(process.cwd(), CONFIG_DIR, CONFIG_FILE);
|
|
30
|
+
}
|
|
31
|
+
function readConfig() {
|
|
32
|
+
try {
|
|
33
|
+
const raw = readFileSync2(configPath(), "utf8");
|
|
34
|
+
return JSON.parse(raw);
|
|
35
|
+
} catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function writeConfig(config) {
|
|
40
|
+
const dir = join(process.cwd(), CONFIG_DIR);
|
|
41
|
+
mkdirSync(dir, { recursive: true });
|
|
42
|
+
writeFileSync(configPath(), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/config/ConfigWizard.tsx
|
|
46
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
47
|
+
var REMOTE_OPTIONS = [
|
|
48
|
+
{ label: "GitHub", value: "gh" },
|
|
49
|
+
{ label: "Azure DevOps", value: "azdo" }
|
|
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
|
+
];
|
|
56
|
+
function ConfigWizard() {
|
|
57
|
+
const existing = readConfig();
|
|
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
|
+
);
|
|
69
|
+
const { exit } = useApp();
|
|
70
|
+
useInput((input, key) => {
|
|
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
|
+
}
|
|
127
|
+
}
|
|
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
|
+
}
|
|
173
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, children: [
|
|
174
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Configure Claude System Prompt" }),
|
|
175
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
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
|
+
] }),
|
|
184
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
185
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Type prompt \xB7 Enter to save and exit \xB7 Ctrl+C to cancel" })
|
|
186
|
+
] });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/commands/config.ts
|
|
190
|
+
var VALID_TYPES = ["gh", "azdo"];
|
|
191
|
+
var VALID_TECHNIQUES = ["label", "assignee", "title-contains"];
|
|
192
|
+
var configSetType = new Command("type").description("Set the remote environment type").argument("<value>", "Remote type: gh (GitHub) or azdo (Azure DevOps)").action((value) => {
|
|
193
|
+
if (!VALID_TYPES.includes(value)) {
|
|
194
|
+
process.stderr.write(`Error: invalid type "${value}". Must be one of: ${VALID_TYPES.join(", ")}
|
|
195
|
+
`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
const current = readConfig();
|
|
199
|
+
writeConfig({ ...current, remoteType: value });
|
|
200
|
+
process.stdout.write(`Remote type set to: ${value}
|
|
201
|
+
`);
|
|
202
|
+
});
|
|
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);
|
|
227
|
+
var configCommand = new Command("config").description("Configure automata settings").addCommand(configSet).action(async () => {
|
|
228
|
+
const { waitUntilExit } = render(React2.createElement(ConfigWizard));
|
|
229
|
+
await waitUntilExit();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// src/commands/git.ts
|
|
233
|
+
import { Command as Command2 } from "commander";
|
|
234
|
+
|
|
235
|
+
// src/git/gitService.ts
|
|
236
|
+
import { spawnSync } from "child_process";
|
|
237
|
+
function run(cmd, args) {
|
|
238
|
+
const result = spawnSync(cmd, args, { encoding: "utf8" });
|
|
239
|
+
return {
|
|
240
|
+
stdout: result.stdout ?? "",
|
|
241
|
+
stderr: result.stderr ?? "",
|
|
242
|
+
status: result.status ?? 1
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function getCurrentBranch() {
|
|
246
|
+
const { stdout, status } = run("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
247
|
+
if (status !== 0) {
|
|
248
|
+
throw new Error("Failed to determine current branch. Are you inside a git repository?");
|
|
249
|
+
}
|
|
250
|
+
return stdout.trim();
|
|
251
|
+
}
|
|
252
|
+
function getPrInfo(branch) {
|
|
253
|
+
const { stdout, stderr, status } = run("gh", [
|
|
254
|
+
"pr",
|
|
255
|
+
"view",
|
|
256
|
+
branch,
|
|
257
|
+
"--json",
|
|
258
|
+
"number,title,state,url"
|
|
259
|
+
]);
|
|
260
|
+
if (status !== 0) {
|
|
261
|
+
if (stderr.includes("no pull requests found") || stderr.includes("Could not resolve")) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
throw new Error(stderr.trim() || "Failed to query GitHub. Is `gh` installed and authenticated?");
|
|
265
|
+
}
|
|
266
|
+
return JSON.parse(stdout);
|
|
267
|
+
}
|
|
268
|
+
function isUpstreamGone(branch) {
|
|
269
|
+
const { status } = run("git", ["ls-remote", "--exit-code", "--heads", "origin", branch]);
|
|
270
|
+
return status !== 0;
|
|
271
|
+
}
|
|
272
|
+
function hasUncommittedChanges() {
|
|
273
|
+
const { stdout } = run("git", ["status", "--porcelain"]);
|
|
274
|
+
return stdout.trim().length > 0;
|
|
275
|
+
}
|
|
276
|
+
function checkoutAndPull(targetBranch) {
|
|
277
|
+
const checkout = run("git", ["checkout", targetBranch]);
|
|
278
|
+
if (checkout.status !== 0) {
|
|
279
|
+
throw new Error(`Failed to checkout ${targetBranch}: ${checkout.stderr.trim()}`);
|
|
280
|
+
}
|
|
281
|
+
const pull = run("git", ["pull"]);
|
|
282
|
+
if (pull.status !== 0) {
|
|
283
|
+
throw new Error(`Failed to pull ${targetBranch}: ${pull.stderr.trim()}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function fetchPrune() {
|
|
287
|
+
const result = run("git", ["fetch", "--prune"]);
|
|
288
|
+
if (result.status !== 0) {
|
|
289
|
+
throw new Error(`Failed to fetch --prune: ${result.stderr.trim()}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function deleteLocalBranch(branch) {
|
|
293
|
+
const result = run("git", ["branch", "-d", branch]);
|
|
294
|
+
if (result.status !== 0) {
|
|
295
|
+
throw new Error(`Failed to delete branch ${branch}: ${result.stderr.trim()}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/commands/git.ts
|
|
300
|
+
var getPrInfoCmd = new Command2("get-pr-info").description("Show pull request info for the current branch").option("--json", "Output as JSON").action((options) => {
|
|
301
|
+
let branch;
|
|
302
|
+
try {
|
|
303
|
+
branch = getCurrentBranch();
|
|
304
|
+
} catch (err) {
|
|
305
|
+
process.stderr.write(`Error: ${err.message}
|
|
306
|
+
`);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
let pr;
|
|
310
|
+
try {
|
|
311
|
+
pr = getPrInfo(branch);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
process.stderr.write(`Error: ${err.message}
|
|
314
|
+
`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
if (pr === null) {
|
|
318
|
+
process.stdout.write(`No pull request found for branch: ${branch}
|
|
319
|
+
`);
|
|
320
|
+
process.exit(0);
|
|
321
|
+
}
|
|
322
|
+
if (options.json) {
|
|
323
|
+
process.stdout.write(JSON.stringify(pr, null, 2) + "\n");
|
|
324
|
+
} else {
|
|
325
|
+
process.stdout.write(`PR: #${pr.number}
|
|
326
|
+
Title: ${pr.title}
|
|
327
|
+
State: ${pr.state}
|
|
328
|
+
URL: ${pr.url}
|
|
329
|
+
`);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
var finishFeatureCmd = new Command2("finish-feature").description("Clean up a merged feature branch: checkout develop, pull, and delete local branch").action(() => {
|
|
333
|
+
let branch;
|
|
334
|
+
try {
|
|
335
|
+
branch = getCurrentBranch();
|
|
336
|
+
} catch (err) {
|
|
337
|
+
process.stderr.write(`Error: ${err.message}
|
|
338
|
+
`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
if (branch === "develop") {
|
|
342
|
+
process.stderr.write("Error: finish-feature cannot be run from the develop branch.\n");
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
if (hasUncommittedChanges()) {
|
|
346
|
+
process.stderr.write(
|
|
347
|
+
"Error: You have uncommitted changes. Commit or stash them before running finish-feature.\n"
|
|
348
|
+
);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
let pr;
|
|
352
|
+
try {
|
|
353
|
+
pr = getPrInfo(branch);
|
|
354
|
+
} catch (err) {
|
|
355
|
+
process.stderr.write(`Error: ${err.message}
|
|
356
|
+
`);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
if (pr === null) {
|
|
360
|
+
process.stderr.write(`Error: No pull request found for branch: ${branch}
|
|
361
|
+
`);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
if (pr.state === "OPEN") {
|
|
365
|
+
process.stderr.write(`Error: Pull request #${pr.number} is still open. Merge it before finishing the feature.
|
|
366
|
+
`);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
if (pr.state === "CLOSED") {
|
|
370
|
+
process.stderr.write(
|
|
371
|
+
`Error: Pull request #${pr.number} was closed without merging. finish-feature only proceeds for merged PRs.
|
|
372
|
+
`
|
|
373
|
+
);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
if (!isUpstreamGone(branch)) {
|
|
377
|
+
process.stderr.write(
|
|
378
|
+
`Error: Remote tracking branch 'origin/${branch}' still exists. Push or delete it remotely before finishing.
|
|
379
|
+
`
|
|
380
|
+
);
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
process.stdout.write(`Fetching and pruning remote refs...
|
|
385
|
+
`);
|
|
386
|
+
fetchPrune();
|
|
387
|
+
process.stdout.write(`Checking out develop and pulling latest...
|
|
388
|
+
`);
|
|
389
|
+
checkoutAndPull("develop");
|
|
390
|
+
process.stdout.write(`Deleting local branch: ${branch}
|
|
391
|
+
`);
|
|
392
|
+
deleteLocalBranch(branch);
|
|
393
|
+
process.stdout.write(`Done. Branch '${branch}' has been removed.
|
|
394
|
+
`);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
process.stderr.write(`Error: ${err.message}
|
|
397
|
+
`);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
var gitCommand = new Command2("git").description("Git workflow commands (requires gh CLI)").addCommand(getPrInfoCmd).addCommand(finishFeatureCmd);
|
|
402
|
+
|
|
403
|
+
// src/commands/getReady.ts
|
|
404
|
+
import { Command as Command3 } from "commander";
|
|
405
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
406
|
+
import { existsSync } from "fs";
|
|
407
|
+
import { delimiter, join as join2 } from "path";
|
|
408
|
+
|
|
409
|
+
// src/config/githubService.ts
|
|
410
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
411
|
+
function run2(cmd, args) {
|
|
412
|
+
const result = spawnSync2(cmd, args, { encoding: "utf8" });
|
|
413
|
+
if (result.error) {
|
|
414
|
+
const err = result.error;
|
|
415
|
+
if (err.code === "ENOENT") {
|
|
416
|
+
throw new Error("`gh` CLI is not installed or not on PATH.");
|
|
417
|
+
}
|
|
418
|
+
throw new Error(err.message);
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
stdout: result.stdout ?? "",
|
|
422
|
+
stderr: result.stderr ?? "",
|
|
423
|
+
status: result.status ?? 1
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function listIssues(technique, value) {
|
|
427
|
+
const baseArgs = [
|
|
428
|
+
"issue",
|
|
429
|
+
"list",
|
|
430
|
+
"--state",
|
|
431
|
+
"open",
|
|
432
|
+
"--sort",
|
|
433
|
+
"created",
|
|
434
|
+
"--order",
|
|
435
|
+
"asc",
|
|
436
|
+
"--limit",
|
|
437
|
+
"1",
|
|
438
|
+
"--json",
|
|
439
|
+
"number,title,body,url"
|
|
440
|
+
];
|
|
441
|
+
let filterArgs;
|
|
442
|
+
switch (technique) {
|
|
443
|
+
case "label":
|
|
444
|
+
filterArgs = ["--label", value];
|
|
445
|
+
break;
|
|
446
|
+
case "assignee":
|
|
447
|
+
filterArgs = ["--assignee", value];
|
|
448
|
+
break;
|
|
449
|
+
case "title-contains":
|
|
450
|
+
filterArgs = ["--search", `${value} in:title`];
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
const { stdout, stderr, status } = run2("gh", [...baseArgs, ...filterArgs]);
|
|
454
|
+
if (status !== 0) {
|
|
455
|
+
throw new Error(stderr.trim() || "Failed to query GitHub issues. Is `gh` installed and authenticated?");
|
|
456
|
+
}
|
|
457
|
+
const issues = JSON.parse(stdout);
|
|
458
|
+
if (issues.length === 0) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
return issues[0];
|
|
462
|
+
}
|
|
463
|
+
function postComment(issueNumber, body) {
|
|
464
|
+
const { stderr, status } = run2("gh", ["issue", "comment", String(issueNumber), "--body", body]);
|
|
465
|
+
if (status !== 0) {
|
|
466
|
+
throw new Error(stderr.trim() || `Failed to post comment on issue #${issueNumber}.`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/commands/getReady.ts
|
|
471
|
+
function resolveCommand(name) {
|
|
472
|
+
const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
|
|
473
|
+
for (const dir of pathDirs) {
|
|
474
|
+
const candidate = join2(dir, name);
|
|
475
|
+
if (existsSync(candidate)) return candidate;
|
|
476
|
+
}
|
|
477
|
+
return name;
|
|
478
|
+
}
|
|
479
|
+
function invokeClaudeCode(issue, systemPrompt) {
|
|
480
|
+
const prompt = systemPrompt ? `${systemPrompt}
|
|
481
|
+
|
|
482
|
+
${issue.body}` : issue.body;
|
|
483
|
+
const claudeBin = resolveCommand("claude");
|
|
484
|
+
const result = spawnSync3(claudeBin, ["-p", prompt], { encoding: "utf8", stdio: "inherit" });
|
|
485
|
+
if (result.error) {
|
|
486
|
+
const err = result.error;
|
|
487
|
+
if (err.code === "ENOENT") {
|
|
488
|
+
process.stderr.write("Error: `claude` CLI is not installed or not on PATH.\n");
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
process.stderr.write(`Error: ${err.message}
|
|
492
|
+
`);
|
|
493
|
+
process.exit(1);
|
|
494
|
+
}
|
|
495
|
+
if (result.status !== 0) {
|
|
496
|
+
process.stderr.write(`Error: Claude Code exited with code ${result.status ?? "unknown"}.
|
|
497
|
+
`);
|
|
498
|
+
process.exit(result.status ?? 1);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
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) => {
|
|
502
|
+
const config = readConfig();
|
|
503
|
+
if (config.remoteType !== "gh") {
|
|
504
|
+
process.stderr.write("Error: get-ready is only available for GitHub (gh) mode.\n");
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
if (!config.issueDiscoveryTechnique) {
|
|
508
|
+
process.stderr.write(
|
|
509
|
+
"Error: No issue discovery technique configured. Run `automata config` to set one.\n"
|
|
510
|
+
);
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
if (!config.issueDiscoveryValue) {
|
|
514
|
+
process.stderr.write(
|
|
515
|
+
"Error: No issue discovery value configured. Run `automata config` to set one.\n"
|
|
516
|
+
);
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
let issue;
|
|
520
|
+
try {
|
|
521
|
+
issue = listIssues(config.issueDiscoveryTechnique, config.issueDiscoveryValue);
|
|
522
|
+
} catch (err) {
|
|
523
|
+
process.stderr.write(`Error: ${err.message}
|
|
524
|
+
`);
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
if (issue === null) {
|
|
528
|
+
process.stdout.write("No issues found matching the configured filter.\n");
|
|
529
|
+
process.exit(0);
|
|
530
|
+
}
|
|
531
|
+
if (options.json) {
|
|
532
|
+
process.stdout.write(JSON.stringify({ number: issue.number, title: issue.title, body: issue.body, url: issue.url }, null, 2) + "\n");
|
|
533
|
+
} else {
|
|
534
|
+
process.stdout.write(`Issue: #${issue.number}
|
|
535
|
+
Title: ${issue.title}
|
|
536
|
+
URL: ${issue.url}
|
|
537
|
+
|
|
538
|
+
${issue.body}
|
|
539
|
+
`);
|
|
540
|
+
}
|
|
541
|
+
try {
|
|
542
|
+
postComment(issue.number, "working");
|
|
543
|
+
} catch (err) {
|
|
544
|
+
process.stderr.write(`Error: ${err.message}
|
|
545
|
+
`);
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
if (options.claude !== false) {
|
|
549
|
+
invokeClaudeCode(issue, config.claudeSystemPrompt);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
14
553
|
// src/index.ts
|
|
15
|
-
var program = new
|
|
554
|
+
var program = new Command4();
|
|
16
555
|
program.name("automata").description("Automata CLI tool").version(version, "-v, --version");
|
|
556
|
+
program.addCommand(configCommand);
|
|
557
|
+
program.addCommand(gitCommand);
|
|
558
|
+
program.addCommand(getReadyCommand);
|
|
17
559
|
program.showHelpAfterError();
|
|
18
560
|
program.parse();
|
|
19
561
|
if (process.argv.length <= 2) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "automata-cli",
|
|
3
|
-
"version": "0.2.0-develop.
|
|
3
|
+
"version": "0.2.0-develop.21",
|
|
4
4
|
"description": "Automata CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,11 +23,14 @@
|
|
|
23
23
|
},
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"commander": "^14.0.3"
|
|
26
|
+
"commander": "^14.0.3",
|
|
27
|
+
"ink": "^6.8.0",
|
|
28
|
+
"react": "^19.2.4"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
31
|
"@eslint/js": "^10.0.1",
|
|
30
32
|
"@types/node": "^25.5.0",
|
|
33
|
+
"@types/react": "^19.2.14",
|
|
31
34
|
"eslint": "^10.1.0",
|
|
32
35
|
"prettier": "^3.8.1",
|
|
33
36
|
"tsup": "^8.5.1",
|