automata-cli 0.8.0-develop.346 → 0.8.0-develop.353

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
@@ -74,12 +74,12 @@ Checks that the PR exists and is merged, the working tree is clean, and the remo
74
74
  Execute the full GitFlow release sequence and push to `origin`. Only requires `git`.
75
75
 
76
76
  ```bash
77
- automata git publish-release # auto-detect version from master tag
77
+ automata git publish-release # auto-detect version from the trunk tag
78
78
  automata git publish-release 2.0.0 # explicit version
79
79
  automata git publish-release --dry-run # preview without executing
80
80
  ```
81
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`).
82
+ When no version is given, the trunk branch is resolved from `origin` (`main`, `master` or whatever the remote calls it), tags are fetched, and the latest semver tag on `origin/<trunk>` is detected and the minor segment incremented (e.g. `1.2.0 → 1.3.0`). See [docs/git.md](docs/git.md#automata-git-publish-release).
83
83
 
84
84
  ---
85
85
 
@@ -36,7 +36,7 @@ var EXECUTOR_OPTIONS = [
36
36
  { label: "Claude Code", value: "claude" },
37
37
  { label: "Codex", value: "codex" }
38
38
  ];
39
- var MAIN_MENU_OPTIONS = ["Remote / Mode", "Implement-Next", "Prompts", "Issue Watch", "Do Work"];
39
+ var MAIN_MENU_OPTIONS = ["Remote / Mode", "Implement-Next", "Prompts", "Issue Watch", "Do Work", "Git"];
40
40
  var PROMPTS_MENU_OPTIONS = [
41
41
  "Sonar",
42
42
  "Fix-Comments",
@@ -173,6 +173,7 @@ function ConfigWizard() {
173
173
  const [doWorkPrOrphanPrompt, setDoWorkPrOrphanPrompt] = useState(
174
174
  existing.doWork?.prompts?.prOrphan ?? DEFAULT_DO_WORK_PR_ORPHAN_PROMPT
175
175
  );
176
+ const [gitTrunkBranch, setGitTrunkBranch] = useState(existing.git?.trunkBranch ?? "");
176
177
  const [pendingRemote, setPendingRemote] = useState(existing.remoteType ?? "gh");
177
178
  const [pendingTechnique, setPendingTechnique] = useState(
178
179
  existing.issueDiscoveryTechnique ?? "label"
@@ -379,6 +380,16 @@ function ConfigWizard() {
379
380
  setScreen("prompts-menu");
380
381
  },
381
382
  onBack: () => setScreen("prompts-menu")
383
+ },
384
+ "git-trunk-branch": {
385
+ setValue: setGitTrunkBranch,
386
+ onSubmit: () => {
387
+ const branch = gitTrunkBranch.trim();
388
+ const current = readRawConfig();
389
+ writeConfig({ ...current, git: { ...current.git, trunkBranch: branch || void 0 } });
390
+ exit();
391
+ },
392
+ onBack: () => setScreen("main")
382
393
  }
383
394
  };
384
395
  const menus = {
@@ -392,6 +403,7 @@ function ConfigWizard() {
392
403
  else if (chosen === "Implement-Next") setScreen("technique");
393
404
  else if (chosen === "Issue Watch") setScreen("allowed-users");
394
405
  else if (chosen === "Do Work") setScreen("do-work-base-branch");
406
+ else if (chosen === "Git") setScreen("git-trunk-branch");
395
407
  else setScreen("prompts-menu");
396
408
  }
397
409
  },
@@ -550,6 +562,12 @@ function ConfigWizard() {
550
562
  label: "Instructions for a pull request with no linked issue:",
551
563
  value: doWorkPrOrphanPrompt,
552
564
  hint: `Type prompt \xB7 Enter to save \xB7 ${BACK}`
565
+ },
566
+ "git-trunk-branch": {
567
+ title: "Git \u2014 Trunk Branch",
568
+ label: "Branch publish-release releases to (blank = detect it from the remote):",
569
+ value: gitTrunkBranch,
570
+ hint: `Type branch \xB7 Enter to save \xB7 ${BACK}`
553
571
  }
554
572
  };
555
573
  const menuViews = {
package/dist/index.js CHANGED
@@ -214,12 +214,23 @@ var configSetDoWorkPrompt = new Command("do-work-prompt").description("Set the t
214
214
  process.stdout.write(`do-work ${turnKind} prompt set.
