jjhub 0.1.2 → 0.1.4

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
@@ -1,4 +1,4 @@
1
- # jjhub — the `jjh` CLI
1
+ # jjhub
2
2
 
3
3
  GitHub CLI with Jujutsu-native superpowers: stable change IDs, stacks,
4
4
  platform-wide undo, and conflicts that wait — layered over plain GitHub
@@ -7,7 +7,7 @@ repos by a [JJHub](https://jjhub.johnhenry.me) server.
7
7
  ## Install
8
8
 
9
9
  ```sh
10
- npm install -g jjhub # installs `jjhub` and its short alias `jjh`
10
+ npm install -g jjhub # installs the `jjhub` command
11
11
  # or one-off:
12
12
  npx jjhub --help
13
13
  ```
@@ -15,20 +15,20 @@ npx jjhub --help
15
15
  ## Quick start
16
16
 
17
17
  ```sh
18
- jjh auth login # device-flow sign-in to your JJHub server
19
- jjh repo list # what the server tracks
20
- jjh change create "Fix login" # a change with a STABLE id (survives rebases)
21
- jjh change list
22
- jjh undo # platform-wide undo, from the op log
23
- jjh agents-md >> AGENTS.md # teach your repo's coding agents to prefer jjh
24
- jjh mcp serve # stdio MCP server over the same API
25
- jjh acp serve # ACP agent (Zed etc.)
26
- jjh help # everything else
18
+ jjhub auth login # device-flow sign-in to your JJHub server
19
+ jjhub repo list # what the server tracks
20
+ jjhub change create "Fix login" # a change with a STABLE id (survives rebases)
21
+ jjhub change list
22
+ jjhub undo # platform-wide undo, from the op log
23
+ jjhub agents-md >> AGENTS.md # teach your repo's coding agents to prefer jjhub
24
+ jjhub mcp serve # stdio MCP server over the same API
25
+ jjhub acp serve # ACP agent (Zed etc.)
26
+ jjhub help # everything else
27
27
  ```
28
28
 
29
29
  The CLI talks to the JJHub server at `JJHUB_URL` (default
30
- `http://localhost:3000`) — e.g. `JJHUB_URL=https://jjhub.johnhenry.me jjh repo list`,
31
- or use `jjh auth login --url https://jjhub.johnhenry.me` once and the saved
30
+ `http://localhost:3000`) — e.g. `JJHUB_URL=https://jjhub.johnhenry.me jjhub repo list`,
31
+ or use `jjhub auth login --url https://jjhub.johnhenry.me` once and the saved
32
32
  config takes over.
33
33
 
