codiedev 0.9.65 → 0.9.66
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.d.ts +0 -1
- package/dist/cli.js +14 -35
- package/dist/commands/doctor.js +0 -34
- package/dist/mcp.js +0 -337
- package/dist/utils.d.ts +0 -4
- package/dist/utils.js +0 -6
- package/package.json +1 -1
- package/dist/commands/promote.d.ts +0 -1
- package/dist/commands/promote.js +0 -53
- package/dist/commands/react.d.ts +0 -2
- package/dist/commands/react.js +0 -40
- package/dist/commands/reverseTicket.d.ts +0 -2
- package/dist/commands/reverseTicket.js +0 -477
- package/dist/prUrl.d.ts +0 -9
- package/dist/prUrl.js +0 -33
package/dist/cli.d.ts
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* codiedev inbox — list pings (use --unread to filter)
|
|
11
11
|
* codiedev read <ping-id> — mark read + show full message
|
|
12
12
|
* codiedev note "<text>" — capture a thought
|
|
13
|
-
* codiedev promote <artifact-id> — promote an extracted artifact to authored
|
|
14
13
|
* codiedev help — show this help
|
|
15
14
|
*/
|
|
16
15
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
* codiedev inbox — list pings (use --unread to filter)
|
|
12
12
|
* codiedev read <ping-id> — mark read + show full message
|
|
13
13
|
* codiedev note "<text>" — capture a thought
|
|
14
|
-
* codiedev promote <artifact-id> — promote an extracted artifact to authored
|
|
15
14
|
* codiedev help — show this help
|
|
16
15
|
*/
|
|
17
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -23,15 +22,12 @@ const ask_1 = require("./commands/ask");
|
|
|
23
22
|
const ping_1 = require("./commands/ping");
|
|
24
23
|
const inbox_1 = require("./commands/inbox");
|
|
25
24
|
const note_1 = require("./commands/note");
|
|
26
|
-
const promote_1 = require("./commands/promote");
|
|
27
|
-
const reverseTicket_1 = require("./commands/reverseTicket");
|
|
28
25
|
const createTicket_1 = require("./commands/createTicket");
|
|
29
26
|
const doctor_1 = require("./commands/doctor");
|
|
30
27
|
const search_1 = require("./commands/search");
|
|
31
28
|
const library_1 = require("./commands/library");
|
|
32
29
|
const share_1 = require("./commands/share");
|
|
33
30
|
const send_1 = require("./commands/send");
|
|
34
|
-
const react_1 = require("./commands/react");
|
|
35
31
|
const capture_1 = require("./commands/capture");
|
|
36
32
|
const version_1 = require("./version");
|
|
37
33
|
const HELP = `
|
|
@@ -64,14 +60,6 @@ Artifacts:
|
|
|
64
60
|
team's collective memory. Returns an
|
|
65
61
|
LLM-synthesized answer with citations
|
|
66
62
|
pointing at sessions + artifacts.
|
|
67
|
-
codiedev promote <artifact-id> Promote an auto-extracted artifact
|
|
68
|
-
to an authored one
|
|
69
|
-
codiedev reverse-ticket [<pr-url>] Generate a Jira ticket draft.
|
|
70
|
-
No URL = branch mode (uses your current
|
|
71
|
-
local git branch, no PR needed).
|
|
72
|
-
Draft lands in /portal/agentic-ticketing.
|
|
73
|
-
codiedev reverse-ticket --base <ref> Override the base ref in branch mode
|
|
74
|
-
(defaults to origin/main).
|
|
75
63
|
codiedev create-ticket [<prompt>] Forward-write a scoped ticket plan.
|
|
76
64
|
Claude investigates the codebase, asks
|
|
77
65
|
"single repo or multiple? where?", then
|
|
@@ -97,9 +85,6 @@ Discovery:
|
|
|
97
85
|
[--type T] [--tag X] [--limit N]
|
|
98
86
|
List artifacts in your workspace
|
|
99
87
|
|
|
100
|
-
Feed:
|
|
101
|
-
codiedev react <postId> <emoji> Toggle a reaction on a feed post
|
|
102
|
-
|
|
103
88
|
Privacy:
|
|
104
89
|
codiedev ignore <path> Never capture this folder or its children
|
|
105
90
|
codiedev unignore <path> Stop ignoring a folder
|
|
@@ -270,20 +255,6 @@ codiedev note — capture a passing thought, linked to your session
|
|
|
270
255
|
unignore: IGNORE_HELP,
|
|
271
256
|
ignored: IGNORE_HELP,
|
|
272
257
|
capture: IGNORE_HELP,
|
|
273
|
-
promote: `
|
|
274
|
-
codiedev promote — promote an auto-extracted artifact to authored
|
|
275
|
-
|
|
276
|
-
Usage:
|
|
277
|
-
codiedev promote <artifact-id> [--key filename.md]
|
|
278
|
-
|
|
279
|
-
The session-capture pipeline auto-extracts spec/bugfix/decision
|
|
280
|
-
artifacts from each agent session. Promote turns one of those into
|
|
281
|
-
a first-class authored artifact your team can ping, pull, and
|
|
282
|
-
update versions of.
|
|
283
|
-
|
|
284
|
-
Find artifact-ids in the portal under Knowledge → Memory, or via
|
|
285
|
-
the portal search.
|
|
286
|
-
`.trim(),
|
|
287
258
|
"create-ticket": `
|
|
288
259
|
codiedev create-ticket — forward-write a scoped ticket plan
|
|
289
260
|
|
|
@@ -409,6 +380,18 @@ Run 'codiedev help <command>' for each, or see the quick list:
|
|
|
409
380
|
|
|
410
381
|
Tokens are per-user. Rotate any time by revoking + creating a new one.
|
|
411
382
|
`.trim();
|
|
383
|
+
// Retired commands: say so instead of "Unknown command", since agent
|
|
384
|
+
// instructions written by older installs may still reference them.
|
|
385
|
+
const REMOVED_COMMANDS = {
|
|
386
|
+
promote: "Share an auto-saved spec with 'Promote to team' in the portal's My artifacts list.",
|
|
387
|
+
"reverse-ticket": "Use 'codiedev create-ticket' to draft Jira tickets.",
|
|
388
|
+
reverseTicket: "Use 'codiedev create-ticket' to draft Jira tickets.",
|
|
389
|
+
react: "React to feed posts in the portal.",
|
|
390
|
+
};
|
|
391
|
+
function removedCommand(command) {
|
|
392
|
+
console.error(`'codiedev ${command}' has been removed. ${REMOVED_COMMANDS[command] ?? ""}`.trim());
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
412
395
|
async function main() {
|
|
413
396
|
const [, , command, ...rest] = process.argv;
|
|
414
397
|
if (!command || command === "--help" || command === "-h") {
|
|
@@ -486,11 +469,10 @@ async function main() {
|
|
|
486
469
|
await (0, note_1.runNote)(rest);
|
|
487
470
|
return;
|
|
488
471
|
case "promote":
|
|
489
|
-
await (0, promote_1.runPromote)(rest);
|
|
490
|
-
return;
|
|
491
472
|
case "reverse-ticket":
|
|
492
473
|
case "reverseTicket":
|
|
493
|
-
|
|
474
|
+
case "react":
|
|
475
|
+
removedCommand(command);
|
|
494
476
|
return;
|
|
495
477
|
case "create-ticket":
|
|
496
478
|
case "createTicket":
|
|
@@ -509,9 +491,6 @@ async function main() {
|
|
|
509
491
|
case "send":
|
|
510
492
|
await (0, send_1.runSend)(rest);
|
|
511
493
|
return;
|
|
512
|
-
case "react":
|
|
513
|
-
await (0, react_1.runReact)(rest);
|
|
514
|
-
return;
|
|
515
494
|
case "ignore":
|
|
516
495
|
case "unignore":
|
|
517
496
|
case "ignored":
|
package/dist/commands/doctor.js
CHANGED
|
@@ -132,24 +132,6 @@ function hasCodiedevInstructions(instructionsPath) {
|
|
|
132
132
|
return false;
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
-
function hasGhCli() {
|
|
136
|
-
try {
|
|
137
|
-
(0, child_process_1.execSync)("gh --version", { stdio: ["pipe", "pipe", "pipe"] });
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
function hasGlabCli() {
|
|
145
|
-
try {
|
|
146
|
-
(0, child_process_1.execSync)("glab --version", { stdio: ["pipe", "pipe", "pipe"] });
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
135
|
async function runDoctor(_args) {
|
|
154
136
|
const checks = [];
|
|
155
137
|
console.log("\nCodiedev doctor — checking your setup…\n");
|
|
@@ -514,22 +496,6 @@ async function runDoctor(_args) {
|
|
|
514
496
|
else {
|
|
515
497
|
checks.push({ name: "GitHub Copilot", status: "warn", detail: "not installed on this machine" });
|
|
516
498
|
}
|
|
517
|
-
// 8. gh / glab CLIs (optional — only needed for reverse-ticket from a
|
|
518
|
-
// specific PR/MR URL; branch mode works without either).
|
|
519
|
-
checks.push({
|
|
520
|
-
name: "GitHub CLI (`gh`) for reverse-ticket from a PR url",
|
|
521
|
-
status: hasGhCli() ? "pass" : "warn",
|
|
522
|
-
detail: hasGhCli()
|
|
523
|
-
? "installed"
|
|
524
|
-
: "not installed — only needed for `codiedev reverse-ticket <github-pr-url>`",
|
|
525
|
-
});
|
|
526
|
-
checks.push({
|
|
527
|
-
name: "GitLab CLI (`glab`) for reverse-ticket from an MR url",
|
|
528
|
-
status: hasGlabCli() ? "pass" : "warn",
|
|
529
|
-
detail: hasGlabCli()
|
|
530
|
-
? "installed"
|
|
531
|
-
: "not installed — only needed for `codiedev reverse-ticket <gitlab-mr-url>`",
|
|
532
|
-
});
|
|
533
499
|
report(checks);
|
|
534
500
|
const failures = checks.filter((c) => c.status === "fail");
|
|
535
501
|
if (failures.length > 0) {
|
package/dist/mcp.js
CHANGED
|
@@ -237,29 +237,6 @@ const TOOLS = [
|
|
|
237
237
|
required: ["body"],
|
|
238
238
|
},
|
|
239
239
|
},
|
|
240
|
-
{
|
|
241
|
-
name: "codiedev_promote",
|
|
242
|
-
description: "Promote an auto-extracted Codiedev artifact (spec/bugfix/decision " +
|
|
243
|
-
"produced by the session-capture pipeline) to an authored artifact " +
|
|
244
|
-
"so teammates can collaborate on it. Use when the user says 'publish " +
|
|
245
|
-
"the extracted spec', 'promote this', or 'make the auto-extracted X " +
|
|
246
|
-
"shareable'.",
|
|
247
|
-
inputSchema: {
|
|
248
|
-
type: "object",
|
|
249
|
-
properties: {
|
|
250
|
-
artifactId: {
|
|
251
|
-
type: "string",
|
|
252
|
-
description: "The extracted artifact's id from Codiedev.",
|
|
253
|
-
},
|
|
254
|
-
keyOverride: {
|
|
255
|
-
type: "string",
|
|
256
|
-
description: "Optional filename key for the promoted artifact. If omitted, " +
|
|
257
|
-
"one is generated from the artifact's type + session id.",
|
|
258
|
-
},
|
|
259
|
-
},
|
|
260
|
-
required: ["artifactId"],
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
240
|
{
|
|
264
241
|
name: "codiedev_share_with",
|
|
265
242
|
description: "Grant a teammate persistent read (or edit) access to an artifact " +
|
|
@@ -306,27 +283,6 @@ const TOOLS = [
|
|
|
306
283
|
required: ["filename", "to"],
|
|
307
284
|
},
|
|
308
285
|
},
|
|
309
|
-
{
|
|
310
|
-
name: "codiedev_react",
|
|
311
|
-
description: "React to a feed post with an emoji. Use when the user says 'react " +
|
|
312
|
-
"to that post with X', 'mark as used', or 'upvote'. The 🛠 " +
|
|
313
|
-
"reaction is the signal for 'I used this' and bumps the linked " +
|
|
314
|
-
"artifact's reuse count. Pass the post id (get it from " +
|
|
315
|
-
"codiedev_get_library or a previous feed response).",
|
|
316
|
-
inputSchema: {
|
|
317
|
-
type: "object",
|
|
318
|
-
properties: {
|
|
319
|
-
postId: { type: "string", description: "Feed post id." },
|
|
320
|
-
emoji: {
|
|
321
|
-
type: "string",
|
|
322
|
-
enum: ["👍", "🔥", "💡", "🛠", "👑"],
|
|
323
|
-
description: "Reaction emoji. 🛠 = 'I used this' (counts heaviest toward " +
|
|
324
|
-
"the author's score and the artifact's reuse count).",
|
|
325
|
-
},
|
|
326
|
-
},
|
|
327
|
-
required: ["postId", "emoji"],
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
286
|
{
|
|
331
287
|
name: "codiedev_search",
|
|
332
288
|
description: "Search the team's artifact library by natural-language query. Use " +
|
|
@@ -413,41 +369,6 @@ const TOOLS = [
|
|
|
413
369
|
},
|
|
414
370
|
},
|
|
415
371
|
},
|
|
416
|
-
{
|
|
417
|
-
name: "codiedev_reverse_ticket",
|
|
418
|
-
description: "Generate a Jira-ready reverse-ticket draft from the work the user has " +
|
|
419
|
-
"done in their CURRENT git branch — mid-session, before any PR exists. " +
|
|
420
|
-
"Reads local git state (current branch, divergence from main, diff, " +
|
|
421
|
-
"commits, files changed) and runs it through the codiedev reverse-" +
|
|
422
|
-
"ticket writer. The draft lands in the user's portal at " +
|
|
423
|
-
"/portal/agentic-ticketing where they can edit the title, description, " +
|
|
424
|
-
"and scope, then push to Jira. Use when the user asks to 'create a " +
|
|
425
|
-
"reverse ticket', 'draft a Jira ticket from this work', 'write up what " +
|
|
426
|
-
"I've done in this branch', or runs `codiedev reverse-ticket` directly. " +
|
|
427
|
-
"The webhook on PR-open later dedupes against this draft so the user " +
|
|
428
|
-
"never gets a double ticket. The draft is intentionally not auto-pushed " +
|
|
429
|
-
"to Jira — trust comes from the user reviewing in the portal first.",
|
|
430
|
-
inputSchema: {
|
|
431
|
-
type: "object",
|
|
432
|
-
properties: {
|
|
433
|
-
title: {
|
|
434
|
-
type: "string",
|
|
435
|
-
description: "Optional working title for the ticket. Defaults to the most " +
|
|
436
|
-
"recent commit subject on the branch.",
|
|
437
|
-
},
|
|
438
|
-
base: {
|
|
439
|
-
type: "string",
|
|
440
|
-
description: "Optional base ref to diff against. Defaults to 'origin/main', " +
|
|
441
|
-
"falls back to 'origin/master'.",
|
|
442
|
-
},
|
|
443
|
-
forcedArtifactKey: {
|
|
444
|
-
type: "string",
|
|
445
|
-
description: "Optional — force the writer to link to a specific artifact key " +
|
|
446
|
-
"for context (e.g. 'spec-auth.md').",
|
|
447
|
-
},
|
|
448
|
-
},
|
|
449
|
-
},
|
|
450
|
-
},
|
|
451
372
|
];
|
|
452
373
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
453
374
|
// Server
|
|
@@ -538,22 +459,16 @@ async function dispatchTool(name, args, config) {
|
|
|
538
459
|
return await handleInbox(args, config);
|
|
539
460
|
case "codiedev_note":
|
|
540
461
|
return await handleNote(args, config);
|
|
541
|
-
case "codiedev_promote":
|
|
542
|
-
return await handlePromote(args, config);
|
|
543
462
|
case "codiedev_share_with":
|
|
544
463
|
return await handleShareWith(args, config);
|
|
545
464
|
case "codiedev_send_to":
|
|
546
465
|
return await handleSendTo(args, config);
|
|
547
|
-
case "codiedev_react":
|
|
548
|
-
return await handleReact(args, config);
|
|
549
466
|
case "codiedev_ask":
|
|
550
467
|
return await handleAsk(args, config);
|
|
551
468
|
case "codiedev_search":
|
|
552
469
|
return await handleSearch(args, config);
|
|
553
470
|
case "codiedev_get_library":
|
|
554
471
|
return await handleGetLibrary(args, config);
|
|
555
|
-
case "codiedev_reverse_ticket":
|
|
556
|
-
return await handleReverseTicket(args, config);
|
|
557
472
|
default:
|
|
558
473
|
return {
|
|
559
474
|
isError: true,
|
|
@@ -705,21 +620,6 @@ async function handleNote(args, config) {
|
|
|
705
620
|
content: [{ type: "text", text: `✓ Noted (${res.key})` }],
|
|
706
621
|
};
|
|
707
622
|
}
|
|
708
|
-
async function handlePromote(args, config) {
|
|
709
|
-
const artifactId = asString(args.artifactId);
|
|
710
|
-
const keyOverride = asStringOrUndefined(args.keyOverride);
|
|
711
|
-
if (!artifactId)
|
|
712
|
-
throw new Error("artifactId required");
|
|
713
|
-
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/promote", { config, body: { artifactId, keyOverride } });
|
|
714
|
-
return {
|
|
715
|
-
content: [
|
|
716
|
-
{
|
|
717
|
-
type: "text",
|
|
718
|
-
text: `✓ Promoted to authored artifact ${res.key} (id=${res.artifactId}).`,
|
|
719
|
-
},
|
|
720
|
-
],
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
623
|
async function handleShareWith(args, config) {
|
|
724
624
|
const filename = asString(args.filename);
|
|
725
625
|
const to = asString(args.to);
|
|
@@ -810,26 +710,6 @@ async function handleSendTo(args, config) {
|
|
|
810
710
|
return formatAmbiguousRecipient(err, to);
|
|
811
711
|
}
|
|
812
712
|
}
|
|
813
|
-
async function handleReact(args, config) {
|
|
814
|
-
const postId = asString(args.postId);
|
|
815
|
-
const emoji = asString(args.emoji);
|
|
816
|
-
if (!postId)
|
|
817
|
-
throw new Error("postId required");
|
|
818
|
-
if (!emoji)
|
|
819
|
-
throw new Error("emoji required");
|
|
820
|
-
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/react", {
|
|
821
|
-
config,
|
|
822
|
-
body: { postId, emoji },
|
|
823
|
-
});
|
|
824
|
-
const verb = res.action === "added"
|
|
825
|
-
? "Reacted"
|
|
826
|
-
: res.action === "removed"
|
|
827
|
-
? "Removed reaction"
|
|
828
|
-
: "Swapped reaction to";
|
|
829
|
-
return {
|
|
830
|
-
content: [{ type: "text", text: `✓ ${verb} ${res.emoji}` }],
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
713
|
async function handleSearch(args, config) {
|
|
834
714
|
const query = asString(args.query);
|
|
835
715
|
const limit = asIntOrUndefined(args.limit);
|
|
@@ -937,223 +817,6 @@ function formatAmbiguousRecipient(err, to) {
|
|
|
937
817
|
}
|
|
938
818
|
throw err;
|
|
939
819
|
}
|
|
940
|
-
async function handleReverseTicket(args, config) {
|
|
941
|
-
const explicitTitle = asStringOrUndefined(args.title);
|
|
942
|
-
const explicitBase = asStringOrUndefined(args.base);
|
|
943
|
-
const forcedKey = asStringOrUndefined(args.forcedArtifactKey);
|
|
944
|
-
// Helper that runs git from cwd with a hard-fail on error.
|
|
945
|
-
const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
946
|
-
const git = (cmd) => {
|
|
947
|
-
try {
|
|
948
|
-
return execSync(`git ${cmd}`, {
|
|
949
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
950
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
951
|
-
})
|
|
952
|
-
.toString("utf8")
|
|
953
|
-
.trim();
|
|
954
|
-
}
|
|
955
|
-
catch (err) {
|
|
956
|
-
throw new Error(`git ${cmd} failed: ${err.message}`);
|
|
957
|
-
}
|
|
958
|
-
};
|
|
959
|
-
// Confirm we're in a git repo.
|
|
960
|
-
try {
|
|
961
|
-
git("rev-parse --is-inside-work-tree");
|
|
962
|
-
}
|
|
963
|
-
catch {
|
|
964
|
-
throw new Error("Not inside a git repository. Run this from a checked-out repo.");
|
|
965
|
-
}
|
|
966
|
-
// Resolve base ref — explicit, then origin/main, then origin/master.
|
|
967
|
-
const candidates = explicitBase
|
|
968
|
-
? [explicitBase]
|
|
969
|
-
: ["origin/main", "origin/master"];
|
|
970
|
-
let baseRef = null;
|
|
971
|
-
for (const c of candidates) {
|
|
972
|
-
try {
|
|
973
|
-
git(`rev-parse --verify ${c}`);
|
|
974
|
-
baseRef = c;
|
|
975
|
-
break;
|
|
976
|
-
}
|
|
977
|
-
catch {
|
|
978
|
-
// try next
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
if (!baseRef) {
|
|
982
|
-
throw new Error(`Couldn't find a base ref (tried ${candidates.join(", ")}). ` +
|
|
983
|
-
`Pass an explicit base via the 'base' argument.`);
|
|
984
|
-
}
|
|
985
|
-
const branch = git("rev-parse --abbrev-ref HEAD");
|
|
986
|
-
if (branch === "HEAD") {
|
|
987
|
-
throw new Error("Detached HEAD — checkout a branch first.");
|
|
988
|
-
}
|
|
989
|
-
const baseSha = git(`merge-base HEAD ${baseRef}`);
|
|
990
|
-
const headSha = git("rev-parse HEAD");
|
|
991
|
-
// Filter generated/lockfile noise so the LLM's diff budget gets spent on
|
|
992
|
-
// real source. The --stat block (collected separately below) still includes
|
|
993
|
-
// every file untouched, so excluded files still appear in the ticket's
|
|
994
|
-
// "Other files touched" section if relevant.
|
|
995
|
-
const isNoise = (p) => /(^|\/)(node_modules|dist|build|\.next|\.turbo|coverage|out)\//.test(p) ||
|
|
996
|
-
/(^|\/)package-lock\.json$/.test(p) ||
|
|
997
|
-
/(^|\/)yarn\.lock$/.test(p) ||
|
|
998
|
-
/(^|\/)pnpm-lock\.yaml$/.test(p) ||
|
|
999
|
-
/(^|\/)bun\.lock(b)?$/.test(p) ||
|
|
1000
|
-
/(^|\/)Cargo\.lock$/.test(p) ||
|
|
1001
|
-
/(^|\/)Gemfile\.lock$/.test(p) ||
|
|
1002
|
-
/(^|\/)poetry\.lock$/.test(p) ||
|
|
1003
|
-
/(^|\/)go\.sum$/.test(p);
|
|
1004
|
-
// Helper: run a shell command tolerating non-zero exit codes (git diff
|
|
1005
|
-
// --no-index exits 1 when files differ, which is the normal case).
|
|
1006
|
-
// Reuses the execSync already imported above for the git() helper.
|
|
1007
|
-
const shellTolerant = (cmd) => {
|
|
1008
|
-
try {
|
|
1009
|
-
return execSync(`${cmd} || true`, {
|
|
1010
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1011
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
1012
|
-
})
|
|
1013
|
-
.toString("utf8")
|
|
1014
|
-
.trim();
|
|
1015
|
-
}
|
|
1016
|
-
catch {
|
|
1017
|
-
return "";
|
|
1018
|
-
}
|
|
1019
|
-
};
|
|
1020
|
-
const shQuote = (s) => `'${s.replace(/'/g, "'\\''")}'`;
|
|
1021
|
-
// Capture all work the developer has done relative to the base ref:
|
|
1022
|
-
// - committed changes between base and HEAD
|
|
1023
|
-
// - uncommitted modifications to tracked files
|
|
1024
|
-
// - new untracked files (path listed + content via synthetic diff)
|
|
1025
|
-
// Matches the user's intent ("ticket my current changes") rather than the
|
|
1026
|
-
// narrower "only what's been committed" interpretation.
|
|
1027
|
-
const trackedRaw = git(`diff --name-only ${baseSha}`);
|
|
1028
|
-
const trackedFiles = trackedRaw ? trackedRaw.split("\n").filter(Boolean) : [];
|
|
1029
|
-
const untrackedRaw = safeGit(() => git("ls-files --others --exclude-standard"));
|
|
1030
|
-
const untrackedFiles = untrackedRaw
|
|
1031
|
-
? untrackedRaw.split("\n").filter(Boolean)
|
|
1032
|
-
: [];
|
|
1033
|
-
const filesChanged = Array.from(new Set([...trackedFiles, ...untrackedFiles]));
|
|
1034
|
-
if (filesChanged.length === 0) {
|
|
1035
|
-
throw new Error(`No changes detected between ${baseRef} and your working tree on ${branch}.`);
|
|
1036
|
-
}
|
|
1037
|
-
// --stat block: compact ground truth for "every file touched". Tracked
|
|
1038
|
-
// changes from `git diff --stat baseSha`; untracked files appended as
|
|
1039
|
-
// synthesized stat rows so the LLM sees them with line counts.
|
|
1040
|
-
let stat = "";
|
|
1041
|
-
try {
|
|
1042
|
-
const trackedStat = git(`diff --stat=200 ${baseSha}`);
|
|
1043
|
-
const untrackedStatLines = untrackedFiles.map((f) => {
|
|
1044
|
-
const wc = shellTolerant(`wc -l < ${shQuote(f)}`);
|
|
1045
|
-
const n = parseInt(wc.split(/\s+/)[0] || "0", 10) || 0;
|
|
1046
|
-
return ` ${f} | ${n} ++++++++++ (new file)`;
|
|
1047
|
-
});
|
|
1048
|
-
stat = [trackedStat, ...untrackedStatLines]
|
|
1049
|
-
.filter(Boolean)
|
|
1050
|
-
.join("\n")
|
|
1051
|
-
.slice(0, 8_000);
|
|
1052
|
-
}
|
|
1053
|
-
catch {
|
|
1054
|
-
// Best effort.
|
|
1055
|
-
}
|
|
1056
|
-
// Diff body: 40k cap, noise-filtered. Tracked changes via `git diff baseSha`,
|
|
1057
|
-
// plus synthetic add-only diffs for untracked files via `git diff --no-index`.
|
|
1058
|
-
let diff = "";
|
|
1059
|
-
try {
|
|
1060
|
-
const sourceTracked = trackedFiles.filter((f) => !isNoise(f));
|
|
1061
|
-
const trackedDiff = sourceTracked.length > 0
|
|
1062
|
-
? git(`diff ${baseSha} -- ${sourceTracked.map(shQuote).join(" ")}`)
|
|
1063
|
-
: git(`diff ${baseSha}`);
|
|
1064
|
-
const sourceUntracked = untrackedFiles.filter((f) => !isNoise(f));
|
|
1065
|
-
const untrackedDiffs = sourceUntracked.map((f) => shellTolerant(`git diff --no-index --no-color -- /dev/null ${shQuote(f)}`));
|
|
1066
|
-
diff = [trackedDiff, ...untrackedDiffs]
|
|
1067
|
-
.filter(Boolean)
|
|
1068
|
-
.join("\n")
|
|
1069
|
-
.slice(0, 40_000);
|
|
1070
|
-
}
|
|
1071
|
-
catch {
|
|
1072
|
-
// Best effort.
|
|
1073
|
-
}
|
|
1074
|
-
// Repo name from origin remote.
|
|
1075
|
-
let repo;
|
|
1076
|
-
try {
|
|
1077
|
-
const remoteUrl = git("config --get remote.origin.url");
|
|
1078
|
-
const m = remoteUrl.match(/[:/]([^/:]+)\/([^/:]+?)(?:\.git)?$/);
|
|
1079
|
-
if (!m) {
|
|
1080
|
-
throw new Error(`Couldn't parse owner/repo from remote: ${remoteUrl}`);
|
|
1081
|
-
}
|
|
1082
|
-
repo = `${m[1]}/${m[2]}`;
|
|
1083
|
-
}
|
|
1084
|
-
catch (err) {
|
|
1085
|
-
throw new Error(`Couldn't read git remote.origin.url: ${err.message}`);
|
|
1086
|
-
}
|
|
1087
|
-
const title = explicitTitle ?? safeGit(() => git("log -1 --format=%s")) ?? `WIP: ${branch}`;
|
|
1088
|
-
const author = safeGit(() => git("config --get user.name"));
|
|
1089
|
-
const authorEmail = safeGit(() => git("config --get user.email"));
|
|
1090
|
-
// Post to backend.
|
|
1091
|
-
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/reverseTicketFromBranch", {
|
|
1092
|
-
config,
|
|
1093
|
-
body: {
|
|
1094
|
-
branch: {
|
|
1095
|
-
repo,
|
|
1096
|
-
branch,
|
|
1097
|
-
baseSha,
|
|
1098
|
-
headSha,
|
|
1099
|
-
title,
|
|
1100
|
-
author,
|
|
1101
|
-
authorEmail,
|
|
1102
|
-
filesChanged,
|
|
1103
|
-
diff: diff || undefined,
|
|
1104
|
-
stat: stat || undefined,
|
|
1105
|
-
},
|
|
1106
|
-
forcedArtifactKey: forcedKey,
|
|
1107
|
-
},
|
|
1108
|
-
});
|
|
1109
|
-
if (!res.ok || !res.artifactId) {
|
|
1110
|
-
return {
|
|
1111
|
-
isError: true,
|
|
1112
|
-
content: [
|
|
1113
|
-
{
|
|
1114
|
-
type: "text",
|
|
1115
|
-
text: "Reverse-ticket generation returned no result. The writer may have " +
|
|
1116
|
-
"skipped due to missing context (no captured sessions, etc.). Try " +
|
|
1117
|
-
"again after working on the branch a bit more, or check the " +
|
|
1118
|
-
"server logs.",
|
|
1119
|
-
},
|
|
1120
|
-
],
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
// Always return an absolute URL so MCP clients (Copilot Chat, Claude Code,
|
|
1124
|
-
// etc.) can render the link correctly without guessing a host. Default to
|
|
1125
|
-
// codiedev.com; allow override via CODIEDEV_PORTAL_URL for self-hosted.
|
|
1126
|
-
const portalHost = (process.env.CODIEDEV_PORTAL_URL || "https://codiedev.com").replace(/\/$/, "");
|
|
1127
|
-
const portalLink = res.portalUrl.startsWith("http")
|
|
1128
|
-
? res.portalUrl
|
|
1129
|
-
: `${portalHost}${res.portalUrl.startsWith("/") ? "" : "/"}${res.portalUrl}`;
|
|
1130
|
-
const verb = res.action === "overwritten" ? "Updated" : "Created";
|
|
1131
|
-
return {
|
|
1132
|
-
content: [
|
|
1133
|
-
{
|
|
1134
|
-
type: "text",
|
|
1135
|
-
text: `✓ ${verb} reverse-ticket draft for branch \`${branch}\` (${repo}).\n` +
|
|
1136
|
-
` files changed: ${filesChanged.length}\n` +
|
|
1137
|
-
` base: ${baseSha.slice(0, 12)} → head: ${headSha.slice(0, 12)} (${baseRef})\n` +
|
|
1138
|
-
` diff size: ${diff.length} chars\n` +
|
|
1139
|
-
` artifact: ${res.artifactKey ?? res.artifactId}\n\n` +
|
|
1140
|
-
`Open in your portal to review, edit, and push to Jira:\n` +
|
|
1141
|
-
` ${portalLink}\n\n` +
|
|
1142
|
-
`Each call creates a fresh draft — if you've made several for the ` +
|
|
1143
|
-
`same branch, clean them up from the portal.`,
|
|
1144
|
-
},
|
|
1145
|
-
],
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
// Run a git invocation; return undefined instead of throwing.
|
|
1149
|
-
function safeGit(fn) {
|
|
1150
|
-
try {
|
|
1151
|
-
return fn();
|
|
1152
|
-
}
|
|
1153
|
-
catch {
|
|
1154
|
-
return undefined;
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
820
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1158
821
|
// Helpers
|
|
1159
822
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/dist/utils.d.ts
CHANGED
|
@@ -147,10 +147,6 @@ export declare function installCursorMcp(): void;
|
|
|
147
147
|
* Claude / Cursor: top-level key is `servers` (not `mcpServers`). On first
|
|
148
148
|
* Copilot Chat run after install, VS Code prompts the user to trust the
|
|
149
149
|
* server — that's expected and documented in the connect success message.
|
|
150
|
-
*
|
|
151
|
-
* Reverse-ticket and the other on-demand verbs work in Copilot identically
|
|
152
|
-
* to Claude Code since the MCP server's reverse_ticket handler is already
|
|
153
|
-
* 100% diff-driven (no transcript dependency).
|
|
154
150
|
*/
|
|
155
151
|
export declare function installVSCodeMcp(): void;
|
|
156
152
|
/**
|
package/dist/utils.js
CHANGED
|
@@ -1502,14 +1502,12 @@ thought, use the \`codiedev\` CLI via Bash:**
|
|
|
1502
1502
|
| "any messages?" / "check my inbox" / "what did a teammate say?" | \`codiedev inbox\` |
|
|
1503
1503
|
| "read that reply" / "mark that ping read" | \`codiedev read <ping-id>\` |
|
|
1504
1504
|
| "note that X is a follow-up" / "remember X" | \`codiedev note "<text>"\` |
|
|
1505
|
-
| "promote the extracted spec" | \`codiedev promote <artifact-id>\` |
|
|
1506
1505
|
| "create a ticket" / "scope this into tickets" / "ticket this for me" | \`codiedev create-ticket [optional-inline-prompt]\` |
|
|
1507
1506
|
| "share with a teammate" / "give someone access" (no notification) | \`codiedev share <key> <name> [--role read\\|edit]\` |
|
|
1508
1507
|
| "send this to a teammate" / "ask someone to look at this" | \`codiedev send <key> <name> ["<msg>"]\` |
|
|
1509
1508
|
| "find something about X" / "has anyone solved Y?" | \`codiedev search "<query>" [--limit N]\` |
|
|
1510
1509
|
| "why did we…?" / "what does the team know about X?" | \`codiedev ask "<question>"\` (synthesized answer + citations) |
|
|
1511
1510
|
| "show my library" / "what artifacts exist?" | \`codiedev library [--scope mine\\|shared\\|all] [--kind doc\\|skill]\` |
|
|
1512
|
-
| "react 🛠 to that post" / "mark as used" | \`codiedev react <postId> <emoji>\` |
|
|
1513
1511
|
|
|
1514
1512
|
**Just push the file — we'll figure out where it goes.** Every artifact
|
|
1515
1513
|
is classified into one of two kinds at upload time:
|
|
@@ -1706,10 +1704,6 @@ function installCursorMcp() {
|
|
|
1706
1704
|
* Claude / Cursor: top-level key is `servers` (not `mcpServers`). On first
|
|
1707
1705
|
* Copilot Chat run after install, VS Code prompts the user to trust the
|
|
1708
1706
|
* server — that's expected and documented in the connect success message.
|
|
1709
|
-
*
|
|
1710
|
-
* Reverse-ticket and the other on-demand verbs work in Copilot identically
|
|
1711
|
-
* to Claude Code since the MCP server's reverse_ticket handler is already
|
|
1712
|
-
* 100% diff-driven (no transcript dependency).
|
|
1713
1707
|
*/
|
|
1714
1708
|
function installVSCodeMcp() {
|
|
1715
1709
|
if (!fs.existsSync(VSCODE_USER_DIR)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codiedev",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.66",
|
|
4
4
|
"description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codiedev": "dist/cli.js",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function runPromote(args: string[]): Promise<void>;
|
package/dist/commands/promote.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runPromote = runPromote;
|
|
4
|
-
const shared_1 = require("./shared");
|
|
5
|
-
function parseArgs(args) {
|
|
6
|
-
let artifactId;
|
|
7
|
-
let keyOverride;
|
|
8
|
-
for (let i = 0; i < args.length; i++) {
|
|
9
|
-
const a = args[i];
|
|
10
|
-
if ((a === "--key" || a === "-k") && i + 1 < args.length) {
|
|
11
|
-
keyOverride = args[++i];
|
|
12
|
-
}
|
|
13
|
-
else if (a.startsWith("--key=")) {
|
|
14
|
-
keyOverride = a.slice("--key=".length);
|
|
15
|
-
}
|
|
16
|
-
else if (!a.startsWith("--") && !artifactId) {
|
|
17
|
-
artifactId = a;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
if (!artifactId) {
|
|
21
|
-
console.error("Usage: codiedev promote <extracted-artifact-id> [--key spec-foo.md]");
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
24
|
-
return { artifactId, keyOverride };
|
|
25
|
-
}
|
|
26
|
-
async function runPromote(args) {
|
|
27
|
-
const { artifactId, keyOverride } = parseArgs(args);
|
|
28
|
-
const config = (0, shared_1.requireConfig)();
|
|
29
|
-
const start = Date.now();
|
|
30
|
-
try {
|
|
31
|
-
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/promote", {
|
|
32
|
-
config,
|
|
33
|
-
body: { artifactId, keyOverride },
|
|
34
|
-
});
|
|
35
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
36
|
-
tool: "codiedev_promote",
|
|
37
|
-
ok: true,
|
|
38
|
-
latencyMs: Date.now() - start,
|
|
39
|
-
});
|
|
40
|
-
console.log(`✓ Promoted to authored artifact ${res.key}`);
|
|
41
|
-
console.log(` id: ${res.artifactId}`);
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
45
|
-
tool: "codiedev_promote",
|
|
46
|
-
ok: false,
|
|
47
|
-
latencyMs: Date.now() - start,
|
|
48
|
-
error: err.message,
|
|
49
|
-
});
|
|
50
|
-
console.error(`Promote failed: ${err.message}`);
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
}
|
package/dist/commands/react.d.ts
DELETED
package/dist/commands/react.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// `codiedev react <postId> <emoji>` — toggle a reaction on a feed post.
|
|
3
|
-
// Calling twice with the same emoji removes it.
|
|
4
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
-
exports.runReact = runReact;
|
|
6
|
-
const shared_1 = require("./shared");
|
|
7
|
-
async function runReact(args, configOverride) {
|
|
8
|
-
const positionals = args.filter((a) => !a.startsWith("--"));
|
|
9
|
-
const [postId, emoji] = positionals;
|
|
10
|
-
if (!postId || !emoji) {
|
|
11
|
-
console.error("Usage: codiedev react <postId> <emoji>");
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
14
|
-
const config = configOverride ?? (0, shared_1.requireConfig)();
|
|
15
|
-
const start = Date.now();
|
|
16
|
-
try {
|
|
17
|
-
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/react", {
|
|
18
|
-
config,
|
|
19
|
-
body: { postId, emoji },
|
|
20
|
-
});
|
|
21
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
22
|
-
tool: "codiedev_react",
|
|
23
|
-
ok: true,
|
|
24
|
-
latencyMs: Date.now() - start,
|
|
25
|
-
});
|
|
26
|
-
console.log(res.action === "removed"
|
|
27
|
-
? `Removed ${res.emoji ?? emoji} from post ${postId}.`
|
|
28
|
-
: `Reacted to post ${postId} with ${res.emoji ?? emoji}.`);
|
|
29
|
-
}
|
|
30
|
-
catch (err) {
|
|
31
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
32
|
-
tool: "codiedev_react",
|
|
33
|
-
ok: false,
|
|
34
|
-
latencyMs: Date.now() - start,
|
|
35
|
-
error: err.message,
|
|
36
|
-
});
|
|
37
|
-
console.error(`React failed: ${err.message}`);
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
@@ -1,477 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isSafeRef = isSafeRef;
|
|
4
|
-
exports.runReverseTicket = runReverseTicket;
|
|
5
|
-
const child_process_1 = require("child_process");
|
|
6
|
-
const shared_1 = require("./shared");
|
|
7
|
-
const prUrl_1 = require("../prUrl");
|
|
8
|
-
/**
|
|
9
|
-
* Run a binary with an explicit argv, never a shell.
|
|
10
|
-
*
|
|
11
|
-
* The `gh`/`glab` API paths are built from an owner/repo parsed out of a URL the
|
|
12
|
-
* caller pastes in. `parsePrOrMrUrl` only bounds those segments to "no slash",
|
|
13
|
-
* so they can still carry `$(…)`, backticks or `;` — which a `shell: true`
|
|
14
|
-
* invocation would execute. Passing argv directly means the shell never sees
|
|
15
|
-
* them at all.
|
|
16
|
-
*/
|
|
17
|
-
function runArgv(bin, argv, opts = {}) {
|
|
18
|
-
const res = (0, child_process_1.spawnSync)(bin, argv, {
|
|
19
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
20
|
-
encoding: "utf8",
|
|
21
|
-
maxBuffer: opts.maxBuffer ?? 50 * 1024 * 1024,
|
|
22
|
-
shell: false,
|
|
23
|
-
});
|
|
24
|
-
if (res.error)
|
|
25
|
-
throw res.error;
|
|
26
|
-
if (res.status !== 0) {
|
|
27
|
-
throw new Error(`${bin} ${argv.join(" ")} failed (exit ${res.status}): ${(res.stderr || "").trim().slice(0, 500)}`);
|
|
28
|
-
}
|
|
29
|
-
return res.stdout ?? "";
|
|
30
|
-
}
|
|
31
|
-
// Git ref names legally contain `$()`, `;`, `|` and backticks, all of which
|
|
32
|
-
// break out of the `shell: true` git calls in branch mode below. A ref supplied
|
|
33
|
-
// via `--base` is caller-controlled, so bound it to the safe subset before it
|
|
34
|
-
// reaches a command line. Mirrors `validate_ref` in modal/runner.py.
|
|
35
|
-
const SAFE_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,255}$/;
|
|
36
|
-
function isSafeRef(ref) {
|
|
37
|
-
if (!ref || ref.startsWith("-"))
|
|
38
|
-
return false;
|
|
39
|
-
if (ref.includes("..") || ref.includes("@{"))
|
|
40
|
-
return false;
|
|
41
|
-
if (ref.endsWith("/") || ref.endsWith("."))
|
|
42
|
-
return false;
|
|
43
|
-
return SAFE_REF_RE.test(ref);
|
|
44
|
-
}
|
|
45
|
-
async function fetchPrOrMrBundle(parsed, rawUrl, noTruncate) {
|
|
46
|
-
console.log(`Fetching ${parsed.provider === "gitlab" ? "MR" : "PR"} ${parsed.owner}/${parsed.repo}#${parsed.number}…`);
|
|
47
|
-
if (parsed.provider === "github") {
|
|
48
|
-
let prJson;
|
|
49
|
-
let filesJson;
|
|
50
|
-
let diff;
|
|
51
|
-
try {
|
|
52
|
-
prJson = ghApi(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}`);
|
|
53
|
-
filesJson = ghApi(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}/files`);
|
|
54
|
-
try {
|
|
55
|
-
const rawDiff = ghRaw(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}`);
|
|
56
|
-
diff = noTruncate ? rawDiff : rawDiff.slice(0, 12_000);
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
// diff is nice-to-have
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
catch (err) {
|
|
63
|
-
console.error(`Failed to fetch PR via gh api: ${err.message}`);
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
repo: `${parsed.owner}/${parsed.repo}`,
|
|
68
|
-
number: parsed.number,
|
|
69
|
-
title: prJson.title ?? "",
|
|
70
|
-
body: prJson.body ?? "",
|
|
71
|
-
author: prJson.user?.login ?? "unknown",
|
|
72
|
-
authorEmail: prJson.user?.email ?? undefined,
|
|
73
|
-
mergedAt: prJson.merged_at
|
|
74
|
-
? Date.parse(prJson.merged_at)
|
|
75
|
-
: prJson.closed_at
|
|
76
|
-
? Date.parse(prJson.closed_at)
|
|
77
|
-
: Date.now(),
|
|
78
|
-
filesChanged: (filesJson ?? []).map((f) => f.filename),
|
|
79
|
-
diff,
|
|
80
|
-
htmlUrl: prJson.html_url ?? rawUrl,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
// GitLab path. `glab api projects/<encoded-path>/...` returns the same
|
|
84
|
-
// MR shape as the REST API. We pull the diff from `merge_requests/<n>/changes`
|
|
85
|
-
// and reassemble a unified diff out of each file's `diff` blob.
|
|
86
|
-
const projectPath = `${parsed.owner}/${parsed.repo}`;
|
|
87
|
-
let mrJson;
|
|
88
|
-
let changesJson;
|
|
89
|
-
try {
|
|
90
|
-
mrJson = glabApi(projectPath, `merge_requests/${parsed.number}`);
|
|
91
|
-
changesJson = glabApi(projectPath, `merge_requests/${parsed.number}/changes`);
|
|
92
|
-
}
|
|
93
|
-
catch (err) {
|
|
94
|
-
console.error(`Failed to fetch MR via glab api: ${err.message}`);
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
const changes = (changesJson?.changes ?? []);
|
|
98
|
-
const filesChanged = changes
|
|
99
|
-
.map((c) => c.new_path ?? c.old_path)
|
|
100
|
-
.filter((p) => Boolean(p));
|
|
101
|
-
const rawDiff = changes
|
|
102
|
-
.map((c) => {
|
|
103
|
-
const header = `diff --git a/${c.old_path ?? c.new_path} b/${c.new_path ?? c.old_path}`;
|
|
104
|
-
return [header, c.diff ?? ""].join("\n");
|
|
105
|
-
})
|
|
106
|
-
.join("\n");
|
|
107
|
-
const diff = noTruncate ? rawDiff : rawDiff.slice(0, 12_000);
|
|
108
|
-
return {
|
|
109
|
-
repo: projectPath,
|
|
110
|
-
number: parsed.number,
|
|
111
|
-
title: mrJson.title ?? "",
|
|
112
|
-
body: mrJson.description ?? "",
|
|
113
|
-
author: mrJson.author?.username ?? "unknown",
|
|
114
|
-
authorEmail: undefined,
|
|
115
|
-
mergedAt: mrJson.merged_at
|
|
116
|
-
? Date.parse(mrJson.merged_at)
|
|
117
|
-
: mrJson.closed_at
|
|
118
|
-
? Date.parse(mrJson.closed_at)
|
|
119
|
-
: Date.now(),
|
|
120
|
-
filesChanged,
|
|
121
|
-
diff,
|
|
122
|
-
htmlUrl: mrJson.web_url ?? rawUrl,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function checkCli(bin) {
|
|
126
|
-
try {
|
|
127
|
-
runArgv(bin, ["--version"], { maxBuffer: 1024 * 1024 });
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
function ghApi(path) {
|
|
135
|
-
return JSON.parse(runArgv("gh", ["api", path]));
|
|
136
|
-
}
|
|
137
|
-
function ghRaw(path) {
|
|
138
|
-
return runArgv("gh", [
|
|
139
|
-
"api",
|
|
140
|
-
"-H",
|
|
141
|
-
"Accept: application/vnd.github.v3.diff",
|
|
142
|
-
path,
|
|
143
|
-
]);
|
|
144
|
-
}
|
|
145
|
-
// glab api takes a project path (URL-encoded) or numeric id and a sub-path.
|
|
146
|
-
// For self-hosted GitLab, glab respects `GITLAB_HOST` env if the user has
|
|
147
|
-
// configured glab against their internal instance.
|
|
148
|
-
function glabApi(projectPath, subpath) {
|
|
149
|
-
const encoded = encodeURIComponent(projectPath);
|
|
150
|
-
return JSON.parse(runArgv("glab", ["api", `projects/${encoded}/${subpath}`]));
|
|
151
|
-
}
|
|
152
|
-
function parseArgs(args) {
|
|
153
|
-
let prUrl;
|
|
154
|
-
let base;
|
|
155
|
-
let forcedKey;
|
|
156
|
-
let noTruncate = false;
|
|
157
|
-
for (let i = 0; i < args.length; i++) {
|
|
158
|
-
const a = args[i];
|
|
159
|
-
if ((a === "--with" || a === "-w") && i + 1 < args.length) {
|
|
160
|
-
forcedKey = args[++i];
|
|
161
|
-
}
|
|
162
|
-
else if (a.startsWith("--with=")) {
|
|
163
|
-
forcedKey = a.slice("--with=".length);
|
|
164
|
-
}
|
|
165
|
-
else if ((a === "--base" || a === "-b") && i + 1 < args.length) {
|
|
166
|
-
base = args[++i];
|
|
167
|
-
}
|
|
168
|
-
else if (a.startsWith("--base=")) {
|
|
169
|
-
base = a.slice("--base=".length);
|
|
170
|
-
}
|
|
171
|
-
else if (a === "--full") {
|
|
172
|
-
noTruncate = true;
|
|
173
|
-
}
|
|
174
|
-
else if (!a.startsWith("--") && !prUrl) {
|
|
175
|
-
prUrl = a;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return { prUrl, base, forcedKey, noTruncate };
|
|
179
|
-
}
|
|
180
|
-
async function runReverseTicket(args) {
|
|
181
|
-
const { prUrl, base, forcedKey, noTruncate } = parseArgs(args);
|
|
182
|
-
// No PR URL → branch mode (mid-session, no PR exists yet).
|
|
183
|
-
if (!prUrl) {
|
|
184
|
-
return runBranchMode({ explicitBase: base, forcedKey });
|
|
185
|
-
}
|
|
186
|
-
const parsed = (0, prUrl_1.parsePrOrMrUrl)(prUrl);
|
|
187
|
-
if (!parsed) {
|
|
188
|
-
console.error("Couldn't parse PR/MR URL. Expected formats:");
|
|
189
|
-
console.error(" https://github.com/<owner>/<repo>/pull/<number>");
|
|
190
|
-
console.error(" https://gitlab.com/<group>/<repo>/-/merge_requests/<number>");
|
|
191
|
-
console.error(" (self-hosted GitLab works too — same shape, your host)");
|
|
192
|
-
console.error("");
|
|
193
|
-
console.error("Run with no arguments for branch mode (uses your current local branch).");
|
|
194
|
-
process.exit(1);
|
|
195
|
-
}
|
|
196
|
-
const requiredCli = parsed.provider === "gitlab" ? "glab" : "gh";
|
|
197
|
-
if (!checkCli(requiredCli)) {
|
|
198
|
-
console.error(`This command needs the \`${requiredCli}\` CLI installed and authenticated.`);
|
|
199
|
-
if (requiredCli === "gh") {
|
|
200
|
-
console.error(" brew install gh (macOS)");
|
|
201
|
-
console.error(" gh auth login");
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
console.error(" brew install glab (macOS)");
|
|
205
|
-
console.error(" glab auth login");
|
|
206
|
-
}
|
|
207
|
-
process.exit(1);
|
|
208
|
-
}
|
|
209
|
-
const config = (0, shared_1.requireConfig)();
|
|
210
|
-
const bundle = await fetchPrOrMrBundle(parsed, prUrl, noTruncate);
|
|
211
|
-
console.log(` title: ${bundle.title}`);
|
|
212
|
-
console.log(` author: ${bundle.author}`);
|
|
213
|
-
console.log(` files: ${bundle.filesChanged.length}`);
|
|
214
|
-
console.log(` merged: ${new Date(bundle.mergedAt).toISOString()}`);
|
|
215
|
-
console.log("");
|
|
216
|
-
console.log("Generating ticket…");
|
|
217
|
-
console.log("");
|
|
218
|
-
const start = Date.now();
|
|
219
|
-
try {
|
|
220
|
-
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/reverseTicket", {
|
|
221
|
-
config,
|
|
222
|
-
body: {
|
|
223
|
-
pr: bundle,
|
|
224
|
-
forcedArtifactKey: forcedKey,
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
228
|
-
tool: "codiedev_reverse_ticket",
|
|
229
|
-
ok: true,
|
|
230
|
-
latencyMs: Date.now() - start,
|
|
231
|
-
});
|
|
232
|
-
if (res.match) {
|
|
233
|
-
console.log(`✓ Matched artifact: ${res.match.key} (score=${res.match.score.toFixed(2)})`);
|
|
234
|
-
console.log(` signals · body=${res.match.signals.bodyMention} · files=${res.match.signals.fileOverlap.toFixed(2)} · transcript=${res.match.signals.transcriptEdit}`);
|
|
235
|
-
}
|
|
236
|
-
else if (res.candidates.length > 0) {
|
|
237
|
-
console.log(`? Ambiguous / low confidence. Candidates:`);
|
|
238
|
-
for (const c of res.candidates.slice(0, 3)) {
|
|
239
|
-
console.log(` - ${c.key} (score=${c.score.toFixed(2)})`);
|
|
240
|
-
}
|
|
241
|
-
console.log(` Re-run with --with <key> to force a specific artifact.`);
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
console.log("· No matched artifact. Ticket generated from PR diff alone.");
|
|
245
|
-
}
|
|
246
|
-
console.log(` prompt tokens (est): ${res.tokensInEstimate}`);
|
|
247
|
-
console.log("");
|
|
248
|
-
console.log("─────────── Generated ticket ───────────");
|
|
249
|
-
console.log("");
|
|
250
|
-
console.log(res.ticket);
|
|
251
|
-
}
|
|
252
|
-
catch (err) {
|
|
253
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
254
|
-
tool: "codiedev_reverse_ticket",
|
|
255
|
-
ok: false,
|
|
256
|
-
latencyMs: Date.now() - start,
|
|
257
|
-
error: err.message,
|
|
258
|
-
});
|
|
259
|
-
console.error(`Reverse-ticket failed: ${err.message}`);
|
|
260
|
-
process.exit(1);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
function git(cmd) {
|
|
264
|
-
try {
|
|
265
|
-
return (0, child_process_1.execSync)(`git ${cmd}`, {
|
|
266
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
267
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
268
|
-
})
|
|
269
|
-
.toString("utf8")
|
|
270
|
-
.trim();
|
|
271
|
-
}
|
|
272
|
-
catch (err) {
|
|
273
|
-
throw new Error(`git ${cmd} failed: ${err.message}`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
function safeGit(fn) {
|
|
277
|
-
try {
|
|
278
|
-
return fn() || undefined;
|
|
279
|
-
}
|
|
280
|
-
catch {
|
|
281
|
-
return undefined;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
async function runBranchMode(opts) {
|
|
285
|
-
// Confirm we're in a git repo.
|
|
286
|
-
try {
|
|
287
|
-
git("rev-parse --is-inside-work-tree");
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
console.error("Not inside a git repository. Run this from a checked-out repo.");
|
|
291
|
-
process.exit(1);
|
|
292
|
-
}
|
|
293
|
-
// `--base` lands unquoted on a `git` command line below, so reject anything
|
|
294
|
-
// outside the safe-ref subset instead of letting the shell interpret it.
|
|
295
|
-
if (opts.explicitBase !== undefined && !isSafeRef(opts.explicitBase)) {
|
|
296
|
-
console.error(`Invalid --base ref: ${JSON.stringify(opts.explicitBase)}. Expected a plain branch or tag name.`);
|
|
297
|
-
process.exit(1);
|
|
298
|
-
}
|
|
299
|
-
// Resolve base ref — explicit, then origin/main, then origin/master.
|
|
300
|
-
const candidates = opts.explicitBase
|
|
301
|
-
? [opts.explicitBase]
|
|
302
|
-
: ["origin/main", "origin/master"];
|
|
303
|
-
let baseRef = null;
|
|
304
|
-
for (const c of candidates) {
|
|
305
|
-
try {
|
|
306
|
-
git(`rev-parse --verify ${c}`);
|
|
307
|
-
baseRef = c;
|
|
308
|
-
break;
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
// try next
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
if (!baseRef) {
|
|
315
|
-
console.error(`Couldn't find a base ref (tried ${candidates.join(", ")}). Pass --base <ref>.`);
|
|
316
|
-
process.exit(1);
|
|
317
|
-
}
|
|
318
|
-
const branch = git("rev-parse --abbrev-ref HEAD");
|
|
319
|
-
if (branch === "HEAD") {
|
|
320
|
-
console.error("Detached HEAD — checkout a branch first.");
|
|
321
|
-
process.exit(1);
|
|
322
|
-
}
|
|
323
|
-
const baseSha = git(`merge-base HEAD ${baseRef}`);
|
|
324
|
-
const headSha = git("rev-parse HEAD");
|
|
325
|
-
// Filter generated/lockfile noise from the diff body. The --stat (collected
|
|
326
|
-
// separately) still includes every file untouched, so excluded files still
|
|
327
|
-
// surface in the ticket's "Other files touched" section.
|
|
328
|
-
const isNoise = (p) => /(^|\/)(node_modules|dist|build|\.next|\.turbo|coverage|out)\//.test(p) ||
|
|
329
|
-
/(^|\/)package-lock\.json$/.test(p) ||
|
|
330
|
-
/(^|\/)yarn\.lock$/.test(p) ||
|
|
331
|
-
/(^|\/)pnpm-lock\.yaml$/.test(p) ||
|
|
332
|
-
/(^|\/)bun\.lock(b)?$/.test(p) ||
|
|
333
|
-
/(^|\/)Cargo\.lock$/.test(p) ||
|
|
334
|
-
/(^|\/)Gemfile\.lock$/.test(p) ||
|
|
335
|
-
/(^|\/)poetry\.lock$/.test(p) ||
|
|
336
|
-
/(^|\/)go\.sum$/.test(p);
|
|
337
|
-
const shellTolerant = (cmd) => {
|
|
338
|
-
try {
|
|
339
|
-
return (0, child_process_1.execSync)(`${cmd} || true`, {
|
|
340
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
341
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
342
|
-
})
|
|
343
|
-
.toString("utf8")
|
|
344
|
-
.trim();
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
return "";
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
const shQuote = (s) => `'${s.replace(/'/g, "'\\''")}'`;
|
|
351
|
-
// Capture all work relative to the base ref: committed + uncommitted-tracked
|
|
352
|
-
// + new untracked. Matches "ticket my current changes" intent.
|
|
353
|
-
const trackedRaw = git(`diff --name-only ${baseSha}`);
|
|
354
|
-
const trackedFiles = trackedRaw ? trackedRaw.split("\n").filter(Boolean) : [];
|
|
355
|
-
const untrackedRaw = safeGit(() => git("ls-files --others --exclude-standard"));
|
|
356
|
-
const untrackedFiles = untrackedRaw
|
|
357
|
-
? untrackedRaw.split("\n").filter(Boolean)
|
|
358
|
-
: [];
|
|
359
|
-
const filesChanged = Array.from(new Set([...trackedFiles, ...untrackedFiles]));
|
|
360
|
-
if (filesChanged.length === 0) {
|
|
361
|
-
console.error(`No changes detected between ${baseRef} and your working tree on ${branch}.`);
|
|
362
|
-
process.exit(1);
|
|
363
|
-
}
|
|
364
|
-
let stat = "";
|
|
365
|
-
try {
|
|
366
|
-
const trackedStat = git(`diff --stat=200 ${baseSha}`);
|
|
367
|
-
const untrackedStatLines = untrackedFiles.map((f) => {
|
|
368
|
-
const wc = shellTolerant(`wc -l < ${shQuote(f)}`);
|
|
369
|
-
const n = parseInt(wc.split(/\s+/)[0] || "0", 10) || 0;
|
|
370
|
-
return ` ${f} | ${n} ++++++++++ (new file)`;
|
|
371
|
-
});
|
|
372
|
-
stat = [trackedStat, ...untrackedStatLines]
|
|
373
|
-
.filter(Boolean)
|
|
374
|
-
.join("\n")
|
|
375
|
-
.slice(0, 8_000);
|
|
376
|
-
}
|
|
377
|
-
catch {
|
|
378
|
-
// best effort
|
|
379
|
-
}
|
|
380
|
-
let diff = "";
|
|
381
|
-
try {
|
|
382
|
-
const sourceTracked = trackedFiles.filter((f) => !isNoise(f));
|
|
383
|
-
const trackedDiff = sourceTracked.length > 0
|
|
384
|
-
? git(`diff ${baseSha} -- ${sourceTracked.map(shQuote).join(" ")}`)
|
|
385
|
-
: git(`diff ${baseSha}`);
|
|
386
|
-
const sourceUntracked = untrackedFiles.filter((f) => !isNoise(f));
|
|
387
|
-
const untrackedDiffs = sourceUntracked.map((f) => shellTolerant(`git diff --no-index --no-color -- /dev/null ${shQuote(f)}`));
|
|
388
|
-
diff = [trackedDiff, ...untrackedDiffs]
|
|
389
|
-
.filter(Boolean)
|
|
390
|
-
.join("\n")
|
|
391
|
-
.slice(0, 40_000);
|
|
392
|
-
}
|
|
393
|
-
catch {
|
|
394
|
-
// best effort
|
|
395
|
-
}
|
|
396
|
-
let repo;
|
|
397
|
-
try {
|
|
398
|
-
const remoteUrl = git("config --get remote.origin.url");
|
|
399
|
-
const m = remoteUrl.match(/[:/]([^/:]+)\/([^/:]+?)(?:\.git)?$/);
|
|
400
|
-
if (!m) {
|
|
401
|
-
throw new Error(`Couldn't parse owner/repo from remote: ${remoteUrl}`);
|
|
402
|
-
}
|
|
403
|
-
repo = `${m[1]}/${m[2]}`;
|
|
404
|
-
}
|
|
405
|
-
catch (err) {
|
|
406
|
-
console.error(`Couldn't read git remote.origin.url: ${err.message}`);
|
|
407
|
-
process.exit(1);
|
|
408
|
-
}
|
|
409
|
-
const title = safeGit(() => git("log -1 --format=%s")) ?? `WIP: ${branch}`;
|
|
410
|
-
const author = safeGit(() => git("config --get user.name"));
|
|
411
|
-
const authorEmail = safeGit(() => git("config --get user.email"));
|
|
412
|
-
console.log(`Branch: ${branch} (${repo})`);
|
|
413
|
-
console.log(`Base: ${baseRef} @ ${baseSha.slice(0, 12)}`);
|
|
414
|
-
console.log(`Head: ${headSha.slice(0, 12)}`);
|
|
415
|
-
console.log(`Files: ${filesChanged.length}`);
|
|
416
|
-
console.log("");
|
|
417
|
-
console.log("Generating ticket draft…");
|
|
418
|
-
console.log("");
|
|
419
|
-
const config = (0, shared_1.requireConfig)();
|
|
420
|
-
const start = Date.now();
|
|
421
|
-
try {
|
|
422
|
-
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/reverseTicketFromBranch", {
|
|
423
|
-
config,
|
|
424
|
-
body: {
|
|
425
|
-
branch: {
|
|
426
|
-
repo,
|
|
427
|
-
branch,
|
|
428
|
-
baseSha,
|
|
429
|
-
headSha,
|
|
430
|
-
title,
|
|
431
|
-
author,
|
|
432
|
-
authorEmail,
|
|
433
|
-
filesChanged,
|
|
434
|
-
diff: diff || undefined,
|
|
435
|
-
stat: stat || undefined,
|
|
436
|
-
},
|
|
437
|
-
forcedArtifactKey: opts.forcedKey,
|
|
438
|
-
},
|
|
439
|
-
});
|
|
440
|
-
if (!res.ok || !res.artifactId) {
|
|
441
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
442
|
-
tool: "codiedev_reverse_ticket",
|
|
443
|
-
ok: false,
|
|
444
|
-
latencyMs: Date.now() - start,
|
|
445
|
-
error: "no result",
|
|
446
|
-
});
|
|
447
|
-
console.error("Reverse-ticket generation returned no result. The writer may have skipped due to missing context.");
|
|
448
|
-
process.exit(1);
|
|
449
|
-
}
|
|
450
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
451
|
-
tool: "codiedev_reverse_ticket",
|
|
452
|
-
ok: true,
|
|
453
|
-
latencyMs: Date.now() - start,
|
|
454
|
-
});
|
|
455
|
-
const portalHost = (process.env.CODIEDEV_PORTAL_URL || "https://codiedev.com").replace(/\/$/, "");
|
|
456
|
-
const portalLink = res.portalUrl.startsWith("http")
|
|
457
|
-
? res.portalUrl
|
|
458
|
-
: `${portalHost}${res.portalUrl.startsWith("/") ? "" : "/"}${res.portalUrl}`;
|
|
459
|
-
const verb = res.action === "overwritten" ? "Updated" : "Created";
|
|
460
|
-
console.log(`✓ ${verb} reverse-ticket draft.`);
|
|
461
|
-
console.log(` artifact: ${res.artifactKey ?? res.artifactId}`);
|
|
462
|
-
console.log(` portal: ${portalLink}`);
|
|
463
|
-
console.log("");
|
|
464
|
-
console.log("Open in your portal to review, edit, and push to Jira.");
|
|
465
|
-
console.log("Each call creates a fresh draft — clean up duplicates from the portal if needed.");
|
|
466
|
-
}
|
|
467
|
-
catch (err) {
|
|
468
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
469
|
-
tool: "codiedev_reverse_ticket",
|
|
470
|
-
ok: false,
|
|
471
|
-
latencyMs: Date.now() - start,
|
|
472
|
-
error: err.message,
|
|
473
|
-
});
|
|
474
|
-
console.error(`Reverse-ticket failed: ${err.message}`);
|
|
475
|
-
process.exit(1);
|
|
476
|
-
}
|
|
477
|
-
}
|
package/dist/prUrl.d.ts
DELETED
package/dist/prUrl.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// PR/MR URL parser. GitHub uses `/pull/<n>`, GitLab uses `/-/merge_requests/<n>`.
|
|
3
|
-
// Self-hosted GitLab keeps the same path shape on a different host (Sonifi
|
|
4
|
-
// runs git.sonifi.com behind a Cloudflare tunnel, for example). Nested groups
|
|
5
|
-
// land in `owner` as a slash-joined path so we can rebuild the full project
|
|
6
|
-
// path when calling the GitLab API.
|
|
7
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.parsePrOrMrUrl = parsePrOrMrUrl;
|
|
9
|
-
function parsePrOrMrUrl(url) {
|
|
10
|
-
if (!url)
|
|
11
|
-
return null;
|
|
12
|
-
const gh = url.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:[/?#]|$)/);
|
|
13
|
-
if (gh && /github/i.test(gh[1])) {
|
|
14
|
-
return {
|
|
15
|
-
provider: "github",
|
|
16
|
-
host: gh[1],
|
|
17
|
-
owner: gh[2],
|
|
18
|
-
repo: gh[3],
|
|
19
|
-
number: Number(gh[4]),
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
const gl = url.match(/^https?:\/\/([^/]+)\/(.+)\/([^/]+)\/-\/merge_requests\/(\d+)(?:[/?#]|$)/);
|
|
23
|
-
if (gl) {
|
|
24
|
-
return {
|
|
25
|
-
provider: "gitlab",
|
|
26
|
-
host: gl[1],
|
|
27
|
-
owner: gl[2],
|
|
28
|
-
repo: gl[3],
|
|
29
|
-
number: Number(gl[4]),
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
33
|
-
}
|