215
215
  `);
216
216
  });
217
- var configSet = new Command("set").description("Set a configuration value").addCommand(configSetType).addCommand(configSetIssueDiscoveryTechnique).addCommand(configSetIssueDiscoveryValue).addCommand(configSetClaudeSystemPrompt).addCommand(configSetAllowedUsers).addCommand(configSetAgentUser).addCommand(configSetDoWorkBaseBranch).addCommand(configSetDoWorkProtectedBranches).addCommand(configSetDoWorkExecutor).addCommand(configSetDoWorkModel).addCommand(configSetDoWorkEffort).addCommand(configSetDoWorkMaxRuns).addCommand(configSetDoWorkLockStaleMinutes).addCommand(configSetDoWorkPrompt);
217
+ var configSetGitTrunkBranch = new Command("git-trunk-branch").description("Pin the branch `publish-release` releases to, instead of detecting it").argument("<value>", "Branch name (unset = detect it from the remote)").action((value) => {
218
+ const branch = value.trim();
219
+ if (branch.length === 0) {
220
+ process.stderr.write("Error: git-trunk-branch requires a non-empty branch name.\n");
221
+ process.exit(1);
222
+ }
223
+ const current = readRawConfig();
224
+ writeConfig({ ...current, git: { ...current.git, trunkBranch: branch } });
225
+ process.stdout.write(`git trunk branch set to: ${branch}
226
+ `);
227
+ });
228
+ var configSet = new Command("set").description("Set a configuration value").addCommand(configSetType).addCommand(configSetIssueDiscoveryTechnique).addCommand(configSetIssueDiscoveryValue).addCommand(configSetClaudeSystemPrompt).addCommand(configSetAllowedUsers).addCommand(configSetAgentUser).addCommand(configSetDoWorkBaseBranch).addCommand(configSetDoWorkProtectedBranches).addCommand(configSetDoWorkExecutor).addCommand(configSetDoWorkModel).addCommand(configSetDoWorkEffort).addCommand(configSetDoWorkMaxRuns).addCommand(configSetDoWorkLockStaleMinutes).addCommand(configSetDoWorkPrompt).addCommand(configSetGitTrunkBranch);
218
229
  var configCommand = new Command("config").description("Configure automata settings").addCommand(configSet).action(async () => {
219
230
  const [{ render }, React, { ConfigWizard }] = await Promise.all([
220
231
  import("ink"),
221
232
  import("react"),
222
- import("./ConfigWizard-D6NYL2E6.js")
233
+ import("./ConfigWizard-GULICHML.js")
223
234
  ]);
224
235
  const { waitUntilExit } = render(React.createElement(ConfigWizard));
225
236
  await waitUntilExit();
@@ -280,6 +291,44 @@ function getPrInfo() {
280
291
  };
281
292
  }
282
293
 
294
+ // src/git/trunkDetection.ts
295
+ var TRUNK_CANDIDATES = ["main", "master"];
296
+ var TRUNK_BRANCH_CONFIG_KEY = "git.trunkBranch";
297
+ var ORIGIN_HEAD_PREFIX = "refs/remotes/origin/";
298
+ var SYMREF_PREFIX = "ref: refs/heads/";
299
+ function parseOriginHeadRef(stdout) {
300
+ const ref = stdout.trim();
301
+ if (!ref.startsWith(ORIGIN_HEAD_PREFIX)) return null;
302
+ const branch = ref.slice(ORIGIN_HEAD_PREFIX.length);
303
+ return branch.length > 0 ? branch : null;
304
+ }
305
+ function parseLsRemoteSymref(stdout) {
306
+ for (const line of stdout.split("\n")) {
307
+ const trimmed2 = line.trim();
308
+ if (!trimmed2.startsWith(SYMREF_PREFIX)) continue;
309
+ const branch = trimmed2.slice(SYMREF_PREFIX.length).split(/\s+/)[0];
310
+ if (branch.length > 0) return branch;
311
+ }
312
+ return null;
313
+ }
314
+ function describeTrunkSource(source, branch) {
315
+ switch (source) {
316
+ case "config":
317
+ return `configured as ${TRUNK_BRANCH_CONFIG_KEY}`;
318
+ case "origin-head":
319
+ return "from origin/HEAD";
320
+ case "ls-remote":
321
+ return "from the remote's advertised HEAD";
322
+ case "probe":
323
+ return `probed as origin/${branch}`;
324
+ }
325
+ }
326
+ function unresolvedTrunkMessage(attempted) {
327
+ return `Could not determine the trunk branch of 'origin'.
328
+ Tried: ${attempted.join(", ")}.
329
+ Set it explicitly with: automata config set git-trunk-branch <branch>`;
330
+ }
331
+
283
332
  // src/git/gitService.ts
284
333
  function run2(cmd, args) {
285
334
  const result = spawnSync2(cmd, args, { encoding: "utf8" });
@@ -963,7 +1012,63 @@ function resolveCurrentBranchComments() {
963
1012
  return { ok: true, branch, comments: raw };
964
1013
  }
965
1014
  var SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
966
- function getLatestTagOnMaster() {
1015
+ function remoteBranchExists(branch) {
1016
+ return !isUpstreamGone(branch);
1017
+ }
1018
+ function resolveTrunkBranch() {
1019
+ const configured = readRawConfig().git?.trunkBranch?.trim();
1020
+ if (configured) {
1021
+ return { ok: true, branch: configured, source: "config" };
1022
+ }
1023
+ const attempted = [];
1024
+ attempted.push("origin/HEAD");
1025
+ const head = run2("git", ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]);
1026
+ if (head.status === 0) {
1027
+ const branch = parseOriginHeadRef(head.stdout);
1028
+ if (branch !== null) return { ok: true, branch, source: "origin-head" };
1029
+ }
1030
+ attempted.push("git ls-remote --symref origin HEAD");
1031
+ const symref = run2("git", ["ls-remote", "--symref", "origin", "HEAD"]);
1032
+ if (symref.status === 0) {
1033
+ const branch = parseLsRemoteSymref(symref.stdout);
1034
+ if (branch !== null) return { ok: true, branch, source: "ls-remote" };
1035
+ }
1036
+ for (const candidate of TRUNK_CANDIDATES) {
1037
+ attempted.push(`origin/${candidate}`);
1038
+ if (remoteBranchExists(candidate)) {
1039
+ return { ok: true, branch: candidate, source: "probe" };
1040
+ }
1041
+ }
1042
+ return { ok: false, attempted };
1043
+ }
1044
+ function fetchTrunkAndTags(trunk) {
1045
+ const { status, stderr } = run2("git", [
1046
+ "fetch",
1047
+ "--tags",
1048
+ "origin",
1049
+ `+refs/heads/${trunk}:refs/remotes/origin/${trunk}`
1050
+ ]);
1051
+ if (status !== 0) {
1052
+ return { ok: false, message: stderr.trim() || `git fetch --tags origin ${trunk} failed.` };
1053
+ }
1054
+ return { ok: true };
1055
+ }
1056
+ function localBranchExists(branch) {
1057
+ const { status } = run2("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]);
1058
+ return status === 0;
1059
+ }
1060
+ function trunkBehindCount(trunk) {
1061
+ if (!localBranchExists(trunk)) return 0;
1062
+ const { stdout, status } = run2("git", [
1063
+ "rev-list",
1064
+ "--count",
1065
+ `refs/heads/${trunk}..refs/remotes/origin/${trunk}`
1066
+ ]);
1067
+ if (status !== 0) return 0;
1068
+ const count = Number.parseInt(stdout.trim(), 10);
1069
+ return Number.isNaN(count) ? 0 : count;
1070
+ }
1071
+ function getLatestTagOnTrunk(ref) {
967
1072
  const { stdout, status } = run2("git", [
968
1073
  "describe",
969
1074
  "--tags",
@@ -972,7 +1077,7 @@ function getLatestTagOnMaster() {
972
1077
  "[0-9]*.[0-9]*.[0-9]*",
973
1078
  "--match",
974
1079
  "v[0-9]*.[0-9]*.[0-9]*",
975
- "master"
1080
+ ref
976
1081
  ]);
977
1082
  if (status !== 0) return null;
978
1083
  const tag = stdout.trim();
@@ -993,17 +1098,45 @@ ${stderr.trim()}`);
993
1098
  }