34
34
  This package is the standalone CLI build (single bundled file, no runtime
@@ -83,7 +83,7 @@ var init_api_client = __esm({
83
83
  /**
84
84
  * `apiKey` falls back to JJHUB_API_KEY when omitted (the legacy shared
85
85
  * secret / ops credential). `opts.bearerToken` is an OAuth access token
86
- * from `jjh auth login` (issue #43) — when present it wins, sent as
86
+ * from `jjhub auth login` (issue #43) — when present it wins, sent as
87
87
  * `Authorization: Bearer`.
88
88
  */
89
89
  constructor(baseUrl, apiKey, opts = {}) {
@@ -298,11 +298,11 @@ var init_api_client = __esm({
298
298
  {}
299
299
  );
300
300
  }
301
- assessLandRisk(id, changeId2) {
301
+ assessLandRisk(id, changeId2, input) {
302
302
  return this.request(
303
303
  "POST",
304
304
  `/repositories/${encodeURIComponent(id)}/changes/${encodeURIComponent(changeId2)}/land-risk`,
305
- {}
305
+ input
306
306
  );
307
307
  }
308
308
  listOperations(id) {
@@ -349,6 +349,16 @@ var init_api_client = __esm({
349
349
  reportCommit(repoId, changeId2, sha) {
350
350
  return this.request("POST", `/repositories/${enc(repoId)}/changes/${enc(changeId2)}/commit`, { sha });
351
351
  }
352
+ /**
353
+ * A short-lived GitHub push credential for real-commit push, minted
354
+ * server-side from this repo's GitHub App installation. Null means
355
+ * there's nothing to mint (no App configured, or not installed on this
356
+ * repo) — the caller (daemon.ts) treats that as a normal graceful-degrade.
357
+ * May not exist on older servers — callers should catch isMissingEndpoint.
358
+ */
359
+ getPushCredential(id) {
360
+ return this.request("POST", `/repositories/${enc(id)}/push-credential`);
361
+ }
352
362
  // --- deleteBookmark: force is a query param, not a JSON body (see operations.ts) ---
353
363
  deleteBookmark(repoId, name, opts) {
354
364
  const qs = opts?.force ? "?force=1" : "";
@@ -59435,9 +59445,12 @@ var init_operations = __esm({
59435
59445
  jjOverlayManaged: external_exports.boolean().optional().describe("Whether JJHub manages jj-native semantics for this repo."),
59436
59446
  stackReviewEnabled: external_exports.boolean().optional().describe("Whether the stack review screen is enabled."),
59437
59447
  defaultBookmark: external_exports.string().min(1).optional().describe("Default bookmark name, e.g. 'main'."),
59438
- contentMode: external_exports.enum(["github", "local"]).optional()
59448
+ contentMode: external_exports.enum(["github", "local"]).optional(),
59449
+ landCheckAppearTimeoutMs: external_exports.number().int().min(0).nullable().optional().describe(
59450
+ "Per-repo override (ms) of how long land waits for a check-run to appear before concluding the repo has no CI. 0 skips the wait entirely. null clears the override (inherits the server-wide default)."
59451
+ )
59439
59452
  }),
59440
- description: "Update a repository's post-creation settings (jjOverlayManaged, stackReviewEnabled, defaultBookmark, contentMode) \u2014 the update dual of createRepository's same-named fields. Omitted fields are left unchanged.",
59453
+ description: "Update a repository's post-creation settings (jjOverlayManaged, stackReviewEnabled, defaultBookmark, contentMode, landCheckAppearTimeoutMs) \u2014 the update dual of createRepository's same-named fields. Omitted fields are left unchanged.",
59441
59454
  returns: "Repository"
59442
59455
  }),
59443
59456
  op({
@@ -59565,9 +59578,12 @@ var init_operations = __esm({
59565
59578
  content: external_exports.string().describe("File content \u2014 UTF-8 text, or base64 if encoding='base64'."),
59566
59579
  encoding: external_exports.enum(["utf-8", "base64"]).optional().describe("Defaults to 'utf-8'.")
59567
59580
  })
59568
- ).describe("Files to write.")
59581
+ ).optional().describe("Files to write. May be omitted/empty for a delete-only commit."),
59582
+ deletePaths: external_exports.array(external_exports.string()).optional().describe(
59583
+ "Repo-relative paths to remove, in the same commit as `files`. A path may not appear in both."
59584
+ )
59569
59585
  }),
59570
- description: "Commit one or more files directly onto a change's projected GitHub branch.",
59586
+ description: "Commit one or more files directly onto a change's projected GitHub branch \u2014 write, delete, or both in one commit.",
59571
59587
  returns: "Change"
59572
59588
  }),
59573
59589
  op({
@@ -59749,7 +59765,10 @@ var init_operations = __esm({
59749
59765
  method: "POST",
59750
59766
  pathParams: ["id", "changeId"],
59751
59767
  path: ([id, changeId2]) => `/repositories/${id}/changes/${changeId2}/land-risk`,
59752
- body: external_exports.object({}),
59768
+ // narrative:false = deterministic-only (skips the AI narrator entirely)
59769
+ // the web land modal uses it so risk renders in ~2 GitHub round trips
59770
+ // instead of waiting up to the AI provider timeout.
59771
+ body: external_exports.object({ narrative: external_exports.boolean().optional() }),
59753
59772
  description: "Deterministic land-risk assessment (checks/conflicts/restack/diff-size signals) with an optional AI narrative. Works without an AI provider; read-only.",
59754
59773
  returns: '{ riskLevel: "low" | "medium" | "high"; reasons: string[]; signals: Record<string, unknown>; narrative: string | null; model: string | null }'
59755
59774
  }),
@@ -64491,6 +64510,30 @@ var init_acp = __esm({
64491
64510
  }
64492
64511
  });
64493
64512
 
64513
+ // src/domain/issue-refs.ts
64514
+ function extractClosedIssueNumbers(description) {
64515
+ const numbers = /* @__PURE__ */ new Set();
64516
+ for (const m of description.matchAll(CLOSING_KEYWORD_RE)) {
64517
+ const n = Number(m[1]);
64518
+ if (Number.isInteger(n)) numbers.add(n);
64519
+ }
64520
+ return [...numbers];
64521
+ }
64522
+ function describesClosingIssue(description, issueNumber) {
64523
+ return extractClosedIssueNumbers(description).includes(issueNumber);
64524
+ }
64525
+ function findChangeClosingIssue(items, issueNumber) {
64526
+ const matches = items.filter((item) => describesClosingIssue(item.description, issueNumber));
64527
+ return matches.length === 1 ? matches[0] : null;
64528
+ }
64529
+ var CLOSING_KEYWORD_RE;
64530
+ var init_issue_refs = __esm({
64531
+ "src/domain/issue-refs.ts"() {
64532
+ "use strict";
64533
+ CLOSING_KEYWORD_RE = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s*#(\d+)\b/gi;
64534
+ }
64535
+ });
64536
+
64494
64537
  // src/acp/commands.ts
64495
64538
  function toSnakeCase2(name) {
64496
64539
  return name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
@@ -64689,12 +64732,15 @@ async function serveAcp(api, opts = {}) {
64689
64732
  process.once("SIGINT", resolve);
64690
64733
  });
64691
64734
  }
64692
- var JJHubAcpAgent;
64735
+ var CHANGE_ID_RE, ISSUE_REF_RE, JJHubAcpAgent;
64693
64736
  var init_agent = __esm({
64694
64737
  "src/acp/agent.ts"() {
64695
64738
  "use strict";
64696
64739
  init_acp();
64740
+ init_issue_refs();
64697
64741
  init_commands();
64742
+ CHANGE_ID_RE = /^CHG-\d+$/i;
64743
+ ISSUE_REF_RE = /^#?(\d+)$/;
64698
64744
  JJHubAcpAgent = class {
64699
64745
  constructor(api, conn, opts = {}) {
64700
64746
  this.api = api;
@@ -64727,7 +64773,7 @@ var init_agent = __esm({
64727
64773
  });
64728
64774
  return { sessionId };
64729
64775
  }
64730
- /** Credentials come from ~/.config/jjhub (jjh auth login) — nothing to do in-protocol. */
64776
+ /** Credentials come from ~/.config/jjhub (jjhub auth login) — nothing to do in-protocol. */
64731
64777
  authenticate() {
64732
64778
  }
64733
64779
  async prompt(params) {
@@ -64749,6 +64795,22 @@ var init_agent = __esm({
64749
64795
  await this.say(sessionId, `error: ${bound.error}`);
64750
64796
  return { stopReason: "end_turn" };
64751
64797
  }
64798
+ const changeIdIdx = cmd.pathParams.indexOf("changeId");
64799
+ if (changeIdIdx !== -1) {
64800
+ const rawChangeId = bound.pathArgs[changeIdIdx];
64801
+ const issueMatch = CHANGE_ID_RE.test(rawChangeId) ? null : rawChangeId.match(ISSUE_REF_RE);
64802
+ if (issueMatch) {
64803
+ const issueNumber = Number(issueMatch[1]);
64804
+ const repositoryIdIdx = cmd.pathParams.indexOf("repositoryId");
64805
+ const repositoryId2 = repositoryIdIdx !== -1 ? bound.pathArgs[repositoryIdIdx] : void 0;
64806
+ const resolved = repositoryId2 ? await this.resolveIssueReference(repositoryId2, issueNumber) : null;
64807
+ if (!resolved) {
64808
+ await this.say(sessionId, `couldn't find a change that closes issue #${issueNumber} in this repo`);
64809
+ return { stopReason: "end_turn" };
64810
+ }
64811
+ bound.pathArgs[changeIdIdx] = resolved.changeId;
64812
+ }
64813
+ }
64752
64814
  if (cmd.requiresForceConfirmation && bound.body.force === true) {
64753
64815
  const confirmed = await this.confirmForce(sessionId, cmd, bound.pathArgs);
64754
64816
  if (this.cancelled.has(sessionId)) return { stopReason: "cancelled" };
@@ -64838,6 +64900,17 @@ var init_agent = __esm({
64838
64900
  return true;
64839
64901
  }
64840
64902
  }
64903
+ /**
64904
+ * Resolve "#123" (or "123") to the change whose description closes that
64905
+ * issue, using only data the agent already fetches over its normal API
64906
+ * surface (list_changes) — no new endpoint or GitHub permission needed.
64907
+ * Ambiguous (>1 match) or absent (0 matches) both resolve to null; the
64908
+ * caller must not guess.
64909
+ */
64910
+ async resolveIssueReference(repositoryId2, issueNumber) {
64911
+ const changes = await this.api.listChanges(repositoryId2);
64912
+ return findChangeClosingIssue(changes, issueNumber);
64913
+ }
64841
64914
  say(sessionId, text) {
64842
64915
  return this.conn.sessionUpdate({
64843
64916
  sessionId,
@@ -93318,7 +93391,7 @@ async function resolveRepoId(api, repoFlag, resolveContext) {
93318
93391
  const match = repos.find((r) => r.fullName === fullName);
93319
93392
  if (!match) {
93320
93393
  throw new Error(
93321
- `no JJHub repository for ${fullName} \u2014 run \`jjh repo init${repoFlag ? ` ${fullName}` : ""}\` to create it`
93394
+ `no JJHub repository for ${fullName} \u2014 run \`jjhub repo init${repoFlag ? ` ${fullName}` : ""}\` to create it`
93322
93395
  );
93323
93396
  }
93324
93397
  return match.id;
@@ -93417,7 +93490,7 @@ async function runDaemonOnce(jj, api, repoId, opts = {}) {
93417
93490
  defaultBookmark = (await api.getRepository(repoId)).defaultBookmark;
93418
93491
  } catch {
93419
93492
  }
93420
- const pushEnabled = opts.pushCommits ?? Boolean(process.env.GITHUB_TOKEN);
93493
+ const pushEnabled = opts.pushCommits ?? true;
93421
93494
  let pusher = pushEnabled ? void 0 : null;
93422
93495
  const resolvePusher = async () => {
93423
93496
  if (pusher !== void 0) return pusher;
@@ -93426,16 +93499,38 @@ async function runDaemonOnce(jj, api, repoId, opts = {}) {
93426
93499
  pusher = opts.pushCommit;
93427
93500
  return pusher;
93428
93501
  }
93429
- const token = process.env.GITHUB_TOKEN;
93430
93502
  try {
93431
93503
  const repo = await api.getRepository(repoId);
93432
- if (token && opts.repoPath && GITHUB_FULLNAME_RE.test(repo.fullName)) {
93433
- pusher = makeGitHubPushCommit(opts.repoPath, repo.fullName, token);
93434
- } else {
93504
+ if (!opts.repoPath) {
93505
+ log("[daemon] commit push skipped: no local repo path");
93506
+ return pusher;
93507
+ }
93508
+ if (!GITHUB_FULLNAME_RE.test(repo.fullName)) {
93509
+ log(
93510
+ `[daemon] commit push skipped: repo fullName "${repo.fullName}" does not look like a GitHub repo`
93511
+ );
93512
+ return pusher;
93513
+ }
93514
+ let credential;
93515
+ try {
93516
+ credential = await api.getPushCredential(repoId);
93517
+ } catch (err) {
93518
+ if (isMissingEndpoint(err)) {
93519
+ log(
93520
+ "[daemon] commit push skipped: server does not support push credentials yet (upgrade the server)"
93521
+ );
93522
+ } else {
93523
+ log(`[daemon] commit push disabled: ${err instanceof Error ? err.message : String(err)}`);
93524
+ }
93525
+ return pusher;
93526
+ }
93527
+ if (!credential) {
93435
93528
  log(
93436
- `[daemon] commit push skipped: ${!token ? "no GITHUB_TOKEN" : !opts.repoPath ? "no local repo path" : `repo fullName "${repo.fullName}" does not look like a GitHub repo`}`
93529
+ "[daemon] commit push skipped: install the JJHub GitHub App on this repository to enable real-commit push"
93437
93530
  );
93531
+ return pusher;
93438
93532
  }
93533
+ pusher = makeGitHubPushCommit(opts.repoPath, repo.fullName, credential.token);
93439
93534
  } catch (err) {
93440
93535
  log(`[daemon] commit push disabled: ${err instanceof Error ? err.message : String(err)}`);
93441
93536
  }
@@ -93631,7 +93726,7 @@ function isProcessAlive(pid) {
93631
93726
  }
93632
93727
  }
93633
93728
  var defaultSpawnFn = (command, args, options) => spawn(command, args, options);
93634
- var CLI_ENTRY = fileURLToPath(new URL("../../bin/jjh.mjs", import.meta.url));
93729
+ var CLI_ENTRY = fileURLToPath(new URL("../../bin/jjhub.mjs", import.meta.url));
93635
93730
  function spawnDaemon(opts) {
93636
93731
  const args = [
93637
93732
  CLI_ENTRY,
@@ -93715,9 +93810,9 @@ var defaultSpawnGh = (argv) => new Promise((resolve, reject) => {
93715
93810
  });
93716
93811
 
93717
93812
  // src/cli/index.ts
93718
- var USAGE = `jjh \u2014 GitHub CLI with Jujutsu-native superpowers (JJHub)
93813
+ var USAGE = `jjhub \u2014 GitHub CLI with Jujutsu-native superpowers (JJHub)
93719
93814
 
93720
- Usage: jjh <command> [args] [--repo owner/name|repoId] [--json]
93815
+ Usage: jjhub <command> [args] [--repo owner/name|repoId] [--json]
93721
93816
 
93722
93817
  repo init [owner/name] [--github-token pat]
93723
93818
  create the JJHub overlay for this repo
@@ -93730,7 +93825,7 @@ Usage: jjh <command> [args] [--repo owner/name|repoId] [--json]
93730
93825
  repo delete hard-delete (ops credential only \u2014 archive is the normal path)
93731
93826
 
93732
93827
  user list list known users (identity is GitHub-federated;
93733
- sign in with "jjh auth login")
93828
+ sign in with "jjhub auth login")
93734
93829
 
93735
93830
  change new <title> [--parent CHG-x] [--stack id]
93736
93831
  change list
@@ -93741,8 +93836,9 @@ Usage: jjh <command> [args] [--repo owner/name|repoId] [--json]
93741
93836
  change abandon <changeId>
93742
93837
  change restack <changeId> rebase a needs-restack change onto its rewritten ancestor
93743
93838
  change land <changeId> [--force] --force bypasses a failing/pending check-run gate
93744
- change commit <changeId> <file...> [-m msg]
93745
- commit working-tree files onto the change's branch
93839
+ change commit <changeId> [<file...>] [-m msg] [--delete <path>]...
93840
+ commit working-tree files onto the change's branch;
93841
+ --delete removes a path in the same commit (repeatable)
93746
93842
  change diff <changeId> per-file diffs of what the change contains
93747
93843
  change split <changeId> <newTitle>
93748
93844
  change checks <changeId> list GitHub check runs for a change
@@ -93777,7 +93873,7 @@ Usage: jjh <command> [args] [--repo owner/name|repoId] [--json]
93777
93873
  slash-commands for ACP clients (Zed's agent panel, ...)
93778
93874
 
93779
93875
  agents-md print an AGENTS.md section teaching coding agents to
93780
- prefer jjh over raw git/gh (jjh agents-md >> AGENTS.md)
93876
+ prefer jjhub over raw git/gh (jjhub agents-md >> AGENTS.md)
93781
93877
 
93782
93878
  sync push overlay state to GitHub
93783
93879
 
@@ -93800,9 +93896,9 @@ Usage: jjh <command> [args] [--repo owner/name|repoId] [--json]
93800
93896
  spawn the same sync as a detached background process
93801
93897
  daemon stop [alias]
93802
93898
  daemon status [alias]
93803
- omit alias to list every repo configured via "jjh daemon start"
93899
+ omit alias to list every repo configured via "jjhub daemon start"
93804
93900
 
93805
- Anything else (pr, issue, ...) is passed through to gh. (jjh's own "auth" command
93901
+ Anything else (pr, issue, ...) is passed through to gh. (jjhub's own "auth" command
93806
93902
  signs into the JJHub server \u2014 it uses your GitHub identity via the server's
93807
93903
  GitHub App, but is separate from "gh auth".)
93808
93904
 
@@ -93860,7 +93956,8 @@ async function runCli(argv, opts = {}) {
93860
93956
  "two-way": { type: "boolean", default: false },
93861
93957
  members: { type: "string" },
93862
93958
  "github-token": { type: "string" },
93863
- force: { type: "boolean", default: false }
93959
+ force: { type: "boolean", default: false },
93960
+ delete: { type: "string", multiple: true }
93864
93961
  }
93865
93962
  });
93866
93963
  positionals = parsed.positionals;
@@ -93937,7 +94034,7 @@ async function dispatch(ctx, pos, values) {
93937
94034
  const requiredBearer = process.env.JJHUB_API_KEY ?? config2.apiKey ?? config2.token;
93938
94035
  if (!requiredBearer) {
93939
94036
  ctx.err(
93940
- "warning: no credential configured (JJHUB_API_KEY / jjh auth login) \u2014 serving MCP over HTTP without authentication; do not expose this port publicly"
94037
+ "warning: no credential configured (JJHUB_API_KEY / jjhub auth login) \u2014 serving MCP over HTTP without authentication; do not expose this port publicly"
93941
94038
  );
93942
94039
  }
93943
94040
  const server = await serveMcpHttp2(ctx.api, port, { requiredBearer, log: ctx.out });
@@ -93992,7 +94089,7 @@ async function repoCmd(ctx, pos, values) {
93992
94089
  const inferred = await ctx.resolveContext();
93993
94090
  if (!inferred) {
93994
94091
  ctx.err(
93995
- "error: could not infer a GitHub repository from the current directory \u2014 run `jjh repo init <owner/name>`"
94092
+ "error: could not infer a GitHub repository from the current directory \u2014 run `jjhub repo init <owner/name>`"
93996
94093
  );
93997
94094
  return 1;
93998
94095
  }
@@ -94034,7 +94131,7 @@ async function repoCmd(ctx, pos, values) {
94034
94131
  }
94035
94132
  if (sub === "members") {
94036
94133
  ctx.err(
94037
- "error: `jjh repo members` was removed \u2014 repo access now derives live from GitHub collaborator permissions (manage collaborators on GitHub itself; see issue #43)"
94134
+ "error: `jjhub repo members` was removed \u2014 repo access now derives live from GitHub collaborator permissions (manage collaborators on GitHub itself; see issue #43)"
94038
94135
  );
94039
94136
  return 1;
94040
94137
  }
@@ -94059,7 +94156,7 @@ async function userCmd(ctx, pos) {
94059
94156
  }
94060
94157
  if (sub === "create" || sub === "token" || sub === "tokens" || sub === "revoke-token") {
94061
94158
  ctx.err(
94062
- `error: \`jjh user ${sub}\` was removed \u2014 identity is GitHub-federated now. Sign in with \`jjh auth login\` (device flow); tokens come from the OAuth flow (issue #43).`
94159
+ `error: \`jjhub user ${sub}\` was removed \u2014 identity is GitHub-federated now. Sign in with \`jjhub auth login\` (device flow); tokens come from the OAuth flow (issue #43).`
94063
94160
  );
94064
94161
  return 1;
94065
94162
  }
@@ -94175,19 +94272,23 @@ async function changeCmd(ctx, pos, values) {
94175
94272
  }
94176
94273
  case "commit": {
94177
94274
  const [changeId2, ...paths] = rest;
94178
- if (!changeId2 || paths.length === 0) {
94179
- return usageError(ctx, "change commit <changeId> <file...> [-m message]");
94275
+ const deletePaths = values.delete;
94276
+ if (!changeId2 || paths.length === 0 && (!deletePaths || deletePaths.length === 0)) {
94277
+ return usageError(ctx, "change commit <changeId> [<file...>] [-m message] [--delete <path>]...");
94180
94278
  }
94181
94279
  const { readFileSync: readFileSync4 } = await import("node:fs");
94182
94280
  const files = paths.map((p) => ({ path: p, content: readFileSync4(p, "utf-8") }));
94183
94281
  const change = await ctx.api.commitFiles(await ctx.repoId(), changeId2, {
94184
94282
  message: values.message,
94185
- files
94283
+ files,
94284
+ deletePaths
94186
94285
  });
94187
94286
  if (ctx.json) return emitJson(ctx, change);
94188
- ctx.out(
94189
- `committed ${files.length} file${files.length === 1 ? "" : "s"} to ${change.changeId} (commit ${change.currentCommitSha})`
94190
- );
94287
+ const parts = [
94288
+ files.length > 0 ? `wrote ${files.length} file${files.length === 1 ? "" : "s"}` : null,
94289
+ deletePaths && deletePaths.length > 0 ? `deleted ${deletePaths.length} file${deletePaths.length === 1 ? "" : "s"}` : null
94290
+ ].filter((p) => p !== null);
94291
+ ctx.out(`${parts.join(", ")} in ${change.changeId} (commit ${change.currentCommitSha})`);
94191
94292
  return 0;
94192
94293
  }
94193
94294
  case "diff": {
@@ -94525,7 +94626,7 @@ async function authCmd(ctx, pos, values) {
94525
94626
  throw e;
94526
94627
  }
94527
94628
  }
94528
- ctx.err("error: sign-in timed out \u2014 run `jjh auth login` again");
94629
+ ctx.err("error: sign-in timed out \u2014 run `jjhub auth login` again");
94529
94630
  return 1;
94530
94631
  }
94531
94632
  if (sub === "sessions") {
@@ -94582,7 +94683,7 @@ async function authCmd(ctx, pos, values) {
94582
94683
  });
94583
94684
  }
94584
94685
  ctx.out(
94585
- `connected to ${ctx.api.baseUrl} (${repos.length} repositor${repos.length === 1 ? "y" : "ies"}, no user identity \u2014 sign in with \`jjh auth login\`)`
94686
+ `connected to ${ctx.api.baseUrl} (${repos.length} repositor${repos.length === 1 ? "y" : "ies"}, no user identity \u2014 sign in with \`jjhub auth login\`)`
94586
94687
  );
94587
94688
  return 0;
94588
94689
  } catch {
@@ -94669,7 +94770,7 @@ async function resolveDaemonTarget(ctx, aliasOrPath, registerIfMissing) {
94669
94770
  const entry = config2.repos?.[aliasOrPath];
94670
94771
  if (!entry) {
94671
94772
  return {
94672
- error: `no configured repo alias "${aliasOrPath}" \u2014 run \`jjh daemon start <path>\` from that repo directory first`
94773
+ error: `no configured repo alias "${aliasOrPath}" \u2014 run \`jjhub daemon start <path>\` from that repo directory first`
94673
94774
  };
94674
94775
  }
94675
94776
  return { repositoryId: entry.repositoryId, localPath: entry.localPath, alias: aliasOrPath };
@@ -94759,7 +94860,7 @@ async function daemonStatusCmd(ctx, rest) {
94759
94860
  ctx.err(`error: no configured repo alias "${aliasOrPath}"`);
94760
94861
  return 1;
94761
94862
  }
94762
- ctx.out("no repos configured \u2014 run `jjh daemon start` from a repo directory first");
94863
+ ctx.out("no repos configured \u2014 run `jjhub daemon start` from a repo directory first");
94763
94864
  return 0;
94764
94865
  }
94765
94866
  const rows = [];
@@ -94842,7 +94943,7 @@ function emitJson(ctx, value) {
94842
94943
  return 0;
94843
94944
  }
94844
94945
  function usageError(ctx, usage) {
94845
- ctx.err(`usage: jjh ${usage}`);
94946
+ ctx.err(`usage: jjhub ${usage}`);
94846
94947
  return 1;
94847
94948
  }
94848
94949
  var invokedDirectly = (() => {
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "jjhub",
3
- "version": "0.1.2",
4
- "description": "jjh — GitHub CLI with Jujutsu-native superpowers: stable change IDs, stacks, platform-wide undo, and conflicts that wait, layered over plain GitHub repos by a JJHub server.",
3
+ "version": "0.1.4",
4
+ "description": "jjhub — GitHub CLI with Jujutsu-native superpowers: stable change IDs, stacks, platform-wide undo, and conflicts that wait, layered over plain GitHub repos by a JJHub server.",
5
5
  "bin": {
6
- "jjhub": "./dist/cli/jjh.mjs",
7
- "jjh": "./dist/cli/jjh.mjs"
6
+ "jjhub": "./dist/cli/jjhub.mjs"
8
7
  },
9
8
  "type": "module",
10
9
  "engines": {
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  name: jjhub-cli
3
- description: Drive a running JJHub instance from the command line with the `jjh` CLI — create and land changes, manage stacks and bookmarks, resolve conflicts, and undo/redo via the operation log. Use when scripting or automating repository operations against a JJHub server instead of the web UI.
3
+ description: Drive a running JJHub instance from the command line with the `jjhub` CLI — create and land changes, manage stacks and bookmarks, resolve conflicts, and undo/redo via the operation log. Use when scripting or automating repository operations against a JJHub server instead of the web UI.
4
4
  ---
5
5
 
6
- # jjh — the JJHub CLI
6
+ # jjhub — the JJHub CLI
7
7
 
8
- `jjh` is a GitHub CLI (`gh`)-like wrapper with Jujutsu-native superpowers. It is a thin
8
+ `jjhub` is a GitHub CLI (`gh`)-like wrapper with Jujutsu-native superpowers. It is a thin
9
9
  client over JJHub's HTTP API: every command reads or mutates state on a running JJHub
10
10
  server, the same server backing the web UI. Repo context is inferred from the current
11
11
  directory's git `origin` remote (override with `--repo`), and any top-level command that
12
- isn't one of `jjh`'s own nouns is passed straight through to the real `gh` binary.
12
+ isn't one of `jjhub`'s own nouns is passed straight through to the real `gh` binary.
13
13
 
14
14
  ## Setup
15
15
 
@@ -22,12 +22,12 @@ tests, requirements). This section covers just the CLI.
22
22
  2. **Point the CLI at it**: `export JJHUB_URL=http://localhost:3000` (this is the default,
23
23
  so only needed if the server is elsewhere). If the server was started with
24
24
  `JJHUB_API_KEY` set, also `export JJHUB_API_KEY=<the same key>` — every request will
25
- otherwise get a `401 unauthorized`. (`jjh auth login` persists these to a config file
25
+ otherwise get a `401 unauthorized`. (`jjhub auth login` persists these to a config file
26
26
  instead of needing them exported every shell — see "Server connection" below.)
27
- 3. **Invoke commands**: `npm run jjh -- <command> [args]` in development, or `jjh <command>`
28
- directly if installed as a binary (`bin/jjh.mjs`).
27
+ 3. **Invoke commands**: `npm run jjhub -- <command> [args]` in development, or `jjhub <command>`
28
+ directly if installed as a binary (`bin/jjhub.mjs`).
29
29
  4. **Initialize the overlay** for a repo before using any other command:
30
- `jjh repo init [owner/name]` (infers `owner/name` from the cwd's git origin if omitted).
30
+ `jjhub repo init [owner/name]` (infers `owner/name` from the cwd's git origin if omitted).
31
31
 
32
32
  The same JJHub server also backs a web UI (`npm run web:dev` from the repo root, in a
33
33
  second terminal — see [`web/README.md`](../../web/README.md)); the CLI and the web UI are
@@ -59,8 +59,9 @@ change amend <changeId>
59
59
  change squash <changeId>
60
60
  change abandon <changeId>
61
61
  change restack <changeId> rebase a needs-restack change onto its rewritten ancestor
62
- change commit <changeId> <file...> [-m msg]
63
- commit working-tree files onto the change's branch
62
+ change commit <changeId> [<file...>] [-m msg] [--delete <path>]...
63
+ commit working-tree files onto the change's branch;
64
+ --delete removes a path in the same commit (repeatable)
64
65
  change diff <changeId> per-file diffs of what the change contains
65
66
  change files <changeId> list locally-stored files (contentMode "local" repos only)
66
67
  change land <changeId> [--force] waits for real CI triggered by its own ready flip,
@@ -104,7 +105,7 @@ mcp serve --http <port> the same tools over Streamable HTTP (POST
104
105
  acp serve Agent Client Protocol agent over stdio (Zed's agent
105
106
  panel, ...) — every operation as a slash command
106
107
  agents-md print an AGENTS.md section teaching coding agents to
107
- prefer jjh over raw git/gh (jjh agents-md >> AGENTS.md)
108
+ prefer jjhub over raw git/gh (jjhub agents-md >> AGENTS.md)
108
109
 
109
110
  daemon <localRepoPath> [--once] [--interval 5] [--engine auto|subprocess|isomorphic] [--two-way]
110
111
  sync a local jj working copy into JJHub (inbound; --two-way mirrors JJHub edits back)
@@ -113,22 +114,22 @@ daemon start [alias|localRepoPath] [--interval 5] [--engine ...] [--two-way]
113
114
  daemon stop [alias] | daemon status [alias]
114
115
  ```
115
116
 
116
- Anything else (`pr`, `issue`, ...) is passed through to `gh` — e.g. `jjh pr view 7`. `jjh auth`
117
- is jjh's own command (it manages the *JJHub server* connection); use `gh auth` for GitHub auth.
117
+ Anything else (`pr`, `issue`, ...) is passed through to `gh` — e.g. `jjhub pr view 7`. `jjhub auth`
118
+ is jjhub's own command (it manages the *JJHub server* connection); use `gh auth` for GitHub auth.
118
119
 
119
120
  ### Server connection: flags, env vars, or a config file
120
121
 
121
122
  Precedence is `--flag` > env var (`JJHUB_URL`/`JJHUB_API_KEY`) > `~/.config/jjhub/config.json`
122
- (written by `jjh auth login`). Run `jjh auth login --url http://localhost:3000 --api-key <key>`
123
+ (written by `jjhub auth login`). Run `jjhub auth login --url http://localhost:3000 --api-key <key>`
123
124
  once and every subsequent command in that shell (any shell, any day) picks it up with no env
124
- vars needed. `jjh auth status` confirms what's currently in effect.
125
+ vars needed. `jjhub auth status` confirms what's currently in effect.
125
126
 
126
127
  ### Identity & permissions (issue #43: GitHub-federated)
127
128
 
128
- A JJHub user IS a GitHub account. `jjh auth login` runs an OAuth device flow against the
129
+ A JJHub user IS a GitHub account. `jjhub auth login` runs an OAuth device flow against the
129
130
  JJHub server's own authorization server: open the printed URL, sign in with GitHub, confirm
130
131
  the code, and the CLI saves a JJHub-issued OAuth token. Every mutation is attributed to that
131
- identity (visible in `jjh op log`).
132
+ identity (visible in `jjhub op log`).
132
133
 
133
134
  What you can do on a repo is asked of GitHub LIVE at request time (cached ~60s):
134
135
  GitHub `admin` → owner, `maintain`/`write` → writer, `triage`/`read` → reader. There is no
@@ -142,26 +143,26 @@ credential: full access, RBAC bypassed, no attribution. Anonymous requests (no s
142
143
 
143
144
  This RBAC is entirely JJHub-internal — a JJHub "owner" isn't checked against real GitHub
144
145
  permissions at all. If the server has `JJHUB_REQUIRE_GITHUB_VERIFICATION=1` set,
145
- `jjh repo init` requires `--github-token <pat>` (or falls back to `$GITHUB_TOKEN`): the
146
+ `jjhub repo init` requires `--github-token <pat>` (or falls back to `$GITHUB_TOKEN`): the
146
147
  server verifies that token has real `admin` access to the exact `owner/name` being created
147
148
  via a live GitHub API call, rejecting creation (403) otherwise. The verified token also
148
149
  becomes the new repo's GitHub connection automatically. See the top-level README's "GitHub
149
150
  admin verification (optional mode)" section.
150
151
 
151
- For exact, always-current syntax straight from the CLI binary, run `jjh help` — this file is
152
+ For exact, always-current syntax straight from the CLI binary, run `jjhub help` — this file is
152
153
  an expanded, example-rich companion to that output, not a replacement for it.
153
154
 
154
- ### Install into your repo (teach YOUR coding agents to prefer jjh)
155
+ ### Install into your repo (teach YOUR coding agents to prefer jjhub)
155
156
 
156
157
  [`agents-snippet.md`](./agents-snippet.md) is a short, imperative AGENTS.md
157
- section — "version control in this repo goes through JJHub; prefer `jjh
158
+ section — "version control in this repo goes through JJHub; prefer `jjhub
158
159
  change/stack/undo` over raw git/gh mutations" — the same pattern as GitHub's
159
160
  `gh skill install github/gh-stack`. Append it to a tracked repo's own
160
161
  AGENTS.md so any AGENTS.md-aware coding agent (Claude Code, Copilot, Cursor,
161
162
  Codex, ...) picks up the workflow automatically:
162
163
 
163
164
  ```bash
164
- jjh agents-md >> AGENTS.md # from the CLI
165
+ jjhub agents-md >> AGENTS.md # from the CLI
165
166
  # or fetch it from a running server:
166
167
  curl https://<your-jjhub>/.well-known/skills/jjhub-cli/agents-snippet.md >> AGENTS.md
167
168
  ```
@@ -171,14 +172,14 @@ curl https://<your-jjhub>/.well-known/skills/jjhub-cli/agents-snippet.md >> AGEN
171
172
  ### Create and land a change
172
173
 
173
174
  ```bash
174
- npm run jjh -- repo init owner/name
175
- npm run jjh -- change new "Fix: add user validation"
176
- npm run jjh -- change list # note the CHG-n id
177
- npm run jjh -- stack create "Fix bundle" --changes CHG-1
178
- npm run jjh -- stack land "Fix bundle"
175
+ npm run jjhub -- repo init owner/name
176
+ npm run jjhub -- change new "Fix: add user validation"
177
+ npm run jjhub -- change list # note the CHG-n id
178
+ npm run jjhub -- stack create "Fix bundle" --changes CHG-1
179
+ npm run jjhub -- stack land "Fix bundle"
179
180
  ```
180
181
 
181
- A single change not part of a stack lands directly: `npm run jjh -- change land CHG-1`.
182
+ A single change not part of a stack lands directly: `npm run jjhub -- change land CHG-1`.
182
183
 
183
184
  ### Sync a local jj working copy into JJHub
184
185
 
@@ -188,22 +189,22 @@ jj bookmark track main --remote=origin # REQUIRED once per clone (issue #28):
188
189
  # without it, already-landed commits look
189
190
  # like new local work and the daemon could
190
191
  # try to re-project them
191
- npm run jjh -- repo init alice/my-project
192
+ npm run jjhub -- repo init alice/my-project
192
193
  jj new -m "Implement feature X" && jj commit
193
- npm run jjh -- daemon . --once # one-shot sync; omit --once to loop every 5s
194
- npm run jjh -- op log # verify the sync landed as a new operation
194
+ npm run jjhub -- daemon . --once # one-shot sync; omit --once to loop every 5s
195
+ npm run jjhub -- op log # verify the sync landed as a new operation
195
196
  ```
196
197
 
197
198
  ### Resolve a conflict
198
199
 
199
200
  ```bash
200
- npm run jjh -- conflict list # note the conflict id
201
- npm run jjh -- conflict resolve CONFLICT-abc123 left # or: right | edited | preserve_unresolved
202
- npm run jjh -- conflict list # confirm it now reads Resolved
201
+ npm run jjhub -- conflict list # note the conflict id
202
+ npm run jjhub -- conflict resolve CONFLICT-abc123 left # or: right | edited | preserve_unresolved
203
+ npm run jjhub -- conflict list # confirm it now reads Resolved
203
204
  ```
204
205
 
205
206
  ## Undo/redo caution
206
207
 
207
- `jjh undo` and `jjh redo` move the repository's operation-log head pointer — they affect
208
+ `jjhub undo` and `jjhub redo` move the repository's operation-log head pointer — they affect
208
209
  *all* state (changes, stacks, bookmarks, conflicts), not just the last command's target.
209
- Run `jjh op log` first if you're not sure what the current head operation is before undoing.
210
+ Run `jjhub op log` first if you're not sure what the current head operation is before undoing.
@@ -1,28 +1,28 @@
1
1
  # Version control in this repo goes through JJHub
2
2
 
3
3
  This repository is tracked by JJHub (a change/stack/operation overlay on
4
- GitHub). Use the `jjh` CLI for version-control mutations instead of raw
5
- `git`/`gh` — every `jjh` action is attributed in an operation log and
6
- reversible with `jjh undo`, and landing is CI-gated.
4
+ GitHub). Use the `jjhub` CLI for version-control mutations instead of raw
5
+ `git`/`gh` — every `jjhub` action is attributed in an operation log and
6
+ reversible with `jjhub undo`, and landing is CI-gated.
7
7
 
8
- - Start work: `jjh change new "<title>"` (not a git branch).
9
- - Describe/retitle: `jjh change describe CHG-N --title t --description d`.
10
- - Amend after edits: `jjh change amend CHG-N` (descendants get flagged and
8
+ - Start work: `jjhub change new "<title>"` (not a git branch).
9
+ - Describe/retitle: `jjhub change describe CHG-N --title t --description d`.
10
+ - Amend after edits: `jjhub change amend CHG-N` (descendants get flagged and
11
11
  restack with one command — no force-push ceremony).
12
- - Stacked work: `jjh stack create "<title>" --changes CHG-1,CHG-2`;
13
- reorder with `jjh stack reorder`; land root-to-tip with
14
- `jjh stack land <stackId>` (all-or-nothing, CI-gated).
15
- - Ship a single change: `jjh change land CHG-N`. Do NOT merge the PR with
12
+ - Stacked work: `jjhub stack create "<title>" --changes CHG-1,CHG-2`;
13
+ reorder with `jjhub stack reorder`; land root-to-tip with
14
+ `jjhub stack land <stackId>` (all-or-nothing, CI-gated).
15
+ - Ship a single change: `jjhub change land CHG-N`. Do NOT merge the PR with
16
16
  `gh pr merge` — that bypasses the gate and the op log.
17
- - Made a mistake: `jjh undo` (works across changes, stacks, bookmarks, and
18
- conflicts; `jjh op log` shows what would be undone).
19
- - Conflicts: `jjh conflict list` / `jjh conflict resolve <id> <state>` —
17
+ - Made a mistake: `jjhub undo` (works across changes, stacks, bookmarks, and
18
+ conflicts; `jjhub op log` shows what would be undone).
19
+ - Conflicts: `jjhub conflict list` / `jjhub conflict resolve <id> <state>` —
20
20
  conflicts are durable objects; you can defer them and keep working.
21
21
  - Machine output: append `--json` to any command.
22
22
  - Repo context is inferred from the git `origin` remote; `--repo owner/name`
23
23
  overrides.
24
24
 
25
- Reads are fine with either tool. Any `jjh` command it doesn't recognize
25
+ Reads are fine with either tool. Any `jjhub` command it doesn't recognize
26
26
  (`pr`, `issue`, ...) passes through to the real `gh` unchanged. Full command
27
- reference: run `jjh help`, or see the served skill at
27
+ reference: run `jjhub help`, or see the served skill at
28
28
  `/.well-known/skills/jjhub-cli/SKILL.md` on your JJHub server.