jjhub 0.1.14 → 0.1.15
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 +3 -3
- package/dist/cli/jjhub.mjs +483 -19
- package/package.json +2 -2
- package/skills/jjhub-cli/SKILL.md +25 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
|
|
5
|
-
repos by a [JJHub](https://jjhub.
|
|
5
|
+
repos by a [JJHub](https://jjhub.erisera.com) server.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -27,8 +27,8 @@ 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.
|
|
31
|
-
or use `jjhub auth login --url https://jjhub.
|
|
30
|
+
`http://localhost:3000`) — e.g. `JJHUB_URL=https://jjhub.erisera.com jjhub repo list`,
|
|
31
|
+
or use `jjhub auth login --url https://jjhub.erisera.com` 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
|
package/dist/cli/jjhub.mjs
CHANGED
|
@@ -198,6 +198,12 @@ var init_api_client = __esm({
|
|
|
198
198
|
input
|
|
199
199
|
);
|
|
200
200
|
}
|
|
201
|
+
listReviewerRoutingAudit(id, changeId2) {
|
|
202
|
+
return this.request(
|
|
203
|
+
"GET",
|
|
204
|
+
`/repositories/${encodeURIComponent(id)}/changes/${encodeURIComponent(changeId2)}/reviewers/audit`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
201
207
|
requestReview(id, changeId2) {
|
|
202
208
|
return this.request(
|
|
203
209
|
"POST",
|
|
@@ -288,6 +294,12 @@ var init_api_client = __esm({
|
|
|
288
294
|
`/repositories/${encodeURIComponent(id)}/changes/${encodeURIComponent(changeId2)}/workflow-evidence`
|
|
289
295
|
);
|
|
290
296
|
}
|
|
297
|
+
rerunWorkflowRun(id, changeId2, runId) {
|
|
298
|
+
return this.request(
|
|
299
|
+
"POST",
|
|
300
|
+
`/repositories/${encodeURIComponent(id)}/changes/${encodeURIComponent(changeId2)}/workflow-runs/${encodeURIComponent(runId)}/rerun`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
291
303
|
getDeploymentEvidence(id, changeId2) {
|
|
292
304
|
return this.request(
|
|
293
305
|
"GET",
|
|
@@ -306,6 +318,12 @@ var init_api_client = __esm({
|
|
|
306
318
|
`/repositories/${encodeURIComponent(id)}/changes/${encodeURIComponent(changeId2)}/issue-context`
|
|
307
319
|
);
|
|
308
320
|
}
|
|
321
|
+
getProjectV2Evidence(id, changeId2) {
|
|
322
|
+
return this.request(
|
|
323
|
+
"GET",
|
|
324
|
+
`/repositories/${encodeURIComponent(id)}/changes/${encodeURIComponent(changeId2)}/project-v2-evidence`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
309
327
|
listChangeFiles(id, changeId2) {
|
|
310
328
|
return this.request(
|
|
311
329
|
"GET",
|
|
@@ -319,6 +337,12 @@ var init_api_client = __esm({
|
|
|
319
337
|
input
|
|
320
338
|
);
|
|
321
339
|
}
|
|
340
|
+
recordContentOpReceipt(id, input) {
|
|
341
|
+
return this.request("POST", `/repositories/${encodeURIComponent(id)}/content-op-receipts`, input);
|
|
342
|
+
}
|
|
343
|
+
listContentOpReceipts(id) {
|
|
344
|
+
return this.request("GET", `/repositories/${encodeURIComponent(id)}/content-op-receipts`);
|
|
345
|
+
}
|
|
322
346
|
listStacks(id) {
|
|
323
347
|
return this.request("GET", `/repositories/${encodeURIComponent(id)}/stacks`);
|
|
324
348
|
}
|
|
@@ -375,6 +399,18 @@ var init_api_client = __esm({
|
|
|
375
399
|
setBookmark(id, input) {
|
|
376
400
|
return this.request("POST", `/repositories/${encodeURIComponent(id)}/bookmarks`, input);
|
|
377
401
|
}
|
|
402
|
+
listSavedViews(id) {
|
|
403
|
+
return this.request("GET", `/repositories/${encodeURIComponent(id)}/saved-views`);
|
|
404
|
+
}
|
|
405
|
+
createSavedView(id, input) {
|
|
406
|
+
return this.request("POST", `/repositories/${encodeURIComponent(id)}/saved-views`, input);
|
|
407
|
+
}
|
|
408
|
+
deleteSavedView(id, viewName) {
|
|
409
|
+
return this.request(
|
|
410
|
+
"DELETE",
|
|
411
|
+
`/repositories/${encodeURIComponent(id)}/saved-views/${encodeURIComponent(viewName)}`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
378
414
|
listConflicts(id) {
|
|
379
415
|
return this.request("GET", `/repositories/${encodeURIComponent(id)}/conflicts`);
|
|
380
416
|
}
|
|
@@ -419,6 +455,12 @@ var init_api_client = __esm({
|
|
|
419
455
|
headOperation(id) {
|
|
420
456
|
return this.request("GET", `/repositories/${encodeURIComponent(id)}/operations/head`);
|
|
421
457
|
}
|
|
458
|
+
getOperationDiff(id, fromOpId, toOpId) {
|
|
459
|
+
return this.request(
|
|
460
|
+
"GET",
|
|
461
|
+
`/repositories/${encodeURIComponent(id)}/operations/diff/${encodeURIComponent(fromOpId)}/${encodeURIComponent(toOpId)}`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
422
464
|
undo(id, input) {
|
|
423
465
|
return this.request("POST", `/repositories/${encodeURIComponent(id)}/undo`, input);
|
|
424
466
|
}
|
|
@@ -60616,6 +60658,15 @@ var init_operations = __esm({
|
|
|
60616
60658
|
description: "Remove explicitly requested GitHub users or teams from an in-review change.",
|
|
60617
60659
|
returns: "ReviewHealth"
|
|
60618
60660
|
}),
|
|
60661
|
+
op({
|
|
60662
|
+
name: "listReviewerRoutingAudit",
|
|
60663
|
+
method: "GET",
|
|
60664
|
+
pathParams: ["id", "changeId"],
|
|
60665
|
+
path: ([id, changeId2]) => `/repositories/${id}/changes/${changeId2}/reviewers/audit`,
|
|
60666
|
+
body: null,
|
|
60667
|
+
description: "List this change's reviewer-routing audit trail: every explicit GitHub reviewer request/remove that actually succeeded, who did it, and when. A failed GitHub API call never creates an entry.",
|
|
60668
|
+
returns: "ReviewerRoutingAuditEntry[]"
|
|
60669
|
+
}),
|
|
60619
60670
|
op({
|
|
60620
60671
|
name: "requestReview",
|
|
60621
60672
|
method: "POST",
|
|
@@ -60722,8 +60773,13 @@ var init_operations = __esm({
|
|
|
60722
60773
|
files: external_exports.array(
|
|
60723
60774
|
external_exports.object({
|
|
60724
60775
|
path: external_exports.string().describe("Repo-relative file path."),
|
|
60725
|
-
content: external_exports.string().describe(
|
|
60726
|
-
|
|
60776
|
+
content: external_exports.string().optional().describe(
|
|
60777
|
+
"Full file content \u2014 UTF-8 text, or base64 if encoding='base64'. Exactly one of content/diff must be set."
|
|
60778
|
+
),
|
|
60779
|
+
diff: external_exports.string().optional().describe(
|
|
60780
|
+
"Unified diff to apply against the file's CURRENT content on this change's branch, as a smaller alternative to sending full content. Exactly one of content/diff must be set. Rejected with a clear error if the diff doesn't apply cleanly (stale base) or the path doesn't exist yet on this branch."
|
|
60781
|
+
),
|
|
60782
|
+
encoding: external_exports.enum(["utf-8", "base64"]).optional().describe("Defaults to 'utf-8'. Only meaningful with content, not diff.")
|
|
60727
60783
|
})
|
|
60728
60784
|
).optional().describe("Files to write. May be omitted/empty for a delete-only commit."),
|
|
60729
60785
|
deletePaths: external_exports.array(external_exports.string()).optional().describe(
|
|
@@ -60760,6 +60816,15 @@ var init_operations = __esm({
|
|
|
60760
60816
|
description: "Show bounded live GitHub Actions workflow-run and job evidence behind a change's projected head, with explicit merge-queue context.",
|
|
60761
60817
|
returns: "WorkflowEvidence"
|
|
60762
60818
|
}),
|
|
60819
|
+
op({
|
|
60820
|
+
name: "rerunWorkflowRun",
|
|
60821
|
+
method: "POST",
|
|
60822
|
+
pathParams: ["id", "changeId", "runId"],
|
|
60823
|
+
path: ([id, changeId2, runId]) => `/repositories/${id}/changes/${changeId2}/workflow-runs/${runId}/rerun`,
|
|
60824
|
+
body: null,
|
|
60825
|
+
description: "Ask GitHub to re-run a workflow run behind a change's projected head. The run id must already appear in that change's bounded workflow evidence \u2014 an unrelated or unknown run id is rejected before any GitHub call.",
|
|
60826
|
+
returns: "WorkflowRerunResult"
|
|
60827
|
+
}),
|
|
60763
60828
|
op({
|
|
60764
60829
|
name: "getDeploymentEvidence",
|
|
60765
60830
|
method: "GET",
|
|
@@ -60787,6 +60852,15 @@ var init_operations = __esm({
|
|
|
60787
60852
|
description: "Show bounded live GitHub issue context referenced by a change's title and description, including closing and blocking intent.",
|
|
60788
60853
|
returns: "ChangeIssueContext"
|
|
60789
60854
|
}),
|
|
60855
|
+
op({
|
|
60856
|
+
name: "getProjectV2Evidence",
|
|
60857
|
+
method: "GET",
|
|
60858
|
+
pathParams: ["id", "changeId"],
|
|
60859
|
+
path: ([id, changeId2]) => `/repositories/${id}/changes/${changeId2}/project-v2-evidence`,
|
|
60860
|
+
body: null,
|
|
60861
|
+
description: "Show bounded, read-only GitHub Projects v2 membership and field values for a change's linked issues \u2014 which project(s) each is on and its status/iteration/other field values there. Never mutates a project item.",
|
|
60862
|
+
returns: "ProjectV2Evidence"
|
|
60863
|
+
}),
|
|
60790
60864
|
op({
|
|
60791
60865
|
name: "listChangeFiles",
|
|
60792
60866
|
method: "GET",
|
|
@@ -60810,6 +60884,34 @@ var init_operations = __esm({
|
|
|
60810
60884
|
description: "Permanently redact a change's title and description everywhere JJHub stores them. Requires repo owner access, an erasure allowlist entry, and a real user session; the legacy API key is rejected. Records audit metadata, never the erased value.",
|
|
60811
60885
|
returns: "{ erasures: Erasure[]; rowsTouched: number }"
|
|
60812
60886
|
}),
|
|
60887
|
+
op({
|
|
60888
|
+
name: "recordContentOpReceipt",
|
|
60889
|
+
method: "POST",
|
|
60890
|
+
pathParams: ["id"],
|
|
60891
|
+
path: ([id]) => `/repositories/${id}/content-op-receipts`,
|
|
60892
|
+
body: external_exports.object({
|
|
60893
|
+
kind: external_exports.enum(["split", "absorb", "squash", "duplicate", "restore"]).describe("Which local jj content-curation operation this receipt covers."),
|
|
60894
|
+
sourceRevision: external_exports.string().min(1).describe("The jj change id the operation was run against."),
|
|
60895
|
+
destinationRevision: external_exports.string().optional().describe("The jj change id passed as --destination, if any."),
|
|
60896
|
+
paths: external_exports.array(external_exports.string()).optional().describe("Path selection from the invocation; omitted = entire revision."),
|
|
60897
|
+
expectedOperationId: external_exports.string().min(1).describe("The --expected-operation id supplied for this apply, from a prior preview."),
|
|
60898
|
+
resultOperationId: external_exports.string().optional().describe("The jj operation id produced by a successful apply; omitted when the apply was rejected."),
|
|
60899
|
+
changedChangeIds: external_exports.array(external_exports.string()).optional().describe("jj change ids the apply actually touched; omitted/empty when the apply was rejected."),
|
|
60900
|
+
succeeded: external_exports.boolean().describe("Whether the local jj mutation actually applied."),
|
|
60901
|
+
errorMessage: external_exports.string().optional().describe("Failure detail (e.g. a stale --expected-operation) when succeeded is false.")
|
|
60902
|
+
}),
|
|
60903
|
+
description: "Record one `jjhub content apply` invocation's outcome for the durable content-op receipt trail (issue #276) \u2014 called by the CLI right after its local jj mutation succeeds or is rejected. The local jj mutation itself already happened by this point; this only records what happened.",
|
|
60904
|
+
returns: "ContentOpReceipt"
|
|
60905
|
+
}),
|
|
60906
|
+
op({
|
|
60907
|
+
name: "listContentOpReceipts",
|
|
60908
|
+
method: "GET",
|
|
60909
|
+
pathParams: ["id"],
|
|
60910
|
+
path: ([id]) => `/repositories/${id}/content-op-receipts`,
|
|
60911
|
+
body: null,
|
|
60912
|
+
description: "List this repository's content-op receipt trail: every `jjhub content apply` invocation, successful or rejected.",
|
|
60913
|
+
returns: "ContentOpReceipt[]"
|
|
60914
|
+
}),
|
|
60813
60915
|
// --- stacks ---
|
|
60814
60916
|
op({
|
|
60815
60917
|
name: "listStacks",
|
|
@@ -60923,6 +61025,37 @@ var init_operations = __esm({
|
|
|
60923
61025
|
returns: "Bookmark"
|
|
60924
61026
|
}),
|
|
60925
61027
|
// deleteBookmark: excluded — see the module doc comment ("force" is a query param).
|
|
61028
|
+
// --- saved views (issue #274) ---
|
|
61029
|
+
op({
|
|
61030
|
+
name: "listSavedViews",
|
|
61031
|
+
method: "GET",
|
|
61032
|
+
pathParams: ["id"],
|
|
61033
|
+
path: ([id]) => `/repositories/${id}/saved-views`,
|
|
61034
|
+
body: null,
|
|
61035
|
+
description: "List saved revset views: named, reusable jj revset expressions stored per repository. Storage only \u2014 evaluate one locally with `jjhub revset <localRepoPath> <expression>`.",
|
|
61036
|
+
returns: "SavedView[]"
|
|
61037
|
+
}),
|
|
61038
|
+
op({
|
|
61039
|
+
name: "createSavedView",
|
|
61040
|
+
method: "POST",
|
|
61041
|
+
pathParams: ["id"],
|
|
61042
|
+
path: ([id]) => `/repositories/${id}/saved-views`,
|
|
61043
|
+
body: external_exports.object({
|
|
61044
|
+
name: external_exports.string().min(1).describe("Saved view name, e.g. 'my-open-changes'."),
|
|
61045
|
+
revsetExpression: external_exports.string().min(1).describe("The jj revset expression this view saves, e.g. 'mine() & open()'.")
|
|
61046
|
+
}),
|
|
61047
|
+
description: "Save a named jj revset expression for later reuse. Storage only \u2014 JJHub has no local jj workspace to evaluate it against; run `jjhub revset <localRepoPath> <expression>` to actually evaluate the expression.",
|
|
61048
|
+
returns: "SavedView"
|
|
61049
|
+
}),
|
|
61050
|
+
op({
|
|
61051
|
+
name: "deleteSavedView",
|
|
61052
|
+
method: "DELETE",
|
|
61053
|
+
pathParams: ["id", "viewName"],
|
|
61054
|
+
path: ([id, viewName]) => `/repositories/${id}/saved-views/${viewName}`,
|
|
61055
|
+
body: null,
|
|
61056
|
+
description: "Delete a saved revset view by name. The dual of createSavedView.",
|
|
61057
|
+
returns: "void"
|
|
61058
|
+
}),
|
|
60926
61059
|
// --- conflicts ---
|
|
60927
61060
|
op({
|
|
60928
61061
|
name: "listConflicts",
|
|
@@ -61006,6 +61139,15 @@ var init_operations = __esm({
|
|
|
61006
61139
|
returns: "Operation | null",
|
|
61007
61140
|
returnsCli: "OperationSummary | null"
|
|
61008
61141
|
}),
|
|
61142
|
+
op({
|
|
61143
|
+
name: "getOperationDiff",
|
|
61144
|
+
method: "GET",
|
|
61145
|
+
pathParams: ["id", "fromOpId", "toOpId"],
|
|
61146
|
+
path: ([id, fromOpId, toOpId]) => `/repositories/${id}/operations/diff/${fromOpId}/${toOpId}`,
|
|
61147
|
+
body: null,
|
|
61148
|
+
description: "Structural diff between two operations' immutable snapshots: which changes/stacks/bookmarks/conflicts were created, modified, or removed going from fromOpId to toOpId. Works for adjacent or far-apart operations; never replays the operations in between.",
|
|
61149
|
+
returns: "OperationDiff"
|
|
61150
|
+
}),
|
|
61009
61151
|
op({
|
|
61010
61152
|
name: "undo",
|
|
61011
61153
|
method: "POST",
|
|
@@ -61152,7 +61294,7 @@ var init_package = __esm({
|
|
|
61152
61294
|
"package.json"() {
|
|
61153
61295
|
package_default = {
|
|
61154
61296
|
name: "jjhub",
|
|
61155
|
-
version: "0.1.
|
|
61297
|
+
version: "0.1.15",
|
|
61156
61298
|
private: true,
|
|
61157
61299
|
description: "JJHub \u2014 a Jujutsu-native overlay atop GitHub: stable change IDs, stacked PRs, platform-wide undo, and an MCP server for coding agents.",
|
|
61158
61300
|
type: "module",
|
|
@@ -61185,6 +61327,7 @@ var init_package = __esm({
|
|
|
61185
61327
|
"ai.matey.backend": "^0.8.2",
|
|
61186
61328
|
"ai.matey.core": "^0.3.4",
|
|
61187
61329
|
"ai.matey.types": "^0.5.1",
|
|
61330
|
+
diff: "^7.0.0",
|
|
61188
61331
|
fastify: "^5.2.0",
|
|
61189
61332
|
"isomorphic-git": "^1.40.0",
|
|
61190
61333
|
"isomorphic-jj": "^1.7.0",
|
|
@@ -61195,6 +61338,7 @@ var init_package = __esm({
|
|
|
61195
61338
|
},
|
|
61196
61339
|
devDependencies: {
|
|
61197
61340
|
"@biomejs/biome": "^2.5.6",
|
|
61341
|
+
"@types/diff": "^7.0.2",
|
|
61198
61342
|
"@types/node": "^26.1.2",
|
|
61199
61343
|
"@types/nodemailer": "^8.0.1",
|
|
61200
61344
|
"@types/web-push": "^3.6.4",
|
|
@@ -61208,7 +61352,7 @@ var init_package = __esm({
|
|
|
61208
61352
|
},
|
|
61209
61353
|
repository: {
|
|
61210
61354
|
type: "git",
|
|
61211
|
-
url: "git+https://github.com/
|
|
61355
|
+
url: "git+https://github.com/erisera-code/jjhub.git"
|
|
61212
61356
|
},
|
|
61213
61357
|
keywords: [
|
|
61214
61358
|
"jujutsu",
|
|
@@ -61810,7 +61954,11 @@ var init_server3 = __esm({
|
|
|
61810
61954
|
stackId,
|
|
61811
61955
|
conflictId: external_exports.string().describe("Conflict id, e.g. from list_conflicts."),
|
|
61812
61956
|
name: external_exports.string().describe("Bookmark name."),
|
|
61813
|
-
|
|
61957
|
+
viewName: external_exports.string().describe("Saved view name, e.g. from list_saved_views."),
|
|
61958
|
+
confirmationId: external_exports.string().describe("Pending confirmation id, e.g. from list_pending_confirmations."),
|
|
61959
|
+
fromOpId: external_exports.string().describe("Earlier operation id to diff from, e.g. 'OP-0050' \u2014 see list_operations."),
|
|
61960
|
+
toOpId: external_exports.string().describe("Later operation id to diff to, e.g. 'OP-0093' \u2014 see list_operations."),
|
|
61961
|
+
runId: external_exports.string().describe("GitHub Actions workflow run id, e.g. from get_workflow_evidence.")
|
|
61814
61962
|
};
|
|
61815
61963
|
HANDWRITTEN_TOOL_NAMES = [
|
|
61816
61964
|
"whoami",
|
|
@@ -119258,6 +119406,51 @@ function sshKeyFingerprint(authorizedKeysLine) {
|
|
|
119258
119406
|
return `SHA256:${digest.replace(/=+$/, "")}`;
|
|
119259
119407
|
}
|
|
119260
119408
|
|
|
119409
|
+
// src/jj/route-selection.ts
|
|
119410
|
+
var RemoteRouteError = class extends Error {
|
|
119411
|
+
candidates;
|
|
119412
|
+
constructor(message, candidates) {
|
|
119413
|
+
super(message);
|
|
119414
|
+
this.name = "RemoteRouteError";
|
|
119415
|
+
this.candidates = candidates;
|
|
119416
|
+
}
|
|
119417
|
+
};
|
|
119418
|
+
function selectPushRoute(topology, overrideRemoteName) {
|
|
119419
|
+
const candidateNames = topology.remotes.map((remote) => remote.name);
|
|
119420
|
+
if (overrideRemoteName) {
|
|
119421
|
+
const match = topology.remotes.find((remote) => remote.name === overrideRemoteName);
|
|
119422
|
+
if (!match) {
|
|
119423
|
+
throw new RemoteRouteError(
|
|
119424
|
+
candidateNames.length > 0 ? `--remote "${overrideRemoteName}" is not among this workspace's remotes (${candidateNames.join(", ")})` : `--remote "${overrideRemoteName}" was given, but this workspace has no Git remotes at all`,
|
|
119425
|
+
candidateNames
|
|
119426
|
+
);
|
|
119427
|
+
}
|
|
119428
|
+
return { remote: match, reason: "override" };
|
|
119429
|
+
}
|
|
119430
|
+
if (topology.remotes.length === 0) {
|
|
119431
|
+
throw new RemoteRouteError(
|
|
119432
|
+
"no Git remotes were observed in this workspace; configure one or pass --remote",
|
|
119433
|
+
[]
|
|
119434
|
+
);
|
|
119435
|
+
}
|
|
119436
|
+
if (topology.remotes.length === 1) {
|
|
119437
|
+
return { remote: topology.remotes[0], reason: "single" };
|
|
119438
|
+
}
|
|
119439
|
+
if (topology.trunkChangeId) {
|
|
119440
|
+
const trunkTrackingBookmark = topology.bookmarks.find(
|
|
119441
|
+
(bookmark) => bookmark.remote !== null && bookmark.tracked && bookmark.targetChangeId === topology.trunkChangeId
|
|
119442
|
+
);
|
|
119443
|
+
const trunkRemote = trunkTrackingBookmark && topology.remotes.find((remote) => remote.name === trunkTrackingBookmark.remote);
|
|
119444
|
+
if (trunkRemote) return { remote: trunkRemote, reason: "trunk-tracking" };
|
|
119445
|
+
}
|
|
119446
|
+
const origin = topology.remotes.find((remote) => remote.name === "origin");
|
|
119447
|
+
if (origin) return { remote: origin, reason: "origin-default" };
|
|
119448
|
+
throw new RemoteRouteError(
|
|
119449
|
+
`multiple remotes are configured (${candidateNames.join(", ")}) and none is unambiguous (no remote tracks trunk(), and none is named "origin"); pass --remote <name> to choose one`,
|
|
119450
|
+
candidateNames
|
|
119451
|
+
);
|
|
119452
|
+
}
|
|
119453
|
+
|
|
119261
119454
|
// src/service/confirmation-signature.ts
|
|
119262
119455
|
function confirmationSignatureMessage(payload) {
|
|
119263
119456
|
return Buffer.from(
|
|
@@ -119369,7 +119562,7 @@ async function runDaemonOnce(jj, api, repoId, opts = {}) {
|
|
|
119369
119562
|
};
|
|
119370
119563
|
const log = opts.log ?? (() => {
|
|
119371
119564
|
});
|
|
119372
|
-
await reportWorkspaceTopology(jj, api, repoId, opts, log);
|
|
119565
|
+
const topologyObservation = await reportWorkspaceTopology(jj, api, repoId, opts, log);
|
|
119373
119566
|
let entries = await jj.snapshotLog();
|
|
119374
119567
|
const serverChanges = await api.listChanges(repoId);
|
|
119375
119568
|
let defaultBookmark = "main";
|
|
@@ -119395,6 +119588,17 @@ async function runDaemonOnce(jj, api, repoId, opts = {}) {
|
|
|
119395
119588
|
log("[daemon] commit push skipped: no local repo path");
|
|
119396
119589
|
return pusher;
|
|
119397
119590
|
}
|
|
119591
|
+
if (topologyObservation) {
|
|
119592
|
+
try {
|
|
119593
|
+
selectPushRoute(topologyObservation.topology, opts.remote);
|
|
119594
|
+
} catch (err2) {
|
|
119595
|
+
if (err2 instanceof RemoteRouteError) {
|
|
119596
|
+
log(`[daemon] commit push skipped: ${err2.message}`);
|
|
119597
|
+
return pusher;
|
|
119598
|
+
}
|
|
119599
|
+
throw err2;
|
|
119600
|
+
}
|
|
119601
|
+
}
|
|
119398
119602
|
if (!GITHUB_FULLNAME_RE.test(repo.fullName)) {
|
|
119399
119603
|
log(
|
|
119400
119604
|
`[daemon] commit push skipped: repo fullName "${repo.fullName}" does not look like a GitHub repo`
|
|
@@ -119701,9 +119905,17 @@ async function signPendingConfirmationsGpg(api, repoId, signer, githubKeyFetcher
|
|
|
119701
119905
|
}
|
|
119702
119906
|
}
|
|
119703
119907
|
async function reportWorkspaceTopology(jj, api, repoId, opts, log) {
|
|
119704
|
-
if (!jj.observeWorkspaceTopology) return;
|
|
119908
|
+
if (!jj.observeWorkspaceTopology) return null;
|
|
119909
|
+
let observation;
|
|
119910
|
+
try {
|
|
119911
|
+
observation = await jj.observeWorkspaceTopology();
|
|
119912
|
+
} catch (err2) {
|
|
119913
|
+
log(
|
|
119914
|
+
`[daemon] workspace topology observation failed (continuing): ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
119915
|
+
);
|
|
119916
|
+
return null;
|
|
119917
|
+
}
|
|
119705
119918
|
try {
|
|
119706
|
-
const observation = await jj.observeWorkspaceTopology();
|
|
119707
119919
|
const capabilities = ["snapshot", "topology"];
|
|
119708
119920
|
if (jj.queryRevset) capabilities.push("revset");
|
|
119709
119921
|
if (jj.previewContentOperation && jj.executeContentOperation) capabilities.push("content-operations");
|
|
@@ -119727,6 +119939,7 @@ async function reportWorkspaceTopology(jj, api, repoId, opts, log) {
|
|
|
119727
119939
|
);
|
|
119728
119940
|
}
|
|
119729
119941
|
}
|
|
119942
|
+
return observation;
|
|
119730
119943
|
}
|
|
119731
119944
|
async function sendHeartbeat(api, repoId, opts, ok, message, log) {
|
|
119732
119945
|
if (opts.reportHeartbeat === false) return;
|
|
@@ -119759,6 +119972,7 @@ async function runDaemonLoop(jj, api, repoId, opts = {}) {
|
|
|
119759
119972
|
pushCommit: opts.pushCommit,
|
|
119760
119973
|
repoPath: opts.repoPath,
|
|
119761
119974
|
twoWay: opts.twoWay,
|
|
119975
|
+
remote: opts.remote,
|
|
119762
119976
|
knownMapping,
|
|
119763
119977
|
log
|
|
119764
119978
|
});
|
|
@@ -119888,7 +120102,8 @@ function spawnDaemon(opts) {
|
|
|
119888
120102
|
String(opts.intervalSeconds ?? 5),
|
|
119889
120103
|
...opts.engine ? ["--engine", opts.engine] : [],
|
|
119890
120104
|
...opts.baseUrl ? ["--url", opts.baseUrl] : [],
|
|
119891
|
-
...opts.twoWay ? ["--two-way"] : []
|
|
120105
|
+
...opts.twoWay ? ["--two-way"] : [],
|
|
120106
|
+
...opts.remote ? ["--remote", opts.remote] : []
|
|
119892
120107
|
];
|
|
119893
120108
|
const spawnFn = opts.spawnFn ?? defaultSpawnFn;
|
|
119894
120109
|
const child = spawnFn(process.execPath, args, { detached: true, stdio: "ignore", env: process.env });
|
|
@@ -119927,6 +120142,8 @@ var JJH_NOUNS = /* @__PURE__ */ new Set([
|
|
|
119927
120142
|
"change",
|
|
119928
120143
|
"stack",
|
|
119929
120144
|
"bookmark",
|
|
120145
|
+
"workspace",
|
|
120146
|
+
"view",
|
|
119930
120147
|
"op",
|
|
119931
120148
|
"undo",
|
|
119932
120149
|
"redo",
|
|
@@ -119990,6 +120207,8 @@ Usage: jjhub <command> [args] [--repo owner/name|repoId] [--json]
|
|
|
119990
120207
|
change squash <changeId>
|
|
119991
120208
|
change abandon <changeId>
|
|
119992
120209
|
change restack <changeId> rebase a needs-restack change onto its rewritten ancestor
|
|
120210
|
+
change request-review <changeId> move a draft change into review and mark its PR ready for review
|
|
120211
|
+
change return-to-draft <changeId> return an in-review change and its PR to draft
|
|
119993
120212
|
change land <changeId> [--force] --force bypasses a failing/pending check-run gate
|
|
119994
120213
|
change commit <changeId> [<file...>] [-m msg] [--delete <path>]...
|
|
119995
120214
|
commit working-tree files onto the change's branch;
|
|
@@ -119998,10 +120217,17 @@ Usage: jjhub <command> [args] [--repo owner/name|repoId] [--json]
|
|
|
119998
120217
|
change split <changeId> <newTitle>
|
|
119999
120218
|
change checks <changeId> list GitHub check runs for a change
|
|
120000
120219
|
change actions <changeId> show bounded GitHub Actions workflow and job evidence
|
|
120220
|
+
change actions rerun <changeId> <runId>
|
|
120221
|
+
ask GitHub to re-run a workflow run shown by
|
|
120222
|
+
"change actions <changeId>" (issue #279); a run id not
|
|
120223
|
+
associated with the change is rejected before any GitHub call
|
|
120001
120224
|
change deployments <changeId> show bounded GitHub deployment evidence
|
|
120002
120225
|
change security <changeId> show bounded GitHub code-scanning evidence
|
|
120003
120226
|
change issues <changeId> show linked GitHub issue context for a change
|
|
120227
|
+
change projects <changeId> show bounded GitHub Projects v2 evidence for linked issues
|
|
120004
120228
|
change files <changeId> list locally-stored files (contentMode "local" repos only)
|
|
120229
|
+
change reviewers <changeId> reviewer-routing audit trail: every request/remove that
|
|
120230
|
+
actually succeeded against GitHub, who did it, and when
|
|
120005
120231
|
|
|
120006
120232
|
stack create <title> [--id x] [--changes CHG-1,CHG-2]
|
|
120007
120233
|
stack list
|
|
@@ -120015,12 +120241,24 @@ Usage: jjhub <command> [args] [--repo owner/name|repoId] [--json]
|
|
|
120015
120241
|
bookmark set <name> <tracked|view> [--target CHG-x]
|
|
120016
120242
|
bookmark delete <name> [--force] --force required for the default (trunk) bookmark
|
|
120017
120243
|
|
|
120244
|
+
workspace list live local jj workspace agents reporting into this
|
|
120245
|
+
repo (see "jjhub daemon"), with their last-seen
|
|
120246
|
+
topology observation
|
|
120247
|
+
|
|
120248
|
+
view save <name> <revsetExpression> save a named jj revset expression for later reuse (storage
|
|
120249
|
+
only \u2014 evaluate it with "jjhub revset")
|
|
120250
|
+
view list list saved views in this repo
|
|
120251
|
+
view delete <name>
|
|
120252
|
+
|
|
120018
120253
|
op log
|
|
120254
|
+
op diff <fromOpId> <toOpId> structural diff between two operations' immutable snapshots
|
|
120255
|
+
(which changes/stacks/bookmarks/conflicts were created,
|
|
120256
|
+
modified, or removed); works for adjacent or far-apart ops
|
|
120019
120257
|
undo [--force] --force allows undoing a "land" op (can't unmerge the real PR)
|
|
120020
120258
|
redo
|
|
120021
120259
|
|
|
120022
120260
|
mcp serve MCP server over stdio for agent clients (Claude Code, Cursor, ...)
|
|
120023
|
-
mcp serve --http <port> same
|
|
120261
|
+
mcp serve --http <port> same 86 tools over Streamable HTTP (POST /); gated by the
|
|
120024
120262
|
configured credential as a bearer token when one is set.
|
|
120025
120263
|
For public exposure prefer the API server's own /mcp
|
|
120026
120264
|
endpoint, which speaks real OAuth (issue #48)
|
|
@@ -120048,11 +120286,16 @@ Usage: jjhub <command> [args] [--repo owner/name|repoId] [--json]
|
|
|
120048
120286
|
auth logout forget the saved connection/token
|
|
120049
120287
|
auth status who the saved credential authenticates as
|
|
120050
120288
|
|
|
120051
|
-
daemon <localRepoPath> [--once] [--interval 5] [--engine auto|subprocess|isomorphic] [--two-way]
|
|
120289
|
+
daemon <localRepoPath> [--once] [--interval 5] [--engine auto|subprocess|isomorphic] [--two-way] [--remote name]
|
|
120052
120290
|
sync a local jj working copy into JJHub (inbound direction), foreground
|
|
120053
120291
|
--two-way also reflects JJHub-side edits back into the local jj repo
|
|
120054
120292
|
(opt-in, subprocess engine only \u2014 see docs/ROADMAP-JJH-PARITY.md Phase 3)
|
|
120055
|
-
|
|
120293
|
+
--remote picks which local jj remote real-commit push targets when the
|
|
120294
|
+
workspace has more than one configured (subprocess engine only); without
|
|
120295
|
+
it, one remote is used as-is, a trunk-tracking remote or "origin" is
|
|
120296
|
+
preferred among several, and a still-ambiguous set of remotes skips the
|
|
120297
|
+
push with a clear error rather than guessing
|
|
120298
|
+
daemon start [alias|localRepoPath] [--interval 5] [--engine ...] [--two-way] [--remote name]
|
|
120056
120299
|
spawn the same sync as a detached background process
|
|
120057
120300
|
daemon stop [alias]
|
|
120058
120301
|
daemon status [alias]
|
|
@@ -120071,6 +120314,10 @@ Usage: jjhub <command> [args] [--repo owner/name|repoId] [--json]
|
|
|
120071
120314
|
content apply <localRepoPath> <split|absorb|squash|duplicate|restore>
|
|
120072
120315
|
same selection plus --expected-operation <jj-operation-id> from preview
|
|
120073
120316
|
local jj content curation with immutable-boundary enforcement; file selections only
|
|
120317
|
+
records a durable receipt on the JJHub server (best-effort) \u2014 see content receipts
|
|
120318
|
+
content receipts [--repo owner/name|repoId]
|
|
120319
|
+
list this repo's content-op receipt trail: every content apply invocation,
|
|
120320
|
+
successful or rejected
|
|
120074
120321
|
|
|
120075
120322
|
Anything else (pr, issue, ...) is passed through to gh. (jjhub's own "auth" command
|
|
120076
120323
|
signs into the JJHub server \u2014 it uses your GitHub identity via the server's
|
|
@@ -120132,6 +120379,7 @@ async function runCli(argv, opts = {}) {
|
|
|
120132
120379
|
repos: { type: "string" },
|
|
120133
120380
|
message: { type: "string", short: "m" },
|
|
120134
120381
|
"two-way": { type: "boolean", default: false },
|
|
120382
|
+
remote: { type: "string" },
|
|
120135
120383
|
members: { type: "string" },
|
|
120136
120384
|
"github-token": { type: "string" },
|
|
120137
120385
|
force: { type: "boolean", default: false },
|
|
@@ -120190,6 +120438,10 @@ async function dispatch(ctx, pos, values) {
|
|
|
120190
120438
|
return undoRedoCmd(ctx, group, values);
|
|
120191
120439
|
case "bookmark":
|
|
120192
120440
|
return bookmarkCmd(ctx, pos.slice(1), values);
|
|
120441
|
+
case "workspace":
|
|
120442
|
+
return workspaceCmd(ctx, pos.slice(1));
|
|
120443
|
+
case "view":
|
|
120444
|
+
return viewCmd(ctx, pos.slice(1));
|
|
120193
120445
|
case "conflict":
|
|
120194
120446
|
return conflictCmd(ctx, pos.slice(1));
|
|
120195
120447
|
case "sync":
|
|
@@ -120351,7 +120603,7 @@ async function changeCmd(ctx, pos, values) {
|
|
|
120351
120603
|
if (!sub) {
|
|
120352
120604
|
return usageError(
|
|
120353
120605
|
ctx,
|
|
120354
|
-
"change <new|list|show|describe|amend|squash|abandon|restack|land|commit|diff|split|checks|files> ..."
|
|
120606
|
+
"change <new|list|show|describe|amend|squash|abandon|restack|request-review|return-to-draft|land|commit|diff|split|checks|files|reviewers> ..."
|
|
120355
120607
|
);
|
|
120356
120608
|
}
|
|
120357
120609
|
switch (sub) {
|
|
@@ -120434,6 +120686,22 @@ async function changeCmd(ctx, pos, values) {
|
|
|
120434
120686
|
ctx.out(`restacked ${change.changeId} (commit ${change.currentCommitSha})`);
|
|
120435
120687
|
return 0;
|
|
120436
120688
|
}
|
|
120689
|
+
case "request-review": {
|
|
120690
|
+
const changeId2 = rest[0];
|
|
120691
|
+
if (!changeId2) return usageError(ctx, "change request-review <changeId>");
|
|
120692
|
+
const change = await ctx.api.requestReview(await ctx.repoId(), changeId2);
|
|
120693
|
+
if (ctx.json) return emitJson(ctx, change);
|
|
120694
|
+
ctx.out(`${change.changeId} now in review`);
|
|
120695
|
+
return 0;
|
|
120696
|
+
}
|
|
120697
|
+
case "return-to-draft": {
|
|
120698
|
+
const changeId2 = rest[0];
|
|
120699
|
+
if (!changeId2) return usageError(ctx, "change return-to-draft <changeId>");
|
|
120700
|
+
const change = await ctx.api.returnToDraft(await ctx.repoId(), changeId2);
|
|
120701
|
+
if (ctx.json) return emitJson(ctx, change);
|
|
120702
|
+
ctx.out(`${change.changeId} returned to draft`);
|
|
120703
|
+
return 0;
|
|
120704
|
+
}
|
|
120437
120705
|
case "land": {
|
|
120438
120706
|
const changeId2 = rest[0];
|
|
120439
120707
|
if (!changeId2) return usageError(ctx, "change land <changeId> [--force]");
|
|
@@ -120515,6 +120783,14 @@ async function changeCmd(ctx, pos, values) {
|
|
|
120515
120783
|
return 0;
|
|
120516
120784
|
}
|
|
120517
120785
|
case "actions": {
|
|
120786
|
+
if (rest[0] === "rerun") {
|
|
120787
|
+
const [, changeId3, runId] = rest;
|
|
120788
|
+
if (!changeId3 || !runId) return usageError(ctx, "change actions rerun <changeId> <runId>");
|
|
120789
|
+
const result = await ctx.api.rerunWorkflowRun(await ctx.repoId(), changeId3, runId);
|
|
120790
|
+
if (ctx.json) return emitJson(ctx, result);
|
|
120791
|
+
ctx.out(`requested rerun of workflow run ${result.runId} (${result.runName}) for ${changeId3}`);
|
|
120792
|
+
return 0;
|
|
120793
|
+
}
|
|
120518
120794
|
const changeId2 = rest[0];
|
|
120519
120795
|
if (!changeId2) return usageError(ctx, "change actions <changeId>");
|
|
120520
120796
|
const evidence = await ctx.api.getWorkflowEvidence(await ctx.repoId(), changeId2);
|
|
@@ -120606,6 +120882,24 @@ async function changeCmd(ctx, pos, values) {
|
|
|
120606
120882
|
);
|
|
120607
120883
|
return 0;
|
|
120608
120884
|
}
|
|
120885
|
+
case "projects": {
|
|
120886
|
+
const changeId2 = rest[0];
|
|
120887
|
+
if (!changeId2) return usageError(ctx, "change projects <changeId>");
|
|
120888
|
+
const evidence = await ctx.api.getProjectV2Evidence(await ctx.repoId(), changeId2);
|
|
120889
|
+
if (ctx.json) return emitJson(ctx, evidence);
|
|
120890
|
+
ctx.out(evidence.detail);
|
|
120891
|
+
const rows = evidence.issues.flatMap(
|
|
120892
|
+
(issue2) => issue2.items.length > 0 ? issue2.items.map((item) => [
|
|
120893
|
+
`${issue2.repositoryFullName}#${issue2.number}`,
|
|
120894
|
+
`${item.projectTitle} (#${item.projectNumber})`,
|
|
120895
|
+
item.fieldValues.map((f) => `${f.fieldName}: ${f.value}`).join(", ") || "-",
|
|
120896
|
+
item.isArchived ? "yes" : "no"
|
|
120897
|
+
]) : []
|
|
120898
|
+
);
|
|
120899
|
+
if (rows.length === 0) return 0;
|
|
120900
|
+
ctx.out(table([["ISSUE", "PROJECT", "FIELDS", "ARCHIVED"], ...rows]));
|
|
120901
|
+
return 0;
|
|
120902
|
+
}
|
|
120609
120903
|
case "files": {
|
|
120610
120904
|
const changeId2 = rest[0];
|
|
120611
120905
|
if (!changeId2) return usageError(ctx, "change files <changeId>");
|
|
@@ -120623,6 +120917,29 @@ async function changeCmd(ctx, pos, values) {
|
|
|
120623
120917
|
);
|
|
120624
120918
|
return 0;
|
|
120625
120919
|
}
|
|
120920
|
+
case "reviewers": {
|
|
120921
|
+
const changeId2 = rest[0];
|
|
120922
|
+
if (!changeId2) return usageError(ctx, "change reviewers <changeId>");
|
|
120923
|
+
const entries = await ctx.api.listReviewerRoutingAudit(await ctx.repoId(), changeId2);
|
|
120924
|
+
if (ctx.json) return emitJson(ctx, entries);
|
|
120925
|
+
if (entries.length === 0) {
|
|
120926
|
+
ctx.out("no reviewer-routing audit entries");
|
|
120927
|
+
return 0;
|
|
120928
|
+
}
|
|
120929
|
+
ctx.out(
|
|
120930
|
+
table([
|
|
120931
|
+
["ACTION", "REVIEWERS", "TEAMS", "BY", "AT"],
|
|
120932
|
+
...entries.map((e) => [
|
|
120933
|
+
e.action,
|
|
120934
|
+
e.reviewers.join(",") || "-",
|
|
120935
|
+
e.teamReviewers.join(",") || "-",
|
|
120936
|
+
e.performedBy ?? "-",
|
|
120937
|
+
e.performedAt
|
|
120938
|
+
])
|
|
120939
|
+
])
|
|
120940
|
+
);
|
|
120941
|
+
return 0;
|
|
120942
|
+
}
|
|
120626
120943
|
default:
|
|
120627
120944
|
return usageError(ctx, `unknown change subcommand "${sub}"`);
|
|
120628
120945
|
}
|
|
@@ -120793,9 +121110,77 @@ async function bookmarkCmd(ctx, pos, values) {
|
|
|
120793
121110
|
}
|
|
120794
121111
|
return usageError(ctx, `unknown bookmark subcommand "${sub}"`);
|
|
120795
121112
|
}
|
|
120796
|
-
async function
|
|
121113
|
+
async function workspaceCmd(ctx, pos) {
|
|
120797
121114
|
const [sub] = pos;
|
|
120798
|
-
if (sub
|
|
121115
|
+
if (sub === "list") {
|
|
121116
|
+
const agents = await ctx.api.listWorkspaceAgents(await ctx.repoId());
|
|
121117
|
+
if (ctx.json) return emitJson(ctx, agents);
|
|
121118
|
+
if (agents.length === 0) {
|
|
121119
|
+
ctx.out("no workspace agents (they appear once a local `jjhub daemon` reports in)");
|
|
121120
|
+
return 0;
|
|
121121
|
+
}
|
|
121122
|
+
ctx.out(
|
|
121123
|
+
table([
|
|
121124
|
+
["WORKSPACE", "LOCAL REPO", "PATH", "WORKING COPY", "JJ VERSION", "OK", "LAST SEEN"],
|
|
121125
|
+
...agents.map(workspaceAgentRow)
|
|
121126
|
+
])
|
|
121127
|
+
);
|
|
121128
|
+
return 0;
|
|
121129
|
+
}
|
|
121130
|
+
return usageError(ctx, "workspace list");
|
|
121131
|
+
}
|
|
121132
|
+
function workspaceAgentRow(a) {
|
|
121133
|
+
return [
|
|
121134
|
+
a.workspaceName,
|
|
121135
|
+
a.localRepositoryId,
|
|
121136
|
+
a.localPath ?? "-",
|
|
121137
|
+
a.workingCopyChangeId ?? "-",
|
|
121138
|
+
a.jjVersion ?? "-",
|
|
121139
|
+
a.ok ? "yes" : "no",
|
|
121140
|
+
a.lastSeenAt
|
|
121141
|
+
];
|
|
121142
|
+
}
|
|
121143
|
+
async function viewCmd(ctx, pos) {
|
|
121144
|
+
const [sub, ...rest] = pos;
|
|
121145
|
+
if (!sub) return usageError(ctx, "view <save|list|delete> ...");
|
|
121146
|
+
if (sub === "list") {
|
|
121147
|
+
const views = await ctx.api.listSavedViews(await ctx.repoId());
|
|
121148
|
+
if (ctx.json) return emitJson(ctx, views);
|
|
121149
|
+
if (views.length === 0) {
|
|
121150
|
+
ctx.out("no saved views");
|
|
121151
|
+
return 0;
|
|
121152
|
+
}
|
|
121153
|
+
ctx.out(
|
|
121154
|
+
table([
|
|
121155
|
+
["NAME", "REVSET", "CREATED BY"],
|
|
121156
|
+
...views.map((v) => [v.name, v.revsetExpression, v.createdBy ?? "-"])
|
|
121157
|
+
])
|
|
121158
|
+
);
|
|
121159
|
+
return 0;
|
|
121160
|
+
}
|
|
121161
|
+
if (sub === "save") {
|
|
121162
|
+
const [name, revsetExpression, ...extra] = rest;
|
|
121163
|
+
if (!name || !revsetExpression || extra.length > 0) {
|
|
121164
|
+
return usageError(ctx, "view save <name> <revsetExpression>");
|
|
121165
|
+
}
|
|
121166
|
+
const view = await ctx.api.createSavedView(await ctx.repoId(), { name, revsetExpression });
|
|
121167
|
+
if (ctx.json) return emitJson(ctx, view);
|
|
121168
|
+
ctx.out(`saved view ${view.name}: ${view.revsetExpression}`);
|
|
121169
|
+
return 0;
|
|
121170
|
+
}
|
|
121171
|
+
if (sub === "delete") {
|
|
121172
|
+
const [name] = rest;
|
|
121173
|
+
if (!name) return usageError(ctx, "view delete <name>");
|
|
121174
|
+
await ctx.api.deleteSavedView(await ctx.repoId(), name);
|
|
121175
|
+
ctx.out(`deleted view ${name}`);
|
|
121176
|
+
return 0;
|
|
121177
|
+
}
|
|
121178
|
+
return usageError(ctx, `unknown view subcommand "${sub}"`);
|
|
121179
|
+
}
|
|
121180
|
+
async function opCmd(ctx, pos) {
|
|
121181
|
+
const [sub, ...rest] = pos;
|
|
121182
|
+
if (sub === "diff") return opDiffCmd(ctx, rest);
|
|
121183
|
+
if (sub !== "log") return usageError(ctx, "op log | op diff <fromOpId> <toOpId>");
|
|
120799
121184
|
const repoId = await ctx.repoId();
|
|
120800
121185
|
const [ops, head] = await Promise.all([ctx.api.listOperations(repoId), ctx.api.headOperation(repoId)]);
|
|
120801
121186
|
if (ctx.json) return emitJson(ctx, ops);
|
|
@@ -120807,6 +121192,28 @@ async function opCmd(ctx, pos) {
|
|
|
120807
121192
|
ctx.out(table([[" ", "OP", "TYPE", "SUMMARY", "AT"], ...rows]));
|
|
120808
121193
|
return 0;
|
|
120809
121194
|
}
|
|
121195
|
+
function formatDiffBucket(label, bucket) {
|
|
121196
|
+
const parts = [];
|
|
121197
|
+
if (bucket.created.length > 0) parts.push(`created: ${bucket.created.join(", ")}`);
|
|
121198
|
+
if (bucket.modified.length > 0) parts.push(`modified: ${bucket.modified.join(", ")}`);
|
|
121199
|
+
if (bucket.removed.length > 0) parts.push(`removed: ${bucket.removed.join(", ")}`);
|
|
121200
|
+
return parts.length === 0 ? `${label}: no changes` : `${label}:
|
|
121201
|
+
${parts.join("\n ")}`;
|
|
121202
|
+
}
|
|
121203
|
+
async function opDiffCmd(ctx, pos) {
|
|
121204
|
+
const [fromOpId, toOpId] = pos;
|
|
121205
|
+
if (!fromOpId || !toOpId) return usageError(ctx, "op diff <fromOpId> <toOpId>");
|
|
121206
|
+
const repoId = await ctx.repoId();
|
|
121207
|
+
const diff = await ctx.api.getOperationDiff(repoId, fromOpId, toOpId);
|
|
121208
|
+
if (ctx.json) return emitJson(ctx, diff);
|
|
121209
|
+
ctx.out(`${diff.fromOpId} (seq ${diff.fromSequence}) -> ${diff.toOpId} (seq ${diff.toSequence})`);
|
|
121210
|
+
ctx.out("");
|
|
121211
|
+
ctx.out(formatDiffBucket("changes", diff.changes));
|
|
121212
|
+
ctx.out(formatDiffBucket("stacks", diff.stacks));
|
|
121213
|
+
ctx.out(formatDiffBucket("bookmarks", diff.bookmarks));
|
|
121214
|
+
ctx.out(formatDiffBucket("conflicts", diff.conflicts));
|
|
121215
|
+
return 0;
|
|
121216
|
+
}
|
|
120810
121217
|
async function undoRedoCmd(ctx, verb, values) {
|
|
120811
121218
|
const repoId = await ctx.repoId();
|
|
120812
121219
|
const op2 = verb === "undo" ? await ctx.api.undo(repoId, { force: values.force }) : await ctx.api.redo(repoId);
|
|
@@ -121095,12 +121502,46 @@ var CONTENT_OPERATION_KINDS = [
|
|
|
121095
121502
|
"duplicate",
|
|
121096
121503
|
"restore"
|
|
121097
121504
|
];
|
|
121505
|
+
async function recordContentOpReceiptBestEffort(ctx, input) {
|
|
121506
|
+
try {
|
|
121507
|
+
const repoId = await ctx.repoId();
|
|
121508
|
+
await ctx.api.recordContentOpReceipt(repoId, input);
|
|
121509
|
+
} catch (e) {
|
|
121510
|
+
ctx.err(
|
|
121511
|
+
`warning: could not record a JJHub content-op receipt for this apply (continuing): ${e instanceof Error ? e.message : String(e)}`
|
|
121512
|
+
);
|
|
121513
|
+
}
|
|
121514
|
+
}
|
|
121515
|
+
async function contentReceiptsCmd(ctx, pos) {
|
|
121516
|
+
if (pos.length > 0) return usageError(ctx, "content receipts [--repo owner/name|repoId]");
|
|
121517
|
+
const repoId = await ctx.repoId();
|
|
121518
|
+
const receipts = await ctx.api.listContentOpReceipts(repoId);
|
|
121519
|
+
if (ctx.json) return emitJson(ctx, receipts);
|
|
121520
|
+
ctx.out(`${receipts.length} content-op receipt${receipts.length === 1 ? "" : "s"}`);
|
|
121521
|
+
if (receipts.length > 0) {
|
|
121522
|
+
ctx.out(
|
|
121523
|
+
table([
|
|
121524
|
+
["WHEN", "KIND", "SOURCE", "OK", "RESULT / ERROR", "EXPECTED-OP"],
|
|
121525
|
+
...receipts.map((r) => [
|
|
121526
|
+
r.requestedAt,
|
|
121527
|
+
r.kind,
|
|
121528
|
+
r.sourceRevision,
|
|
121529
|
+
r.succeeded ? "yes" : "no",
|
|
121530
|
+
r.succeeded ? r.resultOperationId ?? "" : r.errorMessage ?? "failed",
|
|
121531
|
+
r.expectedOperationId
|
|
121532
|
+
])
|
|
121533
|
+
])
|
|
121534
|
+
);
|
|
121535
|
+
}
|
|
121536
|
+
return 0;
|
|
121537
|
+
}
|
|
121098
121538
|
async function contentCmd(ctx, pos, values) {
|
|
121539
|
+
if (pos[0] === "receipts") return contentReceiptsCmd(ctx, pos.slice(1));
|
|
121099
121540
|
const [mode, localRepoPath, kindValue, ...extra] = pos;
|
|
121100
121541
|
if (mode !== "preview" && mode !== "apply" || !localRepoPath || !kindValue || extra.length > 0 || !CONTENT_OPERATION_KINDS.includes(kindValue)) {
|
|
121101
121542
|
return usageError(
|
|
121102
121543
|
ctx,
|
|
121103
|
-
"content <preview|apply> <localRepoPath> <split|absorb|squash|duplicate|restore> --source <jj-change-id> [--destination <jj-change-id>] [--path <relative-path>]
|
|
121544
|
+
"content <preview|apply> <localRepoPath> <split|absorb|squash|duplicate|restore> --source <jj-change-id> [--destination <jj-change-id>] [--path <relative-path>]...\ncontent receipts [--repo owner/name|repoId]"
|
|
121104
121545
|
);
|
|
121105
121546
|
}
|
|
121106
121547
|
const engine = values.engine;
|
|
@@ -121161,7 +121602,26 @@ async function contentCmd(ctx, pos, values) {
|
|
|
121161
121602
|
if (!input.expectedOperationId) {
|
|
121162
121603
|
return usageError(ctx, "content apply ... --expected-operation <jj-operation-id-from-preview>");
|
|
121163
121604
|
}
|
|
121164
|
-
|
|
121605
|
+
let receipt;
|
|
121606
|
+
let applyError;
|
|
121607
|
+
try {
|
|
121608
|
+
receipt = await jj.executeContentOperation(input);
|
|
121609
|
+
} catch (e) {
|
|
121610
|
+
applyError = e instanceof Error ? e : new Error(String(e));
|
|
121611
|
+
}
|
|
121612
|
+
await recordContentOpReceiptBestEffort(ctx, {
|
|
121613
|
+
kind: input.kind,
|
|
121614
|
+
sourceRevision: input.sourceRevision,
|
|
121615
|
+
destinationRevision: input.destinationRevision,
|
|
121616
|
+
paths: input.paths,
|
|
121617
|
+
expectedOperationId: input.expectedOperationId,
|
|
121618
|
+
resultOperationId: receipt?.operationId,
|
|
121619
|
+
changedChangeIds: receipt?.changedChangeIds ?? [],
|
|
121620
|
+
succeeded: applyError === void 0,
|
|
121621
|
+
errorMessage: applyError?.message
|
|
121622
|
+
});
|
|
121623
|
+
if (applyError) throw applyError;
|
|
121624
|
+
if (!receipt) throw new Error("unreachable: content apply produced neither a receipt nor an error");
|
|
121165
121625
|
if (ctx.json) return emitJson(ctx, receipt);
|
|
121166
121626
|
ctx.out(`${receipt.input.kind} completed in ${localRepoPath}`);
|
|
121167
121627
|
ctx.out(` jj operation: ${receipt.operationId}`);
|
|
@@ -121178,7 +121638,7 @@ async function daemonForegroundCmd(ctx, pos, values) {
|
|
|
121178
121638
|
if (!localRepoPath) {
|
|
121179
121639
|
return usageError(
|
|
121180
121640
|
ctx,
|
|
121181
|
-
"daemon <localRepoPath> [--once] [--interval 5] [--engine auto|subprocess|isomorphic]"
|
|
121641
|
+
"daemon <localRepoPath> [--once] [--interval 5] [--engine auto|subprocess|isomorphic] [--remote name]"
|
|
121182
121642
|
);
|
|
121183
121643
|
}
|
|
121184
121644
|
const engine = values.engine;
|
|
@@ -121196,10 +121656,12 @@ async function daemonForegroundCmd(ctx, pos, values) {
|
|
|
121196
121656
|
return 1;
|
|
121197
121657
|
}
|
|
121198
121658
|
const twoWay = values["two-way"] === true;
|
|
121659
|
+
const remote = values.remote;
|
|
121199
121660
|
if (values.once === true) {
|
|
121200
121661
|
const summary = await runDaemonOnce(jj, ctx.api, repoId, {
|
|
121201
121662
|
repoPath: localRepoPath,
|
|
121202
121663
|
twoWay,
|
|
121664
|
+
remote,
|
|
121203
121665
|
log: ctx.json ? ctx.err : ctx.out
|
|
121204
121666
|
});
|
|
121205
121667
|
const exitCode = summary.pushFailures > 0 || summary.syncErrors > 0 ? 1 : 0;
|
|
@@ -121222,7 +121684,8 @@ async function daemonForegroundCmd(ctx, pos, values) {
|
|
|
121222
121684
|
log: ctx.out,
|
|
121223
121685
|
repoPath: localRepoPath,
|
|
121224
121686
|
engineLabel: engine ?? "auto",
|
|
121225
|
-
twoWay
|
|
121687
|
+
twoWay,
|
|
121688
|
+
remote
|
|
121226
121689
|
});
|
|
121227
121690
|
return 0;
|
|
121228
121691
|
}
|
|
@@ -121307,6 +121770,7 @@ async function daemonStartCmd(ctx, rest, values) {
|
|
|
121307
121770
|
engine,
|
|
121308
121771
|
baseUrl: ctx.opts.baseUrl,
|
|
121309
121772
|
twoWay: values["two-way"] === true,
|
|
121773
|
+
remote: values.remote,
|
|
121310
121774
|
spawnFn: ctx.opts.spawnDaemonProcess
|
|
121311
121775
|
});
|
|
121312
121776
|
setRepoAlias(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jjhub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
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
6
|
"jjhub": "./dist/cli/jjhub.mjs"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"node": ">=22.6.0"
|
|
11
11
|
},
|
|
12
12
|
"license": "UNLICENSED",
|
|
13
|
-
"homepage": "https://jjhub.
|
|
13
|
+
"homepage": "https://jjhub.erisera.com",
|
|
14
14
|
"keywords": [
|
|
15
15
|
"jujutsu",
|
|
16
16
|
"jj",
|
|
@@ -69,9 +69,14 @@ change land <changeId> [--force] waits for real CI triggered by its own re
|
|
|
69
69
|
change split <changeId> <newTitle>
|
|
70
70
|
change checks <changeId> list GitHub check runs for a change
|
|
71
71
|
change actions <changeId> show bounded GitHub Actions workflow and job evidence
|
|
72
|
+
change actions rerun <changeId> <runId>
|
|
73
|
+
ask GitHub to re-run a workflow run shown by "change actions"
|
|
72
74
|
change deployments <changeId> show bounded GitHub deployment evidence
|
|
73
75
|
change security <changeId> show bounded GitHub code-scanning evidence
|
|
74
76
|
change issues <changeId> show linked GitHub issue context for a change
|
|
77
|
+
change reviewers <changeId> reviewer-routing audit trail: every request/remove that
|
|
78
|
+
actually succeeded against GitHub, who did it, and when
|
|
79
|
+
change projects <changeId> show bounded GitHub Projects v2 evidence for linked issues
|
|
75
80
|
|
|
76
81
|
stack create <title> [--id x] [--changes CHG-1,CHG-2]
|
|
77
82
|
stack list
|
|
@@ -85,6 +90,11 @@ bookmark list
|
|
|
85
90
|
bookmark delete <name> [--force] --force required for the default (trunk) bookmark
|
|
86
91
|
bookmark set <name> <tracked|view> [--target CHG-x]
|
|
87
92
|
|
|
93
|
+
view save <name> <revsetExpression> save a named jj revset expression for later reuse (storage
|
|
94
|
+
only — evaluate it with "jjhub revset")
|
|
95
|
+
view list list saved views in this repo
|
|
96
|
+
view delete <name>
|
|
97
|
+
|
|
88
98
|
op log "@" marks the current head operation
|
|
89
99
|
undo [--force] refused when the head op is a "land" (real merge can't be unwound); --force reverts local state anyway
|
|
90
100
|
redo
|
|
@@ -105,7 +115,7 @@ auth sessions list your live sessions; auth revoke <id>
|
|
|
105
115
|
auth logout forget the saved connection/token
|
|
106
116
|
auth status who the saved credential authenticates as
|
|
107
117
|
|
|
108
|
-
mcp serve MCP server over stdio (
|
|
118
|
+
mcp serve MCP server over stdio (86 tools) for agent clients
|
|
109
119
|
mcp serve --http <port> the same tools over Streamable HTTP (POST /)
|
|
110
120
|
acp serve Agent Client Protocol agent over stdio (Zed's agent
|
|
111
121
|
panel, ...) — every operation as a slash command
|
|
@@ -130,7 +140,11 @@ content preview <localRepoPath> <split|absorb|squash|duplicate|restore>
|
|
|
130
140
|
content apply <localRepoPath> <split|absorb|squash|duplicate|restore>
|
|
131
141
|
same selection plus --expected-operation <jj-operation-id> returned by preview
|
|
132
142
|
local-only jj content curation; checked against jj's immutable policy and
|
|
133
|
-
never implemented as a browser patch transform
|
|
143
|
+
never implemented as a browser patch transform; records a durable receipt
|
|
144
|
+
on the JJHub server (best-effort — see "content receipts" below)
|
|
145
|
+
content receipts [--repo owner/name|repoId]
|
|
146
|
+
list this repo's content-op receipt trail: every content apply invocation,
|
|
147
|
+
successful or rejected, with its kind, revisions, and outcome
|
|
134
148
|
```
|
|
135
149
|
|
|
136
150
|
Anything else (`pr`, `issue`, ...) is passed through to `gh` — e.g. `jjhub pr view 7`. `jjhub auth`
|
|
@@ -243,6 +257,15 @@ targets are refused without `--ignore-immutable`. `--path` selects complete
|
|
|
243
257
|
relative paths; interactive hunk selection remains jj's configured diff editor.
|
|
244
258
|
Run `jjhub daemon . --once` afterwards to reconcile the local graph.
|
|
245
259
|
|
|
260
|
+
Each `content apply` invocation — successful or rejected (e.g. a stale
|
|
261
|
+
`--expected-operation`) — records a durable receipt on the JJHub server
|
|
262
|
+
(best-effort: a missing repo context or unreachable server only produces a
|
|
263
|
+
warning, never blocks the local jj mutation). Review the trail with:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
npm run jjhub -- content receipts
|
|
267
|
+
```
|
|
268
|
+
|
|
246
269
|
### Resolve a conflict
|
|
247
270
|
|
|
248
271
|
```bash
|