jj-octo 0.4.0 → 0.6.0

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.
Files changed (3) hide show
  1. package/README.md +19 -15
  2. package/dist/jj-octo.js +202 -83
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # jj-octo
2
2
 
3
- A [jj](https://github.com/jj-vcs/jj) subcommand for working with GitHub that supports stacking.
3
+ A [jj](https://github.com/jj-vcs/jj) subcommand for GitHub that supports stacked pull requests and works in Claude Code cloud sessions.
4
4
 
5
5
  > [!WARNING]
6
6
  > `jj-octo` is in early development, and its commands may change. So far it is largely vibe coded.
@@ -28,31 +28,35 @@ mise use --global github:kkom/jj-octo@X.Y.Z
28
28
 
29
29
  Then run `jj-octo init` once in each checkout. It makes the checkout a jj repository colocated with git, makes GitHub's default branch `trunk()`, and sets the `jj gh` alias.
30
30
 
31
+ Run `jj-octo init` again after upgrading jj-octo, to set up what the new version needs. `jj-octo doctor` reports what isn't set up, and changes nothing.
32
+
31
33
  ## GitHub access
32
34
 
33
- `jj-octo` reads the repository from the `origin` remote. It takes its token from `GH_TOKEN` or `GITHUB_TOKEN`, and otherwise from `gh auth token`. It sends requests through the proxy that `HTTPS_PROXY` names, and trusts the certificate authorities in `NODE_EXTRA_CA_CERTS` as well as Bun's own.
35
+ `jj-octo` reads the repository from the `origin` remote.
34
36
 
35
- ## In a Claude Code cloud session
37
+ By default, it reads its token from `GH_TOKEN` or `GITHUB_TOKEN`, and otherwise runs `gh auth token`.
36
38
 
37
- In a [Claude Code](https://claude.com/claude-code) cloud session, a proxy carries all traffic to GitHub. It allows REST calls only for the session's repositories, and blocks GraphQL. That's why `jj-octo` uses only GitHub's REST API, while `gh pr` and other tools that depend on GraphQL fail there.
39
+ With `jj-octo.github-auth` set to `git-proxy`, it sends a placeholder token instead. It sends requests through git's proxy for github.com, and trusts git's certificate authorities for it in place of Bun's own. [In a self-hosted environment](#in-a-self-hosted-environment) describes when to use it.
38
40
 
39
- mise can't read this repository's releases there, since it reads them through the GitHub API. Install the npm package instead:
41
+ ## In Claude Code cloud sessions
40
42
 
41
- ```toml
42
- [tools]
43
- bun = "A.B.C"
43
+ A Claude Code [cloud session](https://code.claude.com/docs/en/claude-code-on-the-web) runs in an Anthropic-hosted environment, or in your organization's self-hosted environment. Each needs its own setup.
44
+
45
+ The session's GitHub proxy can append a "Generated by Claude Code" footer to each pull request body that it creates or edits. `jj gh submit` would read the footer as an edit made on GitHub, and write it into the change. To have `submit` leave the footer out when it compares a pull request's body with its change's description, set:
44
46
 
45
- [tools."npm:jj-octo"]
46
- version = "X.Y.Z"
47
- # mise holds back a package that is new or rarely downloaded, and this approves it.
48
- allow_low_downloads = true
49
47
  ```
48
+ jj config set --repo jj-octo.ignore-claude-code-footer true
49
+ ```
50
+
51
+ ### In an Anthropic-hosted environment
52
+
53
+ There, the [GitHub proxy](https://code.claude.com/docs/en/cloud-environments#github-proxy) carries all GitHub traffic. It serves GitHub API requests only for the repositories attached to the session, and rejects GraphQL requests. That's why `jj-octo` uses only GitHub's REST API, while `gh pr` and other tools that depend on GraphQL fail there.
50
54
 
51
- The session sets `HTTPS_PROXY`, `NODE_EXTRA_CA_CERTS` and a placeholder `GH_TOKEN`. Its proxy replaces the placeholder with real credentials. So `jj-octo` works there with its default settings.
55
+ The session sets `HTTPS_PROXY`, `NODE_EXTRA_CA_CERTS`, and `GH_TOKEN` to the placeholder `proxy-injected`. The GitHub proxy replaces the placeholder with your real credentials. So `jj-octo` works there with its default settings.
52
56
 
53
- ## In a Claude Code self-hosted environment
57
+ ### In a self-hosted environment
54
58
 
55
- A self-hosted runner started with `--use-anthropic-git-proxy` sends git through Anthropic's git proxy, which adds the session user's credentials. The sandbox then holds no GitHub token, and only git's config names the proxy. There, have `jj-octo` use the same proxy:
59
+ A [self-hosted runner](https://code.claude.com/docs/en/self-hosted-environments-deploy#use-the-anthropic-git-proxy) started with `--use-anthropic-git-proxy` sends git through the Anthropic git proxy. That proxy adds the GitHub credentials of the session's creator. The session then holds no GitHub token, and only git's config names the proxy. There, have `jj-octo` use the same proxy:
56
60
 
57
61
  ```
58
62
  jj config set --repo jj-octo.github-auth git-proxy
package/dist/jj-octo.js CHANGED
@@ -18239,8 +18239,8 @@ var layer14 = /* @__PURE__ */ layerMergedContext(/* @__PURE__ */ succeed6(fetch)
18239
18239
  // package.json
18240
18240
  var package_default = {
18241
18241
  name: "jj-octo",
18242
- version: "0.4.0",
18243
- description: "A jj subcommand for working with GitHub that supports stacking",
18242
+ version: "0.6.0",
18243
+ description: "A jj subcommand for GitHub that supports stacked pull requests and works in Claude Code cloud sessions",
18244
18244
  license: "MIT",
18245
18245
  repository: {
18246
18246
  type: "git",
@@ -18343,12 +18343,30 @@ var gitLayer = ({ cwd, env } = {}) => effect(Git, gen2(function* () {
18343
18343
  handle.exitCode
18344
18344
  ], { concurrency: "unbounded" });
18345
18345
  return code === 0 ? stdout : yield* new GitFailed({
18346
+ exitCode: code,
18346
18347
  message: `git ${args.join(" ")} failed${stderr.trim() === "" ? "" : `: ${stderr.trim()}`}`
18347
18348
  });
18348
18349
  })).pipe(catchTag2("PlatformError", (error) => fail6(new GitFailed({ message: `git ${args.join(" ")} failed: ${error.message}` }))))
18349
18350
  };
18350
18351
  }));