994
1099
  return stdout.trim().length > 0;
995
1100
  }
996
- function publishRelease(version2, dryRun) {
1101
+ function checkReleasePreconditions() {
1102
+ let branch;
1103
+ try {
1104
+ branch = getCurrentBranch();
1105
+ } catch (err) {
1106
+ return { ok: false, message: err.message };
1107
+ }
1108
+ if (branch !== "develop") {
1109
+ return {
1110
+ ok: false,
1111
+ message: `publish-release must be run from the 'develop' branch (currently on '${branch}').`
1112
+ };
1113
+ }
1114
+ if (hasUncommittedChanges()) {
1115
+ return {
1116
+ ok: false,
1117
+ message: "You have uncommitted changes. Commit or stash them before publishing a release."
1118
+ };
1119
+ }
1120
+ return { ok: true };
1121
+ }
1122
+ function publishRelease(version2, dryRun, trunk) {
997
1123
  const releaseBranch = `release/${version2}`;
1124
+ const checkoutTrunk = localBranchExists(trunk) ? { args: ["checkout", trunk], desc: `git checkout ${trunk}` } : {
1125
+ args: ["checkout", "-b", trunk, `origin/${trunk}`],
1126
+ desc: `git checkout -b ${trunk} origin/${trunk}`
1127
+ };
998
1128
  const steps = [
999
1129
  { args: ["checkout", "-b", releaseBranch], desc: `git checkout -b ${releaseBranch}` },
1000
- { args: ["checkout", "master"], desc: `git checkout master` },
1130
+ checkoutTrunk,
1001
1131
  { args: ["merge", "--no-ff", releaseBranch], desc: `git merge --no-ff ${releaseBranch}` },
1002
1132
  { args: ["tag", version2], desc: `git tag ${version2}` },
1003
1133
  { args: ["checkout", "develop"], desc: `git checkout develop` },
1004
1134
  { args: ["merge", "--no-ff", releaseBranch], desc: `git merge --no-ff ${releaseBranch}` },
1005
1135
  { args: ["branch", "-d", releaseBranch], desc: `git branch -d ${releaseBranch}` },
1006
- { args: ["push", "origin", "develop", "master", version2], desc: `git push origin develop master ${version2}` }
1136
+ {
1137
+ args: ["push", "origin", "develop", trunk, version2],
1138
+ desc: `git push origin develop ${trunk} ${version2}`
1139
+ }
1007
1140
  ];
1008
1141
  for (const step of steps) {
1009
1142
  if (dryRun) {
@@ -1019,6 +1152,29 @@ ${stderr.trim()}`);
1019
1152
  }
1020
1153
  }
1021
1154
 
1155
+ // src/git/releaseVersion.ts
1156
+ var SEMVER_ARG_RE = /^\d+\.\d+\.\d+$/;
1157
+ function resolveReleaseVersion(requested, trunkRef, latestTag) {
1158
+ if (requested !== void 0) {
1159
+ if (!SEMVER_ARG_RE.test(requested)) {
1160
+ return {
1161
+ ok: false,
1162
+ message: `Version '${requested}' is not valid semver. Use X.Y.Z format (e.g. 1.2.0).`
1163
+ };
1164
+ }
1165
+ return { ok: true, version: requested, notice: null };
1166
+ }
1167
+ const latest = latestTag();
1168
+ if (latest === null) {
1169
+ return {
1170
+ ok: false,
1171
+ message: `No semver tag found on ${trunkRef}. Pass a version explicitly: automata git publish-release <X.Y.Z>`
1172
+ };
1173
+ }
1174
+ const version2 = bumpMinorVersion(latest);
1175
+ return { ok: true, version: version2, notice: `Auto-detected version: ${latest} \u2192 ${version2}` };
1176
+ }
1177
+
1022
1178
  // src/commands/git.ts
1023
1179
  var FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED"]);
1024
1180
  var SKIP_CONCLUSIONS = /* @__PURE__ */ new Set(["SKIPPED", "NEUTRAL"]);
@@ -1383,59 +1539,58 @@ var finishFeatureCmd = new Command2("finish-feature").description("Clean up a me
1383
1539
  process.exit(1);
1384
1540
  }
1385
1541
  });
1386
- var SEMVER_ARG_RE = /^\d+\.\d+\.\d+$/;
1387
- 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(
1542
+ 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 the trunk tag if omitted)").option("--dry-run", "Print git commands without executing them").addHelpText(
1388
1543
  "after",
1389
1544
  `
1390
1545
  Release sequence:
1391
1546
  1. git checkout -b release/<version>
1392
- 2. git checkout master && git merge --no-ff release/<version>
1547
+ 2. git checkout <trunk> && git merge --no-ff release/<version>
1393
1548
  3. git tag <version>
1394
1549
  4. git checkout develop && git merge --no-ff release/<version>
1395
1550
  5. git branch -d release/<version>
1396
- 6. git push origin develop master <version>
1551
+ 6. git push origin develop <trunk> <version>
1552
+
1553
+ <trunk> is resolved from origin \u2014 git.trunkBranch in .automata/config.json if
1554
+ set, else origin/HEAD, the remote's advertised HEAD, or a probe of main/master.
1555
+ Tags are fetched from origin first, in --dry-run too, so the version a dry run
1556
+ prints is the one a real run would use.
1397
1557
 
1398
- When [version] is omitted the latest semver tag on master is detected and the
1399
- minor segment is incremented (e.g. 1.2.0 \u2192 1.3.0).`
1558
+ When [version] is omitted the latest semver tag on origin/<trunk> is detected
1559
+ and the minor segment is incremented (e.g. 1.2.0 \u2192 1.3.0).`
1400
1560
  ).action((version2, options) => {
1401
1561
  const dryRun = options.dryRun ?? false;
1402
- let branch;
1403
- try {
1404
- branch = getCurrentBranch();
1405
- } catch (err) {
1406
- process.stderr.write(`Error: ${err.message}
1562
+ const preconditions = checkReleasePreconditions();
1563
+ if (!preconditions.ok) {
1564
+ process.stderr.write(`Error: ${preconditions.message}
1407
1565
  `);
1408
1566
  process.exit(1);
1409
1567
  }
1410
- if (branch !== "develop") {
1411
- process.stderr.write(
1412
- `Error: publish-release must be run from the 'develop' branch (currently on '${branch}').
1413
- `
1414
- );
1568
+ const trunk = resolveTrunkBranch();
1569
+ if (!trunk.ok) {
1570
+ process.stderr.write(`Error: ${unresolvedTrunkMessage(trunk.attempted)}
1571
+ `);
1415
1572
  process.exit(1);
1416
1573
  }
1417
- if (hasUncommittedChanges()) {
1418
- process.stderr.write("Error: You have uncommitted changes. Commit or stash them before publishing a release.\n");
1574
+ const trunkBranch = trunk.branch;
1575
+ const trunkRef = `origin/${trunkBranch}`;
1576
+ process.stdout.write(`Trunk branch: ${trunkBranch} (${describeTrunkSource(trunk.source, trunkBranch)})
1577
+ `);
1578
+ const fetched = fetchTrunkAndTags(trunkBranch);
1579
+ if (!fetched.ok) {
1580
+ process.stderr.write(`Error: Failed to fetch ${trunkBranch} and tags from origin.
1581
+ ${fetched.message}
1582
+ `);
1419
1583
  process.exit(1);
1420
1584
  }
1421
- let resolvedVersion;
1422
- if (version2 !== void 0) {
1423
- if (!SEMVER_ARG_RE.test(version2)) {
1424
- process.stderr.write(`Error: Version '${version2}' is not valid semver. Use X.Y.Z format (e.g. 1.2.0).
1585
+ const versionResult = resolveReleaseVersion(version2, trunkRef, () => getLatestTagOnTrunk(trunkRef));
1586
+ if (!versionResult.ok) {
1587
+ process.stderr.write(`Error: ${versionResult.message}
1425
1588
  `);
1426
- process.exit(1);
1427
- }
1428
- resolvedVersion = version2;
1429
- } else {
1430
- const latest = getLatestTagOnMaster();
1431
- if (latest === null) {
1432
- process.stderr.write(
1433
- "Error: No semver tag found on master. Pass a version explicitly: automata git publish-release <X.Y.Z>\n"
1434
- );
1435
- process.exit(1);
1436
- }
1437
- resolvedVersion = bumpMinorVersion(latest);
1438
- process.stdout.write(`Auto-detected version: ${latest} \u2192 ${resolvedVersion}
1589
+ process.exit(1);
1590
+ }
1591
+ const resolvedVersion = versionResult.version;
1592
+ if (versionResult.notice !== null) {
1593
+ process.stdout.write(`${versionResult.notice}
1439
1594
  `);
1440
1595
  }
1441
1596
  if (tagExists(resolvedVersion)) {
@@ -1443,6 +1598,14 @@ minor segment is incremented (e.g. 1.2.0 \u2192 1.3.0).`
1443
1598
  `);
1444
1599
  process.exit(1);
1445
1600
  }
1601
+ const behind = trunkBehindCount(trunkBranch);
1602
+ if (behind > 0) {
1603
+ process.stderr.write(
1604
+ `Error: Local branch '${trunkBranch}' is ${String(behind)} commit(s) behind ${trunkRef}. Update it (git checkout ${trunkBranch} && git merge --ff-only ${trunkRef}) or delete it, then re-run.
1605
+ `
1606
+ );
1607
+ process.exit(1);
1608
+ }
1446
1609
  if (dryRun) {
1447
1610
  process.stdout.write(`Dry-run: release ${resolvedVersion}
1448
1611
  `);
@@ -1451,7 +1614,7 @@ minor segment is incremented (e.g. 1.2.0 \u2192 1.3.0).`
1451
1614
  `);
1452
1615
  }
1453
1616
  try {
1454
- publishRelease(resolvedVersion, dryRun);
1617
+ publishRelease(resolvedVersion, dryRun, trunkBranch);
1455
1618
  } catch (err) {
1456
1619
  process.stderr.write(`Error: ${err.message}
1457
1620
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.8.0-develop.346",
3
+ "version": "0.8.0-develop.353",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "engines": {