@locusai/locus-telegram 0.21.10 → 0.21.11
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 +1 -1
- package/package.json +8 -2
- package/CHANGELOG.md +0 -34
- package/src/bot.ts +0 -256
- package/src/commands/git.ts +0 -309
- package/src/commands/locus.ts +0 -232
- package/src/commands/service.ts +0 -89
- package/src/config.ts +0 -78
- package/src/index.ts +0 -169
- package/src/pm2.ts +0 -138
- package/src/ui/format.ts +0 -130
- package/src/ui/keyboards.ts +0 -83
- package/src/ui/messages.ts +0 -189
- package/tsconfig.json +0 -17
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@locusai/locus-telegram",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.11",
|
|
4
4
|
"description": "Remote-control Locus via Telegram with full CLI mapping, git operations, and PM2 management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"locus-telegram": "./bin/locus-telegram.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"dist",
|
|
12
|
+
"package.json",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
9
15
|
"locus": {
|
|
10
16
|
"displayName": "Telegram",
|
|
11
17
|
"description": "Remote-control your Locus agent from Telegram",
|
|
@@ -21,7 +27,7 @@
|
|
|
21
27
|
"format": "biome format --write ."
|
|
22
28
|
},
|
|
23
29
|
"dependencies": {
|
|
24
|
-
"@locusai/sdk": "^0.21.
|
|
30
|
+
"@locusai/sdk": "^0.21.11",
|
|
25
31
|
"grammy": "^1.35.0",
|
|
26
32
|
"pm2": "^6.0.5"
|
|
27
33
|
},
|
package/CHANGELOG.md
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# @locusai/locus-telegram
|
|
2
|
-
|
|
3
|
-
## 0.21.10
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- Package resolution
|
|
8
|
-
- Updated dependencies
|
|
9
|
-
- @locusai/sdk@0.21.10
|
|
10
|
-
|
|
11
|
-
## 0.21.9
|
|
12
|
-
|
|
13
|
-
### Patch Changes
|
|
14
|
-
|
|
15
|
-
- Locus package names convention fixes
|
|
16
|
-
- Updated dependencies
|
|
17
|
-
- @locusai/sdk@0.21.9
|
|
18
|
-
|
|
19
|
-
## 0.21.8
|
|
20
|
-
|
|
21
|
-
### Patch Changes
|
|
22
|
-
|
|
23
|
-
- Continious learning improvements
|
|
24
|
-
Fix Telegram package installation issue
|
|
25
|
-
- Updated dependencies
|
|
26
|
-
- @locusai/sdk@0.21.8
|
|
27
|
-
|
|
28
|
-
## 0.21.7
|
|
29
|
-
|
|
30
|
-
### Patch Changes
|
|
31
|
-
|
|
32
|
-
- Initial telegram package
|
|
33
|
-
- Updated dependencies
|
|
34
|
-
- @locusai/sdk@0.21.7
|
package/src/bot.ts
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bot instance — sets up grammy bot, registers middleware, commands,
|
|
3
|
-
* and callback query handlers.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createLogger } from "@locusai/sdk";
|
|
7
|
-
import { Bot } from "grammy";
|
|
8
|
-
import {
|
|
9
|
-
handleBranch,
|
|
10
|
-
handleCheckout,
|
|
11
|
-
handleCommit,
|
|
12
|
-
handleDiff,
|
|
13
|
-
handleGitStatus,
|
|
14
|
-
handlePR,
|
|
15
|
-
handleStage,
|
|
16
|
-
handleStash,
|
|
17
|
-
} from "./commands/git.js";
|
|
18
|
-
import { handleLocusCommand } from "./commands/locus.js";
|
|
19
|
-
import { handleService } from "./commands/service.js";
|
|
20
|
-
import type { TelegramConfig } from "./config.js";
|
|
21
|
-
import { formatInfo, formatSuccess } from "./ui/format.js";
|
|
22
|
-
import { CB } from "./ui/keyboards.js";
|
|
23
|
-
import { welcomeMessage } from "./ui/messages.js";
|
|
24
|
-
|
|
25
|
-
const logger = createLogger("telegram");
|
|
26
|
-
|
|
27
|
-
// ─── Bot Factory ────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
export function createBot(config: TelegramConfig): Bot {
|
|
30
|
-
const bot = new Bot(config.botToken);
|
|
31
|
-
|
|
32
|
-
// ── Auth Middleware ─────────────────────────────────────────────────────
|
|
33
|
-
bot.use(async (ctx, next) => {
|
|
34
|
-
const chatId = ctx.chat?.id;
|
|
35
|
-
if (!chatId || !config.allowedChatIds.includes(chatId)) {
|
|
36
|
-
logger.warn("Unauthorized access attempt", {
|
|
37
|
-
chatId,
|
|
38
|
-
from: ctx.from?.username,
|
|
39
|
-
});
|
|
40
|
-
return; // silently ignore
|
|
41
|
-
}
|
|
42
|
-
await next();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// ── Help / Start ────────────────────────────────────────────────────────
|
|
46
|
-
bot.command("start", async (ctx) => {
|
|
47
|
-
await ctx.reply(welcomeMessage(), { parse_mode: "HTML" });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
bot.command("help", async (ctx) => {
|
|
51
|
-
await ctx.reply(welcomeMessage(), { parse_mode: "HTML" });
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// ── Locus CLI Commands ──────────────────────────────────────────────────
|
|
55
|
-
const locusCommands = [
|
|
56
|
-
"run",
|
|
57
|
-
"status",
|
|
58
|
-
"issues",
|
|
59
|
-
"issue",
|
|
60
|
-
"sprint",
|
|
61
|
-
"plan",
|
|
62
|
-
"review",
|
|
63
|
-
"iterate",
|
|
64
|
-
"discuss",
|
|
65
|
-
"exec",
|
|
66
|
-
"logs",
|
|
67
|
-
"config",
|
|
68
|
-
"artifacts",
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
for (const cmd of locusCommands) {
|
|
72
|
-
bot.command(cmd, async (ctx) => {
|
|
73
|
-
const args = parseArgs(ctx.message?.text ?? "", cmd);
|
|
74
|
-
await handleLocusCommand(ctx, cmd, args);
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ── Git Commands ────────────────────────────────────────────────────────
|
|
79
|
-
bot.command("gitstatus", async (ctx) => {
|
|
80
|
-
await handleGitStatus(ctx);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
bot.command("stage", async (ctx) => {
|
|
84
|
-
const args = parseArgs(ctx.message?.text ?? "", "stage");
|
|
85
|
-
await handleStage(ctx, args);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
bot.command("commit", async (ctx) => {
|
|
89
|
-
const args = parseArgs(ctx.message?.text ?? "", "commit");
|
|
90
|
-
await handleCommit(ctx, args);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
bot.command("stash", async (ctx) => {
|
|
94
|
-
const args = parseArgs(ctx.message?.text ?? "", "stash");
|
|
95
|
-
await handleStash(ctx, args);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
bot.command("branch", async (ctx) => {
|
|
99
|
-
const args = parseArgs(ctx.message?.text ?? "", "branch");
|
|
100
|
-
await handleBranch(ctx, args);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
bot.command("checkout", async (ctx) => {
|
|
104
|
-
const args = parseArgs(ctx.message?.text ?? "", "checkout");
|
|
105
|
-
await handleCheckout(ctx, args);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
bot.command("diff", async (ctx) => {
|
|
109
|
-
await handleDiff(ctx);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
bot.command("pr", async (ctx) => {
|
|
113
|
-
const args = parseArgs(ctx.message?.text ?? "", "pr");
|
|
114
|
-
await handlePR(ctx, args);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// ── Service Commands ────────────────────────────────────────────────────
|
|
118
|
-
bot.command("service", async (ctx) => {
|
|
119
|
-
const args = parseArgs(ctx.message?.text ?? "", "service");
|
|
120
|
-
await handleService(ctx, args);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// ── Callback Query Handlers ─────────────────────────────────────────────
|
|
124
|
-
registerCallbackHandlers(bot);
|
|
125
|
-
|
|
126
|
-
// ── Fallback ────────────────────────────────────────────────────────────
|
|
127
|
-
bot.on("message:text", async (ctx) => {
|
|
128
|
-
// Treat non-command text as a locus exec prompt
|
|
129
|
-
const text = ctx.message.text;
|
|
130
|
-
if (text.startsWith("/")) return; // unknown command — ignore
|
|
131
|
-
|
|
132
|
-
await handleLocusCommand(ctx, "exec", [text]);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
return bot;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ─── Callback Query Handlers ────────────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
function registerCallbackHandlers(bot: Bot): void {
|
|
141
|
-
// Plan callbacks
|
|
142
|
-
bot.callbackQuery(CB.APPROVE_PLAN, async (ctx) => {
|
|
143
|
-
await ctx.answerCallbackQuery({ text: "Plan approved!" });
|
|
144
|
-
await ctx.editMessageReplyMarkup({ reply_markup: undefined });
|
|
145
|
-
await ctx.reply(formatSuccess("Plan approved. Starting execution..."), {
|
|
146
|
-
parse_mode: "HTML",
|
|
147
|
-
});
|
|
148
|
-
// Trigger the run
|
|
149
|
-
await handleLocusCommand(ctx, "run", []);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
bot.callbackQuery(CB.REJECT_PLAN, async (ctx) => {
|
|
153
|
-
await ctx.answerCallbackQuery({ text: "Plan rejected." });
|
|
154
|
-
await ctx.editMessageReplyMarkup({ reply_markup: undefined });
|
|
155
|
-
await ctx.reply(formatInfo("Plan rejected. No changes will be made."), {
|
|
156
|
-
parse_mode: "HTML",
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
bot.callbackQuery(CB.SHOW_PLAN_DETAILS, async (ctx) => {
|
|
161
|
-
await ctx.answerCallbackQuery();
|
|
162
|
-
await handleLocusCommand(ctx, "plan", ["--show"]);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Run callbacks
|
|
166
|
-
bot.callbackQuery(CB.VIEW_LOGS, async (ctx) => {
|
|
167
|
-
await ctx.answerCallbackQuery();
|
|
168
|
-
await handleLocusCommand(ctx, "logs", ["--lines", "30"]);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
bot.callbackQuery(/^run:again:(\d+)$/, async (ctx) => {
|
|
172
|
-
const issue = ctx.match[1];
|
|
173
|
-
await ctx.answerCallbackQuery({ text: `Re-running issue #${issue}` });
|
|
174
|
-
await ctx.editMessageReplyMarkup({ reply_markup: undefined });
|
|
175
|
-
await handleLocusCommand(ctx, "run", [issue]);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// Review callbacks
|
|
179
|
-
bot.callbackQuery(/^review:approve:(\d+)$/, async (ctx) => {
|
|
180
|
-
const pr = ctx.match[1];
|
|
181
|
-
await ctx.answerCallbackQuery({ text: "Approving PR..." });
|
|
182
|
-
await ctx.editMessageReplyMarkup({ reply_markup: undefined });
|
|
183
|
-
await ctx.reply(formatSuccess(`Approved PR #${pr}`), {
|
|
184
|
-
parse_mode: "HTML",
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
bot.callbackQuery(/^review:changes:(\d+)$/, async (ctx) => {
|
|
189
|
-
const pr = ctx.match[1];
|
|
190
|
-
await ctx.answerCallbackQuery();
|
|
191
|
-
await ctx.editMessageReplyMarkup({ reply_markup: undefined });
|
|
192
|
-
await ctx.reply(
|
|
193
|
-
formatInfo(
|
|
194
|
-
`Requesting changes on PR #${pr}. Reply with your feedback and I'll pass it along via /iterate ${pr}`
|
|
195
|
-
),
|
|
196
|
-
{ parse_mode: "HTML" }
|
|
197
|
-
);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
bot.callbackQuery(/^review:diff:(\d+)$/, async (ctx) => {
|
|
201
|
-
const pr = ctx.match[1];
|
|
202
|
-
await ctx.answerCallbackQuery();
|
|
203
|
-
await handleLocusCommand(ctx, "review", [pr, "--diff"]);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// Status callbacks
|
|
207
|
-
bot.callbackQuery(CB.RUN_SPRINT, async (ctx) => {
|
|
208
|
-
await ctx.answerCallbackQuery({ text: "Starting sprint run..." });
|
|
209
|
-
await ctx.editMessageReplyMarkup({ reply_markup: undefined });
|
|
210
|
-
await handleLocusCommand(ctx, "run", []);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
bot.callbackQuery(CB.VIEW_ISSUES, async (ctx) => {
|
|
214
|
-
await ctx.answerCallbackQuery();
|
|
215
|
-
await handleLocusCommand(ctx, "issues", []);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// Stash callbacks
|
|
219
|
-
bot.callbackQuery(CB.STASH_POP, async (ctx) => {
|
|
220
|
-
await ctx.answerCallbackQuery();
|
|
221
|
-
await handleStash(ctx, ["pop"]);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
bot.callbackQuery(CB.STASH_LIST, async (ctx) => {
|
|
225
|
-
await ctx.answerCallbackQuery();
|
|
226
|
-
await handleStash(ctx, ["list"]);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
bot.callbackQuery(CB.STASH_DROP, async (ctx) => {
|
|
230
|
-
await ctx.answerCallbackQuery();
|
|
231
|
-
await handleStash(ctx, ["drop"]);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// Confirmation callbacks
|
|
235
|
-
bot.callbackQuery(CB.CONFIRM_ACTION, async (ctx) => {
|
|
236
|
-
await ctx.answerCallbackQuery({ text: "Confirmed!" });
|
|
237
|
-
await ctx.editMessageReplyMarkup({ reply_markup: undefined });
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
bot.callbackQuery(CB.CANCEL_ACTION, async (ctx) => {
|
|
241
|
-
await ctx.answerCallbackQuery({ text: "Cancelled." });
|
|
242
|
-
await ctx.editMessageReplyMarkup({ reply_markup: undefined });
|
|
243
|
-
await ctx.reply(formatInfo("Action cancelled."), { parse_mode: "HTML" });
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
248
|
-
|
|
249
|
-
/** Parse arguments from a Telegram command message. */
|
|
250
|
-
function parseArgs(text: string, command: string): string[] {
|
|
251
|
-
// Remove the /command part (handles @botname suffix too)
|
|
252
|
-
const prefixRegex = new RegExp(`^/${command}(@\\S+)?\\s*`);
|
|
253
|
-
const rest = text.replace(prefixRegex, "").trim();
|
|
254
|
-
if (!rest) return [];
|
|
255
|
-
return rest.split(/\s+/);
|
|
256
|
-
}
|
package/src/commands/git.ts
DELETED
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git command handlers — stage, commit, stash, branch, checkout, diff, PR.
|
|
3
|
-
*
|
|
4
|
-
* Uses direct git subprocess calls for fast, local operations.
|
|
5
|
-
* PR creation uses `gh` CLI (same as the locus CLI does).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { execSync } from "node:child_process";
|
|
9
|
-
import type { Context } from "grammy";
|
|
10
|
-
import {
|
|
11
|
-
bold,
|
|
12
|
-
codeBlock,
|
|
13
|
-
escapeHtml,
|
|
14
|
-
formatError,
|
|
15
|
-
formatSuccess,
|
|
16
|
-
} from "../ui/format.js";
|
|
17
|
-
import { stashKeyboard } from "../ui/keyboards.js";
|
|
18
|
-
import {
|
|
19
|
-
gitBranchCreatedMessage,
|
|
20
|
-
gitCheckoutMessage,
|
|
21
|
-
gitCommitMessage,
|
|
22
|
-
gitStashMessage,
|
|
23
|
-
prCreatedMessage,
|
|
24
|
-
} from "../ui/messages.js";
|
|
25
|
-
|
|
26
|
-
// ─── Git Helper ─────────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
function git(args: string): string {
|
|
29
|
-
return execSync(`git ${args}`, {
|
|
30
|
-
encoding: "utf-8",
|
|
31
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
32
|
-
cwd: process.cwd(),
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function gitSafe(args: string): string | null {
|
|
37
|
-
try {
|
|
38
|
-
return git(args);
|
|
39
|
-
} catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ─── Command Handlers ───────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
/** /gitstatus — show git status */
|
|
47
|
-
export async function handleGitStatus(ctx: Context): Promise<void> {
|
|
48
|
-
try {
|
|
49
|
-
const status = git("status --short");
|
|
50
|
-
if (!status.trim()) {
|
|
51
|
-
await ctx.reply(formatSuccess("Working tree is clean."), {
|
|
52
|
-
parse_mode: "HTML",
|
|
53
|
-
});
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const branch = git("branch --show-current").trim();
|
|
58
|
-
await ctx.reply(
|
|
59
|
-
`${bold("Branch:")} ${escapeHtml(branch)}\n\n${codeBlock(status)}`,
|
|
60
|
-
{ parse_mode: "HTML" }
|
|
61
|
-
);
|
|
62
|
-
} catch (error: unknown) {
|
|
63
|
-
await ctx.reply(formatError("Failed to get git status", String(error)), {
|
|
64
|
-
parse_mode: "HTML",
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** /stage [files|.] — stage files for commit */
|
|
70
|
-
export async function handleStage(ctx: Context, args: string[]): Promise<void> {
|
|
71
|
-
const target = args.length > 0 ? args.join(" ") : ".";
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
git(`add ${target}`);
|
|
75
|
-
const status = git("status --short");
|
|
76
|
-
await ctx.reply(
|
|
77
|
-
`${formatSuccess(`Staged: ${target}`)}\n\n${codeBlock(status)}`,
|
|
78
|
-
{ parse_mode: "HTML" }
|
|
79
|
-
);
|
|
80
|
-
} catch (error: unknown) {
|
|
81
|
-
await ctx.reply(formatError("Failed to stage files", String(error)), {
|
|
82
|
-
parse_mode: "HTML",
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** /commit <message> — commit staged changes */
|
|
88
|
-
export async function handleCommit(
|
|
89
|
-
ctx: Context,
|
|
90
|
-
args: string[]
|
|
91
|
-
): Promise<void> {
|
|
92
|
-
if (args.length === 0) {
|
|
93
|
-
await ctx.reply(formatError("Usage: /commit <message>"), {
|
|
94
|
-
parse_mode: "HTML",
|
|
95
|
-
});
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const message = args.join(" ");
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
const result = git(`commit -m ${JSON.stringify(message)}`);
|
|
103
|
-
// Extract short hash from commit output
|
|
104
|
-
const hashMatch = result.match(/\[[\w/.-]+ ([a-f0-9]+)\]/);
|
|
105
|
-
const hash = hashMatch?.[1] ?? "unknown";
|
|
106
|
-
|
|
107
|
-
await ctx.reply(gitCommitMessage(message, hash), {
|
|
108
|
-
parse_mode: "HTML",
|
|
109
|
-
});
|
|
110
|
-
} catch (error: unknown) {
|
|
111
|
-
const errStr = String(error);
|
|
112
|
-
if (errStr.includes("nothing to commit")) {
|
|
113
|
-
await ctx.reply(
|
|
114
|
-
formatError("Nothing to commit. Stage changes first with /stage"),
|
|
115
|
-
{
|
|
116
|
-
parse_mode: "HTML",
|
|
117
|
-
}
|
|
118
|
-
);
|
|
119
|
-
} else {
|
|
120
|
-
await ctx.reply(formatError("Failed to commit", errStr), {
|
|
121
|
-
parse_mode: "HTML",
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** /stash [pop|list|drop|save] — stash operations */
|
|
128
|
-
export async function handleStash(ctx: Context, args: string[]): Promise<void> {
|
|
129
|
-
const subcommand = args[0] ?? "push";
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
switch (subcommand) {
|
|
133
|
-
case "push":
|
|
134
|
-
case "save": {
|
|
135
|
-
const message = args.slice(1).join(" ");
|
|
136
|
-
const stashArgs = message
|
|
137
|
-
? `stash push -m ${JSON.stringify(message)}`
|
|
138
|
-
: "stash push";
|
|
139
|
-
git(stashArgs);
|
|
140
|
-
await ctx.reply(gitStashMessage("Changes stashed"), {
|
|
141
|
-
parse_mode: "HTML",
|
|
142
|
-
reply_markup: stashKeyboard(),
|
|
143
|
-
});
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
case "pop": {
|
|
147
|
-
git("stash pop");
|
|
148
|
-
await ctx.reply(gitStashMessage("Stash popped"), {
|
|
149
|
-
parse_mode: "HTML",
|
|
150
|
-
});
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
case "list": {
|
|
154
|
-
const list = gitSafe("stash list") ?? "";
|
|
155
|
-
if (!list.trim()) {
|
|
156
|
-
await ctx.reply(formatSuccess("No stashes."), {
|
|
157
|
-
parse_mode: "HTML",
|
|
158
|
-
});
|
|
159
|
-
} else {
|
|
160
|
-
await ctx.reply(codeBlock(list), { parse_mode: "HTML" });
|
|
161
|
-
}
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
case "drop": {
|
|
165
|
-
const stashRef = args[1] ?? "stash@{0}";
|
|
166
|
-
git(`stash drop ${stashRef}`);
|
|
167
|
-
await ctx.reply(gitStashMessage(`Dropped ${stashRef}`), {
|
|
168
|
-
parse_mode: "HTML",
|
|
169
|
-
});
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
default: {
|
|
173
|
-
// Default: just stash
|
|
174
|
-
git("stash push");
|
|
175
|
-
await ctx.reply(gitStashMessage("Changes stashed"), {
|
|
176
|
-
parse_mode: "HTML",
|
|
177
|
-
reply_markup: stashKeyboard(),
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
} catch (error: unknown) {
|
|
182
|
-
await ctx.reply(formatError("Stash operation failed", String(error)), {
|
|
183
|
-
parse_mode: "HTML",
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** /branch [name] — list branches or create a new one */
|
|
189
|
-
export async function handleBranch(
|
|
190
|
-
ctx: Context,
|
|
191
|
-
args: string[]
|
|
192
|
-
): Promise<void> {
|
|
193
|
-
try {
|
|
194
|
-
if (args.length === 0) {
|
|
195
|
-
// List branches
|
|
196
|
-
const branches = git("branch -a --format='%(refname:short)'");
|
|
197
|
-
const current = git("branch --show-current").trim();
|
|
198
|
-
await ctx.reply(
|
|
199
|
-
`${bold("Current:")} ${escapeHtml(current)}\n\n${codeBlock(branches)}`,
|
|
200
|
-
{ parse_mode: "HTML" }
|
|
201
|
-
);
|
|
202
|
-
} else {
|
|
203
|
-
// Create new branch
|
|
204
|
-
const branchName = args[0];
|
|
205
|
-
git(`branch ${branchName}`);
|
|
206
|
-
await ctx.reply(gitBranchCreatedMessage(branchName), {
|
|
207
|
-
parse_mode: "HTML",
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
} catch (error: unknown) {
|
|
211
|
-
await ctx.reply(formatError("Branch operation failed", String(error)), {
|
|
212
|
-
parse_mode: "HTML",
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** /checkout <branch> — switch to a branch */
|
|
218
|
-
export async function handleCheckout(
|
|
219
|
-
ctx: Context,
|
|
220
|
-
args: string[]
|
|
221
|
-
): Promise<void> {
|
|
222
|
-
if (args.length === 0) {
|
|
223
|
-
await ctx.reply(formatError("Usage: /checkout <branch>"), {
|
|
224
|
-
parse_mode: "HTML",
|
|
225
|
-
});
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const branch = args[0];
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
git(`checkout ${branch}`);
|
|
233
|
-
await ctx.reply(gitCheckoutMessage(branch), { parse_mode: "HTML" });
|
|
234
|
-
} catch (error: unknown) {
|
|
235
|
-
await ctx.reply(formatError("Checkout failed", String(error)), {
|
|
236
|
-
parse_mode: "HTML",
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** /diff — show git diff summary */
|
|
242
|
-
export async function handleDiff(ctx: Context): Promise<void> {
|
|
243
|
-
try {
|
|
244
|
-
const diff = git("diff --stat");
|
|
245
|
-
if (!diff.trim()) {
|
|
246
|
-
const staged = git("diff --cached --stat");
|
|
247
|
-
if (!staged.trim()) {
|
|
248
|
-
await ctx.reply(formatSuccess("No changes."), {
|
|
249
|
-
parse_mode: "HTML",
|
|
250
|
-
});
|
|
251
|
-
} else {
|
|
252
|
-
await ctx.reply(`${bold("Staged changes:")}\n\n${codeBlock(staged)}`, {
|
|
253
|
-
parse_mode: "HTML",
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
await ctx.reply(`${bold("Unstaged changes:")}\n\n${codeBlock(diff)}`, {
|
|
258
|
-
parse_mode: "HTML",
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
} catch (error: unknown) {
|
|
262
|
-
await ctx.reply(formatError("Diff failed", String(error)), {
|
|
263
|
-
parse_mode: "HTML",
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/** /pr <title> — create a pull request */
|
|
269
|
-
export async function handlePR(ctx: Context, args: string[]): Promise<void> {
|
|
270
|
-
if (args.length === 0) {
|
|
271
|
-
await ctx.reply(formatError("Usage: /pr <title>"), {
|
|
272
|
-
parse_mode: "HTML",
|
|
273
|
-
});
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const title = args.join(" ");
|
|
278
|
-
|
|
279
|
-
try {
|
|
280
|
-
// Push current branch first
|
|
281
|
-
const branch = git("branch --show-current").trim();
|
|
282
|
-
try {
|
|
283
|
-
git(`push -u origin ${branch}`);
|
|
284
|
-
} catch {
|
|
285
|
-
// May already be pushed — continue
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Create PR using gh CLI
|
|
289
|
-
const result = execSync(
|
|
290
|
-
`gh pr create --title ${JSON.stringify(title)} --body "Created via Locus Telegram Bot" --head ${branch}`,
|
|
291
|
-
{
|
|
292
|
-
encoding: "utf-8",
|
|
293
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
294
|
-
cwd: process.cwd(),
|
|
295
|
-
}
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
const prMatch = result.match(/\/pull\/(\d+)/);
|
|
299
|
-
const prNumber = prMatch ? Number(prMatch[1]) : 0;
|
|
300
|
-
|
|
301
|
-
await ctx.reply(prCreatedMessage(prNumber, result.trim()), {
|
|
302
|
-
parse_mode: "HTML",
|
|
303
|
-
});
|
|
304
|
-
} catch (error: unknown) {
|
|
305
|
-
await ctx.reply(formatError("Failed to create PR", String(error)), {
|
|
306
|
-
parse_mode: "HTML",
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
}
|