18351
18352
  var GitLive = gitLayer();
18353
+ var gitConfig = (args) => gen2(function* () {
18354
+ const git = yield* Git;
18355
+ const value = yield* git.read(["config", ...args]).pipe(catch_2((error) => error.exitCode === 1 ? succeed6("") : fail6(error)));
18356
+ return value.trim();
18357
+ });
18358
+ var gitDirectories = gen2(function* () {
18359
+ const git = yield* Git;
18360
+ const [topLevel = "", gitDirectory = "", commonDirectory = ""] = (yield* git.read([
18361
+ "rev-parse",
18362
+ "--path-format=absolute",
18363
+ "--show-toplevel",
18364
+ "--git-dir",
18365
+ "--git-common-dir"
18366
+ ])).split(`
18367
+ `);
18368
+ return { commonDirectory, gitDirectory, topLevel };
18369
+ });
18352
18370
 
18353
18371
  // src/clients/jj.ts
18354
18372
  class JjFailed extends TaggedError2("JjFailed") {
@@ -18376,6 +18394,42 @@ var jjLayer = ({ cwd, env, quiet = false } = {}) => effect(Jj, gen2(function* ()
18376
18394
  return { read, run };
18377
18395
  }));
18378
18396
  var JjLive = jjLayer();
18397
+ var ENTRY = String.raw`json(name) ++ " " ++ json(value) ++ "\n"`;
18398
+ var readJjConfig = (key, { repo = false } = {}) => gen2(function* () {
18399
+ const jj = yield* Jj;
18400
+ const scope = repo ? ["--repo"] : [];
18401
+ const output = yield* jj.read(["config", "list", ...scope, key, "-T", ENTRY]);
18402
+ const lines = output.split(`
18403
+ `).filter((line) => line !== "");
18404
+ const prefix = `${JSON.stringify(key)} `;
18405
+ if (lines.some((line) => !line.startsWith(prefix))) {
18406
+ return yield* new JjFailed({ message: `${key} is a table; set it to a single value` });
18407
+ }
18408
+ const [line] = lines;
18409
+ return line === undefined ? undefined : JSON.parse(line.slice(prefix.length));
18410
+ });
18411
+ var jjConfig = (key) => gen2(function* () {
18412
+ const value = yield* readJjConfig(key);
18413
+ if (value === undefined || typeof value === "string") {
18414
+ return value ?? "";
18415
+ }
18416
+ return yield* new JjFailed({
18417
+ message: `${key} is ${JSON.stringify(value)}; set it to a string`
18418
+ });
18419
+ });
18420
+ var jjFlag = (key) => gen2(function* () {
18421
+ const value = yield* readJjConfig(key);
18422
+ if (value === undefined || typeof value === "boolean") {
18423
+ return value === true;
18424
+ }
18425
+ return yield* new JjFailed({
18426
+ message: `${key} is ${JSON.stringify(value)}; set it to true or false`
18427
+ });
18428
+ });
18429
+ var setJjConfig = (key, value) => gen2(function* () {
18430
+ const jj = yield* Jj;
18431
+ yield* jj.run(["config", "set", "--repo", key, JSON.stringify(value)]);
18432
+ });
18379
18433
 
