jjhub 0.1.3 → 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/dist/cli/jjhub.mjs +124 -23
- package/package.json +1 -1
- package/skills/jjhub-cli/SKILL.md +3 -2
package/dist/cli/jjhub.mjs
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
@@ -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,
|
|
@@ -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 ??
|
|
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 (
|
|
93433
|
-
|
|
93434
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -93741,8 +93836,9 @@ Usage: jjhub <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
|
|
@@ -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;
|
|
@@ -94175,19 +94272,23 @@ async function changeCmd(ctx, pos, values) {
|
|
|
94175
94272
|
}
|
|
94176
94273
|
case "commit": {
|
|
94177
94274
|
const [changeId2, ...paths] = rest;
|
|
94178
|
-
|
|
94179
|
-
|
|
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
|
-
|
|
94189
|
-
`
|
|
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": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jjhub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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"
|
|
@@ -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,
|