18380
18434
  // src/cli/gh/stack/changes.ts
18381
18435
  class NotAStack extends TaggedError2("NotAStack") {
@@ -18497,46 +18551,16 @@ var trunkRevset = (branch) => `${JSON.stringify(branch)}@origin`;
18497
18551
  var checkTrunk = (branch) => gen2(function* () {
18498
18552
  if ((yield* commitsAt(`trunk() & present(${trunkRevset(branch)})`)).length === 0) {
18499
18553
  yield* new TrunkMismatch({
18500
- message: `trunk() isn't ${branch}@origin, GitHub's default branch; run \`jj gh init\``
18554
+ message: `trunk() isn't ${branch}@origin, GitHub's default branch; run \`jj-octo init\``
18501
18555
  });
18502
18556
  }
18503
18557
  });
18504
18558
 
18505
- // src/cli/gh/init.ts
18506
- var colocate = gen2(function* () {
18507
- const fs = yield* FileSystem;
18508
- const git = yield* Git;
18509
- const jj = yield* Jj;
18510
- const resolve = (output) => fs.realPath(output.trim()).pipe(orElseSucceed2(() => ""));
18511
- const root = yield* resolve(yield* git.read(["rev-parse", "--show-toplevel"]));
18512
- const jjRoot = yield* jj.read(["root"]).pipe(flatMap4(resolve), orElseSucceed2(() => ""));
18513
- if (jjRoot !== root) {
18514
- const [gitDirectory, commonDirectory] = (yield* git.read([
18515
- "rev-parse",
18516
- "--path-format=absolute",
18517
- "--git-dir",
18518
- "--git-common-dir"
18519
- ])).split(`
18520
- `);
18521
- if (gitDirectory !== commonDirectory) {
18522
- yield* new GitFailed({
18523
- message: "this is a git worktree, where jj would make a second repository; run `jj workspace add` in the main checkout instead"
18524
- });
18525
- }
18526
- yield* jj.run(["git", "init", "--colocate", root]);
18527
- }
18528
- });
18529
- var copyIdentity = gen2(function* () {
18530
- const git = yield* Git;
18531
- const jj = yield* Jj;
18532
- for (const key of ["user.name", "user.email"]) {
18533
- const own = (yield* jj.read(["config", "get", key])).trim();
18534
- const value = (yield* git.read(["config", key]).pipe(orElseSucceed2(() => ""))).trim();
18535
- if (own === "" && value !== "") {
18536
- yield* jj.run(["config", "set", "--repo", key, JSON.stringify(value)]);
18537
- }
18538
- }
18539
- });
18559
+ // src/cli/gh/setup.ts
18560
+ class NotSetUp extends TaggedError2("NotSetUp") {
18561
+ }
18562
+ var forInit = (message) => ({ byInit: true, message });
18563
+ var advice = ({ byInit, message }) => byInit ? `${message}; run \`jj-octo init\`` : message;
18540
18564
  var defaultBranch = gen2(function* () {
18541
18565
  const git = yield* Git;
18542
18566
  const output = yield* git.read(["ls-remote", "--symref", "origin", "HEAD"]);
@@ -18546,43 +18570,123 @@ var defaultBranch = gen2(function* () {
18546
18570
  }
18547
18571
  return branch;
18548
18572
  });
18549
- var setTrunk = gen2(function* () {
18550
- const jj = yield* Jj;
18551
- const branch = yield* defaultBranch;
18552
- if ((yield* commitsAt(`present(${trunkRevset(branch)})`)).length === 0) {
18553
- yield* jj.run(["git", "fetch", "--remote", "origin", "--branch", `exact:${branch}`]);
18554
- }
18555
- if ((yield* bookmarkTargets).get(branch)?.tracked !== true) {
18556
- yield* jj.run(["bookmark", "track", `exact:${branch}`, "--remote", "origin"]);
18557
- }
18558
- yield* jj.run([
18559
- "config",
18560
- "set",
18561
- "--repo",
18562
- 'revset-aliases."trunk()"',
18563
- JSON.stringify(trunkRevset(branch))
18564
- ]);
18565
- return branch;
18573
+ var checkout = gen2(function* () {
18574
+ const fs = yield* FileSystem;
18575
+ const { commonDirectory, gitDirectory, topLevel: root } = yield* gitDirectories;
18576
+ const isColocated = yield* fs.exists(`${root}/.jj`).pipe(orElseSucceed2(() => false));
18577
+ return { isColocated, isWorktree: gitDirectory !== commonDirectory, root };
18578
+ });
18579
+ var colocated = {
18580
+ check: map7(checkout, ({ isColocated, isWorktree }) => {
18581
+ const problem = isWorktree ? {
18582
+ byInit: false,
18583
+ message: "this is a git worktree, where jj would make a second repository; run `jj workspace add` in the main checkout instead"
18584
+ } : forInit("this checkout isn't a jj repository colocated with git");
18585
+ return isColocated ? undefined : problem;
18586
+ }),
18587
+ fix: gen2(function* () {
18588
+ const jj = yield* Jj;
18589
+ const { root } = yield* checkout;
18590
+ yield* jj.run(["git", "init", "--colocate", root]);
18591
+ })
18592
+ };
18593
+ var IDENTITY = ["user.name", "user.email"];
18594
+ var identity2 = (key) => ({
18595
+ check: gen2(function* () {
18596
+ const inJj = (yield* jjConfig(key)) !== "";
18597
+ const inGit = !inJj && (yield* gitConfig([key])) !== "";
18598
+ const problem = inGit ? forInit(`jj has no ${key}, which git has`) : {
18599
+ byInit: false,
18600
+ message: `neither jj nor git has ${key}; set it with \`jj config set --user ${key}\``
18601
+ };
18602
+ return inJj ? undefined : problem;
18603
+ }),
18604
+ fix: gen2(function* () {
18605
+ yield* setJjConfig(key, yield* gitConfig([key]));
18606
+ })
18566
18607
  });
18608
+ var ANONYMOUS = 'mutable() & (author_name(exact:"") | author_email(exact:""))';
18609
+ var authors = {
18610
+ check: gen2(function* () {
18611
+ const values = yield* forEach2(IDENTITY, jjConfig);
18612
+ const hasIdentity = values.every((value) => value !== "");
18613
+ const isAnonymous = hasIdentity && (yield* commitsAt(ANONYMOUS)).length > 0;
18614
+ return isAnonymous ? forInit("some mutable changes have no author") : undefined;
18615
+ }),
18616
+ fix: gen2(function* () {
18617
+ const jj = yield* Jj;
18618
+ yield* jj.run(["metaedit", "--quiet", "--update-author", "-r", ANONYMOUS]);
18619
+ })
18620
+ };
18621
+ var isTracked = (branch) => map7(commitsAt(`tracked_remote_bookmarks(exact:${JSON.stringify(branch)}, remote=exact:"origin")`), (commits) => commits.length > 0);
18622
+ var namesTrunk = (branch) => map7(jjConfig('revset-aliases."trunk()"'), (alias) => alias === trunkRevset(branch) || alias === `${branch}@origin`);
18623
+ var trunk = (branchOnOrigin) => ({
18624
+ check: gen2(function* () {
18625
+ const branch = yield* branchOnOrigin;
18626
+ return (yield* isTracked(branch)) && (yield* namesTrunk(branch)) ? undefined : forInit(`trunk() isn't ${branch}@origin, GitHub's default branch, fetched and tracked`);
18627
+ }),
18628
+ fix: gen2(function* () {
18629
+ const jj = yield* Jj;
18630
+ const branch = yield* branchOnOrigin;
18631
+ if ((yield* commitsAt(`present(${trunkRevset(branch)})`)).length === 0) {
18632
+ yield* jj.run(["git", "fetch", "--remote", "origin", "--branch", `exact:${branch}`]);
18633
+ }
18634
+ if (!(yield* isTracked(branch))) {
18635
+ yield* jj.run(["bookmark", "track", `exact:${branch}`, "--remote", "origin"]);
18636
+ }
18637
+ if (!(yield* namesTrunk(branch))) {
18638
+ yield* setJjConfig('revset-aliases."trunk()"', trunkRevset(branch));
18639
+ }
18640
+ })
18641
+ });
18642
+ var alias = {
18643
+ check: map7(readJjConfig("aliases.gh", { repo: true }), (value) => value === undefined ? forInit("`jj gh` isn't set up") : undefined),
18644
+ fix: setJjConfig("aliases.gh", ["util", "exec", "--", "jj-octo"])
18645
+ };
18646
+ var setup = map7(cached2(defaultBranch), (branch) => ({
18647
+ branch,
18648
+ steps: [...IDENTITY.map((key) => identity2(key)), authors, trunk(branch), alias]
18649
+ }));
18650
+ var runSteps = (run) => gen2(function* () {
18651
+ const { branch, steps } = yield* setup;
18652
+ const notColocated = yield* run(colocated);
18653
+ const found = notColocated === undefined ? yield* forEach2(steps, (step) => run(step)) : [notColocated];
18654
+ const problems = new Set(found.filter((problem) => problem !== undefined));
18655
+ return { branch, problems: [...problems] };
18656
+ });
18657
+
18658
+ // src/cli/gh/doctor.ts
18659
+ var find = ({ check }) => check.pipe(map7((problem) => problem === undefined ? undefined : advice(problem)), catch_2((error2) => succeed6(`could not check: ${error2.message}`)));
18660
+ var problems = map7(runSteps(find), ({ problems: found }) => found);
18661
+ var doctor = gen2(function* () {
18662
+ const found = yield* problems;
18663
+ if (found.length > 0) {
18664
+ yield* new NotSetUp({ message: found.join(`
18665
+ `) });
18666
+ }
18667
+ yield* error("set up for jj-octo");
18668
+ });
18669
+ var doctorCommand = make31("doctor", {}, () => asUserError(doctor)).pipe(withHelp("Check that a checkout is set up for the other commands", "Runs the checks of everything `init` sets up or requires, and changes nothing. Fails with each problem, and what fixes it."));
18670
+
18671
+ // src/cli/gh/init.ts
18672
+ var fixed = ({ check, fix }) => gen2(function* () {
18673
+ const found = yield* check;
18674
+ if (found?.byInit !== true) {
18675
+ return found?.message;
18676
+ }
18677
+ yield* fix;
18678
+ const problem = yield* check;
18679
+ return problem?.byInit === true ? `${problem.message}, though init set it up` : problem?.message;
18680
+ }).pipe(catch_2((error2) => succeed6(error2.message)));
18567
18681
  var init = gen2(function* () {
18568
- const jj = yield* Jj;
18569
- yield* colocate;
18570
- yield* copyIdentity;
18571
- const anonymous = 'mutable() & (author_name(exact:"") | author_email(exact:""))';
18572
- if ((yield* commitsAt(anonymous)).length > 0) {
18573
- yield* jj.run(["metaedit", "--quiet", "--update-author", "-r", anonymous]);
18682
+ const { branch, problems } = yield* runSteps(fixed);
18683
+ if (problems.length > 0) {
18684
+ yield* new NotSetUp({ message: problems.join(`
18685
+ `) });
18574
18686
  }
18575
- const branch = yield* setTrunk;
18576
- yield* jj.run([
18577
- "config",
18578
- "set",
18579
- "--repo",
18580
- "aliases.gh",
18581
- JSON.stringify(["util", "exec", "--", "jj-octo"])
18582
- ]);
18583
- yield* error(`set up jj, with trunk() as ${branch}@origin`);
18687
+ yield* error(`set up jj, with trunk() as ${yield* branch}@origin`);
18584
18688
  });
18585
- var initCommand = make31("init", {}, () => asUserError(init)).pipe(withHelp("Set up a git checkout for the other commands", "Makes it a jj repository colocated with git, unless it already is one, and copies git's name and email into the repository's jj config where jj has none.", "Reads the default branch from `origin`, fetches and tracks it, and sets `trunk()` to it in the repository's config. jj's own `trunk()` guesses from branch names, so without this a default branch it doesn't guess would be the base of pull requests but not of local rebases. `submit`, `merge` and `sync` check that the two agree.", "Sets the `jj gh` alias to run `jj-octo` from `PATH`, in the repository's config. Set it again afterwards to run another build.", "Needs no GitHub token. Running it again changes nothing that is already set up. Run it again after the default branch changes on GitHub."));
18689
+ var initCommand = make31("init", {}, () => asUserError(init)).pipe(withHelp("Set up a git checkout for the other commands", "Makes it a jj repository colocated with git, unless it already is one, and copies git's name and email into the repository's jj config where jj has none. Where neither has them, set them with `jj config set --user`.", "Reads the default branch from `origin`, fetches and tracks it, and sets `trunk()` to it in the repository's config. jj's own `trunk()` guesses from branch names, so without this a default branch it doesn't guess would be the base of pull requests but not of local rebases. `submit`, `merge` and `sync` check that the two agree.", "Sets the `jj gh` alias to run `jj-octo` from `PATH`, in the repository's config, unless the repository has one, such as one that runs another build.", "Needs no GitHub token. Running it again changes nothing that is already set up. Run it again after the default branch changes on GitHub. Fails with what it couldn't set up, after setting up the rest. Where it can't make a colocated jj repository, such as in a git worktree, it sets up nothing else."));
18586
18690
 
18587
18691
  // src/clients/github/openapi.ts
18588
18692
  var responseOf = () => (schema) => schema;
@@ -18711,8 +18815,7 @@ var ownToken = gen2(function* () {
18711
18815
  return stored === "" ? yield* fail6("no token") : make20(stored);
18712
18816
  }).pipe(mapError3(() => new GitHubFailed({ message: "no GitHub token: set GH_TOKEN, or run `gh auth login`" })));
18713
18817
  var gitSetting = (name) => gen2(function* () {
18714
- const git = yield* Git;
18715
- const value = (yield* git.read(["config", "--get-urlmatch", name, "https://github.com"]).pipe(orElseSucceed2(() => ""))).trim();
18818
+ const value = yield* gitConfig(["--get-urlmatch", name, "https://github.com"]).pipe(mapError3(({ message }) => new GitHubFailed({ message })));
18716
18819
  return value === "" ? none2() : some2(value);
18717
18820
  });
18718
18821
  var gitProxy = gen2(function* () {
@@ -18735,8 +18838,7 @@ var gitProxy = gen2(function* () {
18735
18838
  return connection;
18736
18839
  });
18737
18840
  var connection = gen2(function* () {
18738
- const jj = yield* Jj;
18739
- const auth = (yield* jj.read(["config", "get", authKey]).pipe(orElseSucceed2(() => ""))).trim();
18841
+ const auth = yield* jjConfig(authKey).pipe(mapError3(({ message }) => new GitHubFailed({ message })));
18740
18842
  switch (auth) {
18741
18843
  case "":
18742
18844
  case "token": {
@@ -18839,9 +18941,13 @@ var descriptionFrom = ({
18839
18941
  }) => body === "" ? title : `${title}
18840
18942
 
18841
18943
  ${body}`;
18842
- var normalizedDescription = (description) => descriptionFrom(titleAndBody(description.replaceAll(`\r
18944
+ var claudeCodeFooter = /\n*---\n_Generated by \[Claude Code\]\(https:\/\/claude\.ai\/code[^\s)]*\)_\s*$/u;
18945
+ var normalizedDescription = (description, { ignoreClaudeCodeFooter }) => {
18946
+ const text = description.replaceAll(`\r
18843
18947
  `, `
18844
- `)));
18948
+ `);
18949
+ return descriptionFrom(titleAndBody(ignoreClaudeCodeFooter ? text.replace(claudeCodeFooter, "") : text));
18950
+ };
18845
18951
  var descriptionSyncFor = ({
18846
18952
  local,
18847
18953
  onGitHub,
@@ -18859,18 +18965,22 @@ var descriptionSyncFor = ({
18859
18965
  // src/cli/gh/stack/descriptions.ts
18860
18966
  class DescriptionConflict extends TaggedError2("DescriptionConflict") {
18861
18967
  }
18968
+ var ignoreClaudeCodeFooterKey = "jj-octo.ignore-claude-code-footer";
18969
+ var ignoresClaudeCodeFooter = jjFlag(ignoreClaudeCodeFooterKey);
18862
18970
  var descriptionsOf = (commits) => commits.length === 0 ? succeed6(new Map) : readLog(commits.join(" | "), String.raw`commit_id ++ "\t" ++ description.escape_json() ++ "\n"`).pipe(map7((output) => new Map(lines2(output).map((line) => {
18863
18971
  const [commit = "", json = '""'] = line.split("\t");
18864
18972
  const parsed = JSON.parse(json);
18865
18973
  return [commit, typeof parsed === "string" ? parsed : ""];
18866
18974
  }))));
18867
- var syncDescription = ({ bookmark, description }, pullRequest, { overwriteDescriptions, pushedDescription }) => gen2(function* () {
18975
+ var syncDescription = ({ bookmark, description }, pullRequest, { ignoreClaudeCodeFooter, overwriteDescriptions, pushedDescription }) => gen2(function* () {
18868
18976
  const git = yield* Git;
18869
18977
  const jj = yield* Jj;
18870
18978
  const describe = (text) => jj.run(["describe", "-r", bookmarkRevset(bookmark), "-m", text]);
18871
- const local = normalizedDescription(description);
18872
- const onGitHub = normalizedDescription(descriptionFrom(pullRequest));
18873
- const pushed = normalizedDescription(pushedDescription);
18979
+ const local = normalizedDescription(description, { ignoreClaudeCodeFooter });
18980
+ const onGitHub = normalizedDescription(descriptionFrom(pullRequest), {
18981
+ ignoreClaudeCodeFooter
18982
+ });
18983
+ const pushed = normalizedDescription(pushedDescription, { ignoreClaudeCodeFooter });
18874
18984
  return yield* value4(descriptionSyncFor({ local, onGitHub, pushed })).pipe(tag3("InSync", () => succeed6(none2())), tag3("TakeGitHub", () => describe(onGitHub).pipe(andThen2(error(`took the description of #${pullRequest.number} from GitHub`)), as2(none2()))), tag3("SendLocal", () => succeed6(some2(local))), tag3("Merge", () => gen2(function* () {
18875
18985
  if (overwriteDescriptions) {
18876
18986
  return some2(local);
@@ -18920,12 +19030,14 @@ var publish = (stack, { draft, overwriteDescriptions }) => gen2(function* () {
18920
19030
  const found = yield* all2(stack.map((change, index) => github.openPullRequest(change.bookmark).pipe(map7((pullRequest) => ({ base: bases[index] ?? trunk, change, pullRequest })))), { concurrency: "unbounded" });
18921
19031
  const targets = yield* bookmarkTargets;
18922
19032
  const pushedDescriptions = yield* descriptionsOf(bookmarks.flatMap((bookmark) => targets.get(bookmark)?.origin ?? []));
19033
+ const ignoreClaudeCodeFooter = yield* ignoresClaudeCodeFooter;
18923
19034
  const changes = [];
18924
19035
  for (const entry of found) {
18925
19036
  const pushed = targets.get(entry.change.bookmark)?.origin;
18926
19037
  changes.push({
18927
19038
  ...entry,
18928
19039
  description: isNone2(entry.pullRequest) ? none2() : yield* syncDescription(entry.change, entry.pullRequest.value, {
19040
+ ignoreClaudeCodeFooter,
18929
19041
  overwriteDescriptions,
18930
19042
  pushedDescription: (pushed === undefined ? undefined : pushedDescriptions.get(pushed)) ?? ""
18931
19043
  })
@@ -19135,7 +19247,7 @@ var sync4 = (known = []) => gen2(function* () {
19135
19247
  var syncCommand = make31("sync", {}, () => asUserError(gen2(function* () {
19136
19248
  yield* checkTrunk(yield* (yield* GitHub).defaultBranch);
19137
19249
  yield* sync4();
19138
- }).pipe(provide3(GitHubFromOrigin)))).pipe(withHelp("Fetch, and rebase every change onto the trunk", "Fetches, then rebases every change that isn't merged yet onto the trunk, keeping each on the changes below it. A change whose pull request GitHub merged is abandoned and its bookmark forgotten, unless it was edited after the merge.", "A stack that would conflict with the trunk is left where it was, and named. Rebase it yourself with `jj rebase` when you want to resolve it. The rest of a stack whose changes merged is rebased even where it conflicts, and the conflict is recorded in the change.", "Commits added to a branch on GitHub are squashed into the branch's local change, whether or not that change was also edited locally. Where GitHub only rebased a branch, the local change is kept.", "Fails before fetching if `trunk()` isn't GitHub's default branch, which `jj gh init` sets.", "Pushes nothing. Run `jj gh submit` to update the pull requests of a change and the ones below it."));
19250
+ }).pipe(provide3(GitHubFromOrigin)))).pipe(withHelp("Fetch, and rebase every change onto the trunk", "Fetches, then rebases every change that isn't merged yet onto the trunk, keeping each on the changes below it. A change whose pull request GitHub merged is abandoned and its bookmark forgotten, unless it was edited after the merge.", "A stack that would conflict with the trunk is left where it was, and named. Rebase it yourself with `jj rebase` when you want to resolve it. The rest of a stack whose changes merged is rebased even where it conflicts, and the conflict is recorded in the change.", "Commits added to a branch on GitHub are squashed into the branch's local change, whether or not that change was also edited locally. Where GitHub only rebased a branch, the local change is kept.", "Fails before fetching if `trunk()` isn't GitHub's default branch, which `jj-octo init` sets.", "Pushes nothing. Run `jj gh submit` to update the pull requests of a change and the ones below it."));
19139
19251
 
19140
19252
  // src/cli/gh/merge.ts
19141
19253
  class NotMergeable extends TaggedError2("NotMergeable") {
@@ -19205,7 +19317,14 @@ var submitCommand = make31("submit", {
19205
19317
  }, ({ overwriteDescriptions, publish, revision }) => asUserError(submit(revision, { draft: !publish, overwriteDescriptions }).pipe(provide3(GitHubFromOrigin)))).pipe(withHelp("Push a change and the ones below it, and open their pull requests", "Pushes the bookmark of each of those changes. An empty working copy means the change below it, so from the top of a stack this submits all of it. Opens a pull request for each branch that has none, with the title and body from the change's description. Points each pull request at the branch below it, and the bottom one at the default branch.", "New pull requests are opened as drafts, or ready for review with `--publish`. The flag doesn't change a pull request that already exists.", "Keeps each pull request's title and body the same as its change's description. A description edited on GitHub is written into the change, one edited locally is sent to the pull request, and edits on both sides are merged line by line. Edits to the same lines stop the submit before anything is pushed.", "Registers the pull requests as one GitHub stack. A stack of one change is a single pull request, which GitHub has no stack for.", "If changes were reordered, inserted or dropped since the last submit, the GitHub stack is dissolved and registered again under a new number. The pull requests stay open, including the one of a dropped change."));
19206
19318
 
19207
19319
  // src/cli/gh/command.ts
19208
- var jjGhCommand = make31("jj gh").pipe(withSubcommands([initCommand, logCommand, mergeCommand, submitCommand, syncCommand]), withHelp("Commands for publishing jj changes as GitHub pull requests", "A stack is a chain of changes, each published as one pull request on the branch its bookmark names. Each pull request targets the branch of the change below it, so it shows only that change's diff. A single change is a stack of one, and is published as an ordinary pull request.", "The stack of a change is every change connected to it through mutable changes. An empty working copy with no description isn't part of it. `log` shows the whole stack. `submit` and `merge` act on a change and the ones below it, which must be a chain: no merges, no conflicts, and exactly one bookmark on each change.", "The repository is the one the `origin` remote points to. The token is read from `GH_TOKEN` or `GITHUB_TOKEN`, and otherwise from `gh auth token`."));
19320
+ var jjGhCommand = make31("jj gh").pipe(withSubcommands([
19321
+ doctorCommand,
19322
+ initCommand,
19323
+ logCommand,
19324
+ mergeCommand,
19325
+ submitCommand,
19326
+ syncCommand
19327
+ ]), withHelp("Commands for publishing jj changes as GitHub pull requests", "A stack is a chain of changes, each published as one pull request on the branch its bookmark names. Each pull request targets the branch of the change below it, so it shows only that change's diff. A single change is a stack of one, and is published as an ordinary pull request.", "The stack of a change is every change connected to it through mutable changes. An empty working copy with no description isn't part of it. `log` shows the whole stack. `submit` and `merge` act on a change and the ones below it, which must be a chain: no merges, no conflicts, and exactly one bookmark on each change.", "The repository is the one the `origin` remote points to. The token is read from `GH_TOKEN` or `GITHUB_TOKEN`, and otherwise from `gh auth token`."));
19209
19328
 
19210
19329
  // src/main.ts
19211
19330
  runMain2(run5(jjGhCommand, { version: package_default.version }).pipe(provide3([GitLive, JjLive]), provide3(layer13({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jj-octo",
3
- "version": "0.4.0",
4
- "description": "A jj subcommand for working with GitHub that supports stacking",
3
+ "version": "0.6.0",
4
+ "description": "A jj subcommand for GitHub that supports stacked pull requests and works in Claude Code cloud sessions",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",