@opentag/cli 0.2.0

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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/catalogs/executors.d.ts +23 -0
  4. package/dist/catalogs/executors.d.ts.map +1 -0
  5. package/dist/catalogs/languages.d.ts +8 -0
  6. package/dist/catalogs/languages.d.ts.map +1 -0
  7. package/dist/catalogs/platforms.d.ts +16 -0
  8. package/dist/catalogs/platforms.d.ts.map +1 -0
  9. package/dist/commands/executors.d.ts +5 -0
  10. package/dist/commands/executors.d.ts.map +1 -0
  11. package/dist/commands/platforms.d.ts +2 -0
  12. package/dist/commands/platforms.d.ts.map +1 -0
  13. package/dist/commands/setup.d.ts +10 -0
  14. package/dist/commands/setup.d.ts.map +1 -0
  15. package/dist/config.d.ts +638 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/doctor.d.ts +5 -0
  18. package/dist/doctor.d.ts.map +1 -0
  19. package/dist/health.d.ts +11 -0
  20. package/dist/health.d.ts.map +1 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +2320 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/lark-registration.d.ts +2 -0
  26. package/dist/lark-registration.d.ts.map +1 -0
  27. package/dist/platforms/github/display.d.ts +10 -0
  28. package/dist/platforms/github/display.d.ts.map +1 -0
  29. package/dist/platforms/lark/display.d.ts +13 -0
  30. package/dist/platforms/lark/display.d.ts.map +1 -0
  31. package/dist/platforms/lark/registration-ui.d.ts +10 -0
  32. package/dist/platforms/lark/registration-ui.d.ts.map +1 -0
  33. package/dist/platforms/lark/saved-config.d.ts +14 -0
  34. package/dist/platforms/lark/saved-config.d.ts.map +1 -0
  35. package/dist/platforms/ports.d.ts +4 -0
  36. package/dist/platforms/ports.d.ts.map +1 -0
  37. package/dist/setup/builders.d.ts +4 -0
  38. package/dist/setup/builders.d.ts.map +1 -0
  39. package/dist/setup/defaults.d.ts +5 -0
  40. package/dist/setup/defaults.d.ts.map +1 -0
  41. package/dist/setup/flow.d.ts +44 -0
  42. package/dist/setup/flow.d.ts.map +1 -0
  43. package/dist/setup/guides.d.ts +25 -0
  44. package/dist/setup/guides.d.ts.map +1 -0
  45. package/dist/setup/summary.d.ts +5 -0
  46. package/dist/setup/summary.d.ts.map +1 -0
  47. package/dist/setup/types.d.ts +68 -0
  48. package/dist/setup/types.d.ts.map +1 -0
  49. package/dist/setup.d.ts +7 -0
  50. package/dist/setup.d.ts.map +1 -0
  51. package/dist/start.d.ts +34 -0
  52. package/dist/start.d.ts.map +1 -0
  53. package/dist/status.d.ts +25 -0
  54. package/dist/status.d.ts.map +1 -0
  55. package/dist/ui/clack.d.ts +3 -0
  56. package/dist/ui/clack.d.ts.map +1 -0
  57. package/dist/ui/messages.d.ts +12 -0
  58. package/dist/ui/messages.d.ts.map +1 -0
  59. package/dist/ui/prompts.d.ts +30 -0
  60. package/dist/ui/prompts.d.ts.map +1 -0
  61. package/package.json +57 -0
package/dist/index.js ADDED
@@ -0,0 +1,2320 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/config.ts
7
+ import { randomUUID } from "crypto";
8
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
9
+ import { homedir } from "os";
10
+ import { dirname, join, resolve } from "path";
11
+ import { formatConfigError as formatDaemonConfigError, parseDaemonConfig } from "@opentag/local-runtime";
12
+ import { z } from "zod";
13
+ var ExecutorSchema = z.enum(["echo", "codex", "claude-code"]);
14
+ var KeepWorktreeSchema = z.enum(["always", "on_failure", "never"]);
15
+ var PositiveIntegerSchema = z.number().int().positive();
16
+ var CliLanguageSchema = z.enum(["en", "zh-CN"]);
17
+ var PlatformSchema = z.enum(["lark", "slack", "github", "telegram"]);
18
+ var LarkSetupMethodSchema = z.enum(["saved", "scan", "manual"]);
19
+ var SlackModeSchema = z.enum(["socket_mode", "events_api"]);
20
+ var BindingMethodSchema = z.enum(["default_project", "bind_later"]);
21
+ var OptionalPortSchema = z.number().int().min(1).max(65535).optional();
22
+ var RepositoryBindingSchema = z.object({
23
+ provider: z.string().min(1),
24
+ owner: z.string().min(1),
25
+ repo: z.string().min(1),
26
+ checkoutPath: z.string().min(1),
27
+ defaultExecutor: ExecutorSchema,
28
+ baseBranch: z.string().min(1),
29
+ pushRemote: z.string().min(1),
30
+ worktreeRoot: z.string().min(1),
31
+ keepWorktree: KeepWorktreeSchema
32
+ }).strict();
33
+ var ChannelBindingSchema = z.object({
34
+ provider: z.string().min(1),
35
+ accountId: z.string().min(1),
36
+ conversationId: z.string().min(1),
37
+ repoProvider: z.string().min(1),
38
+ owner: z.string().min(1),
39
+ repo: z.string().min(1),
40
+ metadata: z.record(z.string(), z.unknown()).optional()
41
+ }).strict();
42
+ var ClaudeCodeSchema = z.object({
43
+ command: z.string().min(1).optional(),
44
+ model: z.string().min(1).optional(),
45
+ permissionMode: z.enum(["acceptEdits", "auto", "bypassPermissions", "default", "plan"]).optional(),
46
+ dangerouslySkipPermissions: z.boolean().optional()
47
+ }).strict();
48
+ var SecuritySchema = z.object({
49
+ mode: z.enum(["enforce", "audit", "off"]).optional(),
50
+ allowedWorkspaceRoot: z.string().min(1).optional(),
51
+ allowUnsafePrompts: z.boolean().optional(),
52
+ extraSafeEnv: z.array(z.string().min(1)).optional()
53
+ }).strict();
54
+ var DaemonConfigSchema = z.object({
55
+ runnerId: z.string().min(1),
56
+ dispatcherUrl: z.string().url(),
57
+ repositories: z.array(RepositoryBindingSchema).min(1),
58
+ channelBindings: z.array(ChannelBindingSchema).optional(),
59
+ claudeCode: ClaudeCodeSchema.optional(),
60
+ security: SecuritySchema.optional(),
61
+ githubToken: z.string().min(1).optional(),
62
+ preparePullRequestBranch: z.boolean().optional(),
63
+ allowAutoCreatePullRequest: z.boolean().optional(),
64
+ pairingToken: z.string().min(1),
65
+ pollIntervalMs: PositiveIntegerSchema,
66
+ heartbeatIntervalMs: PositiveIntegerSchema
67
+ }).strict();
68
+ var LarkPlatformSchema = z.object({
69
+ appId: z.string().min(1),
70
+ appSecret: z.string().min(1),
71
+ domain: z.enum(["lark", "feishu"]),
72
+ botOpenId: z.string().min(1).optional(),
73
+ defaultProjectBinding: z.boolean().optional()
74
+ }).strict();
75
+ var SlackPlatformSchema = z.object({
76
+ mode: SlackModeSchema.optional(),
77
+ appToken: z.string().min(1).optional(),
78
+ signingSecret: z.string().min(1).optional(),
79
+ botToken: z.string().min(1),
80
+ teamId: z.string().min(1),
81
+ channelId: z.string().min(1),
82
+ appId: z.string().min(1).optional(),
83
+ defaultProjectBinding: z.boolean().optional(),
84
+ port: OptionalPortSchema
85
+ }).strict().superRefine((value, context) => {
86
+ const mode = value.mode ?? "events_api";
87
+ if (mode === "socket_mode" && !value.appToken) {
88
+ context.addIssue({
89
+ code: z.ZodIssueCode.custom,
90
+ path: ["appToken"],
91
+ message: "Slack Socket Mode requires appToken."
92
+ });
93
+ }
94
+ if (mode === "events_api" && !value.signingSecret) {
95
+ context.addIssue({
96
+ code: z.ZodIssueCode.custom,
97
+ path: ["signingSecret"],
98
+ message: "Slack Events API requires signingSecret."
99
+ });
100
+ }
101
+ });
102
+ var GitHubPlatformSchema = z.object({
103
+ webhookSecret: z.string().min(1),
104
+ owner: z.string().min(1),
105
+ repo: z.string().min(1),
106
+ webhookPath: z.string().min(1).optional(),
107
+ port: OptionalPortSchema
108
+ }).strict();
109
+ var PreferencesSchema = z.object({
110
+ language: CliLanguageSchema.optional(),
111
+ lastSetup: z.object({
112
+ platforms: z.array(PlatformSchema).optional(),
113
+ executor: ExecutorSchema.optional(),
114
+ projectPath: z.string().min(1).optional(),
115
+ larkSetupMethod: LarkSetupMethodSchema.optional(),
116
+ larkDomain: z.enum(["lark", "feishu"]).optional(),
117
+ bindingMethod: BindingMethodSchema.optional(),
118
+ slackMode: SlackModeSchema.optional(),
119
+ slackTeamId: z.string().min(1).optional(),
120
+ slackChannelId: z.string().min(1).optional(),
121
+ slackPort: OptionalPortSchema,
122
+ githubOwner: z.string().min(1).optional(),
123
+ githubRepo: z.string().min(1).optional(),
124
+ githubPort: OptionalPortSchema,
125
+ githubAutoCreatePullRequest: z.boolean().optional()
126
+ }).strict().optional()
127
+ }).strict();
128
+ var OpenTagCliConfigSchema = z.object({
129
+ schemaVersion: z.literal(1),
130
+ state: z.object({
131
+ directory: z.string().min(1),
132
+ databasePath: z.string().min(1),
133
+ worktreeRoot: z.string().min(1)
134
+ }).strict(),
135
+ preferences: PreferencesSchema.optional(),
136
+ daemon: DaemonConfigSchema,
137
+ platforms: z.object({
138
+ lark: LarkPlatformSchema.optional(),
139
+ slack: SlackPlatformSchema.optional(),
140
+ github: GitHubPlatformSchema.optional()
141
+ }).strict()
142
+ }).strict();
143
+ function configHome(env, home = homedir()) {
144
+ if (env.OPENTAG_CONFIG_HOME) return resolve(env.OPENTAG_CONFIG_HOME);
145
+ if (env.XDG_CONFIG_HOME) return resolve(env.XDG_CONFIG_HOME, "opentag");
146
+ return join(home, ".config", "opentag");
147
+ }
148
+ function defaultConfigPath(env = process.env, home = homedir()) {
149
+ if (env.OPENTAG_CONFIG_PATH) return resolve(env.OPENTAG_CONFIG_PATH);
150
+ return join(configHome(env, home), "config.json");
151
+ }
152
+ function defaultStateDirectory(env = process.env, home = homedir()) {
153
+ if (env.OPENTAG_STATE_DIR) return resolve(env.OPENTAG_STATE_DIR);
154
+ if (env.XDG_STATE_HOME) return resolve(env.XDG_STATE_HOME, "opentag");
155
+ return join(home, ".local", "state", "opentag");
156
+ }
157
+ function formatPath(path) {
158
+ return path.length ? path.join(".") : "config";
159
+ }
160
+ function formatCliConfigError(error) {
161
+ if (error instanceof z.ZodError) {
162
+ return error.issues.map((issue) => `${formatPath(issue.path)}: ${issue.message}`).join("\n");
163
+ }
164
+ return formatDaemonConfigError(error);
165
+ }
166
+ function parseCliConfig(value) {
167
+ const parsed = OpenTagCliConfigSchema.parse(value);
168
+ return {
169
+ ...parsed,
170
+ daemon: parseDaemonConfig(parsed.daemon)
171
+ };
172
+ }
173
+ function readCliConfig(path = defaultConfigPath()) {
174
+ assertPrivateConfigFile(path);
175
+ return parseCliConfig(JSON.parse(readFileSync(path, "utf8")));
176
+ }
177
+ function ensurePrivateDirectory(path) {
178
+ const createdPath = mkdirSync(path, { recursive: true, mode: 448 });
179
+ if (createdPath) {
180
+ chmodSync(path, 448);
181
+ }
182
+ }
183
+ function writeCliConfigAtomic(path, config) {
184
+ ensurePrivateDirectory(dirname(path));
185
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
186
+ try {
187
+ writeFileSync(tempPath, `${JSON.stringify(config, null, 2)}
188
+ `, { mode: 384, flag: "wx" });
189
+ chmodSync(tempPath, 384);
190
+ renameSync(tempPath, path);
191
+ chmodSync(path, 384);
192
+ } catch (error) {
193
+ rmSync(tempPath, { force: true });
194
+ throw error;
195
+ }
196
+ }
197
+ function assertPrivateConfigFile(path) {
198
+ if (process.platform === "win32") return;
199
+ const mode = statSync(path).mode & 511;
200
+ if ((mode & 63) !== 0) {
201
+ throw new Error(`OpenTag config contains secrets and must not be readable by group or others: ${path}
202
+ Fix it with: chmod 600 ${path}`);
203
+ }
204
+ }
205
+ function redactValue(key, value) {
206
+ if (["appSecret", "appToken", "botToken", "githubToken", "pairingToken", "signingSecret", "webhookSecret"].includes(key)) {
207
+ return "[REDACTED]";
208
+ }
209
+ if (Array.isArray(value)) {
210
+ return value.map((entry) => redactValue("", entry));
211
+ }
212
+ if (value && typeof value === "object") {
213
+ return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, redactValue(entryKey, entryValue)]));
214
+ }
215
+ return value;
216
+ }
217
+ function redactedCliConfig(config) {
218
+ return redactValue("", config);
219
+ }
220
+
221
+ // src/catalogs/executors.ts
222
+ import { existsSync } from "fs";
223
+ import { delimiter, extname, join as join2 } from "path";
224
+ var EXECUTOR_CATALOG = [
225
+ {
226
+ id: "codex",
227
+ label: "Codex",
228
+ command: "codex"
229
+ },
230
+ {
231
+ id: "claude-code",
232
+ label: "Claude Code",
233
+ command: "claude"
234
+ },
235
+ {
236
+ id: "echo",
237
+ label: "Echo",
238
+ alwaysAvailable: true,
239
+ devOnly: true
240
+ }
241
+ ];
242
+ function pathExistsOnPath(command, env = process.env) {
243
+ const paths = env.PATH?.split(delimiter).filter(Boolean) ?? [];
244
+ const candidates = process.platform === "win32" && !extname(command) ? [command, ...(env.PATHEXT?.split(delimiter).filter(Boolean) ?? [".COM", ".EXE", ".BAT", ".CMD"]).map((extension) => `${command}${extension.toLowerCase()}`)] : [command];
245
+ return paths.some((directory) => candidates.some((candidate) => existsSync(join2(directory, candidate))));
246
+ }
247
+ function parseExecutorId(value) {
248
+ if (value === "echo" || value === "codex" || value === "claude-code") {
249
+ return value;
250
+ }
251
+ throw new Error("Executor must be echo, codex, or claude-code.");
252
+ }
253
+ function detectExecutors(env = process.env) {
254
+ return EXECUTOR_CATALOG.map((executor) => {
255
+ if (executor.alwaysAvailable) {
256
+ return {
257
+ id: executor.id,
258
+ available: true,
259
+ reason: executor.devOnly ? "Dev/test only; does not run a real coding agent" : "Built in"
260
+ };
261
+ }
262
+ const available = executor.command ? pathExistsOnPath(executor.command, env) : false;
263
+ return {
264
+ id: executor.id,
265
+ available,
266
+ reason: available ? `Found ${executor.command} on PATH` : `Could not find ${executor.command} on PATH`
267
+ };
268
+ });
269
+ }
270
+ function defaultExecutorId(input = {}) {
271
+ if (input.previous) {
272
+ return input.previous;
273
+ }
274
+ const detections = input.detections ?? detectExecutors();
275
+ if (detections.find((executor) => executor.id === "codex")?.available) {
276
+ return "codex";
277
+ }
278
+ if (detections.find((executor) => executor.id === "claude-code")?.available) {
279
+ return "claude-code";
280
+ }
281
+ return "echo";
282
+ }
283
+ function executorLabel(id) {
284
+ return EXECUTOR_CATALOG.find((executor) => executor.id === id)?.label ?? id;
285
+ }
286
+ function formatExecutorStatus(executor, available) {
287
+ if (executor.devOnly) {
288
+ return "dev/test only";
289
+ }
290
+ return available ? "available" : "not found";
291
+ }
292
+ function formatExecutors(env = process.env) {
293
+ const detections = detectExecutors(env);
294
+ return [
295
+ "Coding agents:",
296
+ ...EXECUTOR_CATALOG.map((executor) => {
297
+ const detection = detections.find((entry) => entry.id === executor.id);
298
+ const status = formatExecutorStatus(executor, detection?.available ?? false);
299
+ return ` ${executor.label}: ${status}${detection ? ` (${detection.reason})` : ""}`;
300
+ })
301
+ ].join("\n");
302
+ }
303
+
304
+ // src/commands/executors.ts
305
+ function runExecutorsCommand(options = {}) {
306
+ console.log(formatExecutors(options.env ?? process.env));
307
+ }
308
+
309
+ // src/catalogs/platforms.ts
310
+ var SETUP_GUIDE_BASE_URL = "https://github.com/amplifthq/opentag/blob/main/docs/platforms";
311
+ var PLATFORM_SETUP_GUIDE_FILES = {
312
+ lark: {
313
+ en: "lark.en.md",
314
+ "zh-CN": "lark.zh-CN.md"
315
+ },
316
+ slack: {
317
+ en: "slack.en.md",
318
+ "zh-CN": "slack.zh-CN.md"
319
+ },
320
+ github: {
321
+ en: "github.en.md",
322
+ "zh-CN": "github.zh-CN.md"
323
+ }
324
+ };
325
+ var PLATFORM_CATALOG = [
326
+ {
327
+ id: "lark",
328
+ label: "Lark / Feishu",
329
+ status: "setup_ready",
330
+ startable: true
331
+ },
332
+ {
333
+ id: "slack",
334
+ label: "Slack",
335
+ status: "setup_ready",
336
+ startable: true
337
+ },
338
+ {
339
+ id: "github",
340
+ label: "GitHub",
341
+ status: "setup_ready",
342
+ startable: true
343
+ },
344
+ {
345
+ id: "telegram",
346
+ label: "Telegram",
347
+ status: "experimental_setup_pending",
348
+ startable: false
349
+ }
350
+ ];
351
+ function parsePlatformId(value) {
352
+ if (value === "lark" || value === "slack" || value === "github" || value === "telegram") {
353
+ return value;
354
+ }
355
+ throw new Error("Platform must be lark, slack, github, or telegram.");
356
+ }
357
+ function platformById(id) {
358
+ const descriptor = PLATFORM_CATALOG.find((platform) => platform.id === id);
359
+ if (!descriptor) {
360
+ throw new Error(`Unknown platform: ${id}`);
361
+ }
362
+ return descriptor;
363
+ }
364
+ function platformSetupGuideUrl(id, language) {
365
+ const file = PLATFORM_SETUP_GUIDE_FILES[id]?.[language];
366
+ return file ? `${SETUP_GUIDE_BASE_URL}/${file}` : void 0;
367
+ }
368
+ function formatPlatformStatus(status) {
369
+ switch (status) {
370
+ case "setup_ready":
371
+ return "Setup wizard ready";
372
+ case "setup_pending":
373
+ return "Adapter exists; CLI setup pending";
374
+ case "experimental_setup_pending":
375
+ return "Experimental adapter; CLI setup pending";
376
+ }
377
+ }
378
+ function formatPlatforms() {
379
+ return [
380
+ "CLI setup support:",
381
+ ...PLATFORM_CATALOG.map((platform) => {
382
+ return ` ${platform.label}: ${formatPlatformStatus(platform.status)}`;
383
+ })
384
+ ].join("\n");
385
+ }
386
+
387
+ // src/commands/platforms.ts
388
+ function runPlatformsCommand() {
389
+ console.log(formatPlatforms());
390
+ }
391
+
392
+ // src/doctor.ts
393
+ import { doctorHasFailures, executorsFromConfig, formatDoctorChecks, runDoctor } from "@opentag/local-runtime";
394
+ async function runDoctorCommand(options) {
395
+ const config = readCliConfig(options.config ?? defaultConfigPath());
396
+ const checks = await runDoctor({
397
+ config: config.daemon,
398
+ executors: executorsFromConfig(config.daemon)
399
+ });
400
+ console.log(formatDoctorChecks(checks));
401
+ if (doctorHasFailures(checks)) {
402
+ process.exitCode = 1;
403
+ }
404
+ }
405
+
406
+ // src/commands/setup.ts
407
+ import { existsSync as existsSync5 } from "fs";
408
+
409
+ // src/setup/builders.ts
410
+ import { randomBytes } from "crypto";
411
+ import { realpathSync } from "fs";
412
+ import { join as join3 } from "path";
413
+ import { projectTargetRefFromLocalPath } from "@opentag/core";
414
+ function pairingToken() {
415
+ return randomBytes(32).toString("hex");
416
+ }
417
+ function createSetupConfig(input, env = process.env) {
418
+ const checkoutPath = realpathSync.native(input.projectPath);
419
+ const target = projectTargetRefFromLocalPath(checkoutPath);
420
+ const stateDirectory = input.stateDirectory ?? defaultStateDirectory(env);
421
+ const worktreeRoot = join3(stateDirectory, "worktrees");
422
+ const databasePath = join3(stateDirectory, "opentag.db");
423
+ const repositoryBindings = [
424
+ {
425
+ provider: target.provider,
426
+ owner: target.owner,
427
+ repo: target.repo,
428
+ checkoutPath,
429
+ defaultExecutor: input.executor,
430
+ baseBranch: "main",
431
+ pushRemote: "origin",
432
+ worktreeRoot,
433
+ keepWorktree: "on_failure"
434
+ },
435
+ ...input.github ? [
436
+ {
437
+ provider: "github",
438
+ owner: input.github.owner,
439
+ repo: input.github.repo,
440
+ checkoutPath,
441
+ defaultExecutor: input.executor,
442
+ baseBranch: "main",
443
+ pushRemote: "origin",
444
+ worktreeRoot,
445
+ keepWorktree: "on_failure"
446
+ }
447
+ ] : []
448
+ ].filter((binding, index, bindings) => {
449
+ return bindings.findIndex((candidate) => candidate.provider === binding.provider && candidate.owner === binding.owner && candidate.repo === binding.repo) === index;
450
+ });
451
+ const channelBindings = [
452
+ ...input.slack && input.slack.bindingMethod === "default_project" ? [
453
+ {
454
+ provider: "slack",
455
+ accountId: input.slack.teamId,
456
+ conversationId: input.slack.channelId,
457
+ repoProvider: target.provider,
458
+ owner: target.owner,
459
+ repo: target.repo
460
+ }
461
+ ] : []
462
+ ];
463
+ return {
464
+ schemaVersion: 1,
465
+ preferences: {
466
+ language: input.language,
467
+ lastSetup: {
468
+ platforms: [input.platform],
469
+ executor: input.executor,
470
+ projectPath: checkoutPath,
471
+ ...input.lark ? {
472
+ larkSetupMethod: input.lark.setupMethod,
473
+ larkDomain: input.lark.domain,
474
+ bindingMethod: input.lark.bindingMethod
475
+ } : {},
476
+ ...input.slack ? {
477
+ bindingMethod: input.slack.bindingMethod,
478
+ slackMode: input.slack.mode,
479
+ slackTeamId: input.slack.teamId,
480
+ slackChannelId: input.slack.channelId,
481
+ ...input.slack.port ? { slackPort: input.slack.port } : {}
482
+ } : {},
483
+ ...input.github ? {
484
+ githubOwner: input.github.owner,
485
+ githubRepo: input.github.repo,
486
+ githubPort: input.github.port,
487
+ githubAutoCreatePullRequest: input.github.autoCreatePullRequest
488
+ } : {}
489
+ }
490
+ },
491
+ state: {
492
+ directory: stateDirectory,
493
+ databasePath,
494
+ worktreeRoot
495
+ },
496
+ daemon: {
497
+ runnerId: "runner_local",
498
+ dispatcherUrl: "http://localhost:3030",
499
+ pairingToken: pairingToken(),
500
+ repositories: repositoryBindings,
501
+ ...channelBindings.length > 0 ? { channelBindings } : {},
502
+ ...input.github ? { githubToken: input.github.token } : {},
503
+ ...input.github ? { preparePullRequestBranch: true } : {},
504
+ ...input.github ? { allowAutoCreatePullRequest: input.github.autoCreatePullRequest } : {},
505
+ pollIntervalMs: 5e3,
506
+ heartbeatIntervalMs: 15e3
507
+ },
508
+ platforms: {
509
+ ...input.lark ? {
510
+ lark: {
511
+ appId: input.lark.appId,
512
+ appSecret: input.lark.appSecret,
513
+ domain: input.lark.domain,
514
+ defaultProjectBinding: input.lark.bindingMethod === "default_project",
515
+ ...input.lark.botOpenId ? { botOpenId: input.lark.botOpenId } : {}
516
+ }
517
+ } : {},
518
+ ...input.slack ? {
519
+ slack: {
520
+ mode: input.slack.mode,
521
+ ...input.slack.appToken ? { appToken: input.slack.appToken } : {},
522
+ ...input.slack.signingSecret ? { signingSecret: input.slack.signingSecret } : {},
523
+ botToken: input.slack.botToken,
524
+ teamId: input.slack.teamId,
525
+ channelId: input.slack.channelId,
526
+ defaultProjectBinding: input.slack.bindingMethod === "default_project",
527
+ ...input.slack.appId ? { appId: input.slack.appId } : {},
528
+ ...input.slack.port ? { port: input.slack.port } : {}
529
+ }
530
+ } : {},
531
+ ...input.github ? {
532
+ github: {
533
+ webhookSecret: input.github.webhookSecret,
534
+ owner: input.github.owner,
535
+ repo: input.github.repo,
536
+ webhookPath: input.github.webhookPath,
537
+ port: input.github.port
538
+ }
539
+ } : {}
540
+ }
541
+ };
542
+ }
543
+
544
+ // src/setup/flow.ts
545
+ import { execFileSync } from "child_process";
546
+ import { randomBytes as randomBytes2 } from "crypto";
547
+ import { existsSync as existsSync4 } from "fs";
548
+
549
+ // src/catalogs/languages.ts
550
+ var LANGUAGE_OPTIONS = [
551
+ { id: "en", label: "English", hint: "Default" },
552
+ { id: "zh-CN", label: "\u7B80\u4F53\u4E2D\u6587", hint: "Chinese" }
553
+ ];
554
+ function parseCliLanguage(value) {
555
+ if (value === "en" || value === "zh-CN") {
556
+ return value;
557
+ }
558
+ throw new Error("Language must be en or zh-CN.");
559
+ }
560
+
561
+ // src/platforms/lark/display.ts
562
+ function shortId(value) {
563
+ if (value.length <= 16) return value;
564
+ return `${value.slice(0, 6)}...${value.slice(-6)}`;
565
+ }
566
+ function domainLabel(domain) {
567
+ return domain === "feishu" ? "Feishu" : "Lark";
568
+ }
569
+ function sourceLabel(source, language) {
570
+ if (language === "zh-CN") {
571
+ return source === "opentag_config" ? "OpenTag \u914D\u7F6E" : "\u65E7 start-lark \u914D\u7F6E";
572
+ }
573
+ return source === "opentag_config" ? "OpenTag config" : "legacy start-lark config";
574
+ }
575
+ function formatLarkPersonalAgentSummary(input, language) {
576
+ const parts = [
577
+ domainLabel(input.domain),
578
+ `App ID ${shortId(input.appId)}`,
579
+ input.botOpenId ? `Bot Open ID ${shortId(input.botOpenId)}` : void 0,
580
+ input.source ? language === "zh-CN" ? `\u6765\u6E90: ${sourceLabel(input.source, language)}` : `from ${sourceLabel(input.source, language)}` : void 0
581
+ ];
582
+ return parts.filter((part) => Boolean(part)).join(" | ");
583
+ }
584
+ function formatSavedLarkCredentialsHint(credentials, language) {
585
+ return formatLarkPersonalAgentSummary(credentials, language);
586
+ }
587
+
588
+ // src/platforms/lark/saved-config.ts
589
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
590
+ import { join as join4 } from "path";
591
+ import { z as z2 } from "zod";
592
+ var SavedLarkCredentialsSchema = z2.object({
593
+ appId: z2.string().min(1),
594
+ appSecret: z2.string().min(1),
595
+ domain: z2.enum(["lark", "feishu"]),
596
+ botOpenId: z2.string().min(1).optional()
597
+ }).passthrough();
598
+ function savedLarkCredentialsFromCliConfig(config) {
599
+ const lark = config.platforms.lark;
600
+ if (!lark) return void 0;
601
+ return {
602
+ appId: lark.appId,
603
+ appSecret: lark.appSecret,
604
+ domain: lark.domain,
605
+ ...lark.botOpenId ? { botOpenId: lark.botOpenId } : {},
606
+ source: "opentag_config"
607
+ };
608
+ }
609
+ function legacyLarkConfigPath(projectPath) {
610
+ return join4(projectPath, ".opentag", "lark", "lark.local.json");
611
+ }
612
+ function parseJsonFile(path) {
613
+ try {
614
+ return JSON.parse(readFileSync2(path, "utf8"));
615
+ } catch (error) {
616
+ throw new Error(`Saved Lark config at ${path} is invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
617
+ }
618
+ }
619
+ function readLegacyLarkCredentials(projectPath) {
620
+ const path = legacyLarkConfigPath(projectPath);
621
+ if (!existsSync2(path)) return void 0;
622
+ assertPrivateConfigFile(path);
623
+ const parsed = SavedLarkCredentialsSchema.safeParse(parseJsonFile(path));
624
+ if (!parsed.success) {
625
+ throw new Error(`Saved Lark config at ${path} is invalid: ${parsed.error.message}`);
626
+ }
627
+ return {
628
+ appId: parsed.data.appId,
629
+ appSecret: parsed.data.appSecret,
630
+ domain: parsed.data.domain,
631
+ ...parsed.data.botOpenId ? { botOpenId: parsed.data.botOpenId } : {},
632
+ source: "legacy_start_lark",
633
+ path
634
+ };
635
+ }
636
+
637
+ // src/platforms/ports.ts
638
+ var DEFAULT_SLACK_EVENTS_PORT = 3040;
639
+ var DEFAULT_GITHUB_WEBHOOK_PORT = 3050;
640
+ function parseLocalPort(value, label) {
641
+ const port = typeof value === "number" ? value : Number(value.trim());
642
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
643
+ throw new Error(`${label} must be an integer from 1 to 65535.`);
644
+ }
645
+ return port;
646
+ }
647
+
648
+ // src/ui/messages.ts
649
+ var MESSAGES = {
650
+ en: {
651
+ intro: "OpenTag setup",
652
+ language: "Language / \u8BED\u8A00",
653
+ platform: "Where should OpenTag listen?",
654
+ executor: "Which coding agent should OpenTag use?",
655
+ projectPath: "Which project should OpenTag use?",
656
+ larkSetup: "How should OpenTag connect to Lark / Feishu?",
657
+ larkDomain: "Which Lark domain should OpenTag use?",
658
+ larkAppId: "Lark App ID",
659
+ larkAppSecret: "Lark App Secret",
660
+ larkBotOpenId: "Lark Bot Open ID (optional)",
661
+ slackMode: "How should OpenTag connect to Slack?",
662
+ slackAppToken: "Slack App-Level Token",
663
+ slackSigningSecret: "Slack Signing Secret",
664
+ slackBotToken: "Slack Bot User OAuth Token",
665
+ slackAppId: "Slack App ID (optional)",
666
+ slackTeamId: "Slack Team ID",
667
+ slackChannelId: "Slack Channel ID",
668
+ slackPort: "Local Slack Events API port",
669
+ githubRepository: "GitHub repository (owner/repo)",
670
+ githubToken: "GitHub token for comments and `apply 1` pull requests",
671
+ githubWebhookSecret: "GitHub webhook secret",
672
+ githubPort: "Local GitHub webhook port",
673
+ githubAutoCreatePr: "Create pull requests immediately after runs? (advanced)",
674
+ bindingMethod: "How should Lark chats bind to this project?",
675
+ confirmSetup: "Write this OpenTag config?",
676
+ cancelled: "OpenTag setup cancelled.",
677
+ complete: "OpenTag setup complete."
678
+ },
679
+ "zh-CN": {
680
+ intro: "OpenTag \u8BBE\u7F6E",
681
+ language: "Language / \u8BED\u8A00",
682
+ platform: "OpenTag \u8981\u76D1\u542C\u54EA\u4E2A\u5E73\u53F0\uFF1F",
683
+ executor: "OpenTag \u8981\u4F7F\u7528\u54EA\u4E2A coding agent\uFF1F",
684
+ projectPath: "OpenTag \u8981\u4F7F\u7528\u54EA\u4E2A\u9879\u76EE\uFF1F",
685
+ larkSetup: "OpenTag \u8981\u5982\u4F55\u8FDE\u63A5 Lark / \u98DE\u4E66\uFF1F",
686
+ larkDomain: "OpenTag \u8981\u4F7F\u7528\u54EA\u4E2A Lark \u57DF\u540D\uFF1F",
687
+ larkAppId: "Lark App ID",
688
+ larkAppSecret: "Lark App Secret",
689
+ larkBotOpenId: "Lark Bot Open ID\uFF08\u53EF\u9009\uFF09",
690
+ slackMode: "OpenTag \u8981\u5982\u4F55\u8FDE\u63A5 Slack\uFF1F",
691
+ slackAppToken: "Slack App-Level Token",
692
+ slackSigningSecret: "Slack Signing Secret",
693
+ slackBotToken: "Slack Bot User OAuth Token",
694
+ slackAppId: "Slack App ID\uFF08\u53EF\u9009\uFF09",
695
+ slackTeamId: "Slack Team ID",
696
+ slackChannelId: "Slack Channel ID",
697
+ slackPort: "\u672C\u5730 Slack Events API \u7AEF\u53E3",
698
+ githubRepository: "GitHub \u4ED3\u5E93\uFF08owner/repo\uFF09",
699
+ githubToken: "GitHub token\uFF08\u7528\u4E8E\u56DE\u5199\u8BC4\u8BBA\u548C apply 1 \u521B\u5EFA PR\uFF09",
700
+ githubWebhookSecret: "GitHub webhook secret",
701
+ githubPort: "\u672C\u5730 GitHub webhook \u7AEF\u53E3",
702
+ githubAutoCreatePr: "run \u7ED3\u675F\u540E\u7ACB\u523B\u81EA\u52A8\u521B\u5EFA pull request \u5417\uFF1F\uFF08\u9AD8\u7EA7\u9009\u9879\uFF09",
703
+ bindingMethod: "Lark \u7FA4\u804A\u8981\u5982\u4F55\u7ED1\u5B9A\u5230\u8FD9\u4E2A\u9879\u76EE\uFF1F",
704
+ confirmSetup: "\u5199\u5165\u8FD9\u4EFD OpenTag \u914D\u7F6E\uFF1F",
705
+ cancelled: "OpenTag \u8BBE\u7F6E\u5DF2\u53D6\u6D88\u3002",
706
+ complete: "OpenTag \u8BBE\u7F6E\u5B8C\u6210\u3002"
707
+ }
708
+ };
709
+ function t(language, key) {
710
+ return MESSAGES[language][key];
711
+ }
712
+ function larkSetupLabel(language, method) {
713
+ if (language === "zh-CN") {
714
+ if (method === "saved") return "\u4F7F\u7528\u5DF2\u4FDD\u5B58\u7684 Personal Agent";
715
+ return method === "scan" ? "\u521B\u5EFA\u65B0\u7684 Personal Agent" : "\u624B\u52A8\u586B\u5199 App ID / Secret";
716
+ }
717
+ if (method === "saved") return "Use saved Personal Agent";
718
+ return method === "scan" ? "Create a new Personal Agent" : "Manual credentials";
719
+ }
720
+ function larkSetupHint(language, method) {
721
+ if (language === "zh-CN") {
722
+ if (method === "saved") return "\u63A8\u8350\uFF0C\u4E0D\u9700\u8981\u91CD\u65B0\u626B\u7801";
723
+ return method === "scan" ? "\u6CA1\u6709\u5DF2\u4FDD\u5B58\u914D\u7F6E\u65F6\u4F7F\u7528" : "\u5DF2\u6709\u81EA\u5EFA\u5E94\u7528\u65F6\u4F7F\u7528";
724
+ }
725
+ if (method === "saved") return "Recommended; no new scan";
726
+ return method === "scan" ? "Use when no saved app exists" : "Use an existing app";
727
+ }
728
+ function slackModeLabel(language, mode) {
729
+ if (language === "zh-CN") {
730
+ return mode === "socket_mode" ? "\u672C\u5730 Socket Mode\uFF08\u63A8\u8350\uFF09" : "\u516C\u7F51 Events API";
731
+ }
732
+ return mode === "socket_mode" ? "Local Socket Mode (Recommended)" : "Public Events API";
733
+ }
734
+ function slackModeHint(language, mode) {
735
+ if (language === "zh-CN") {
736
+ return mode === "socket_mode" ? "\u9002\u5408\u672C\u673A\u8FD0\u884C\uFF0C\u4E0D\u9700\u8981\u516C\u7F51 URL" : "\u9002\u5408\u4E91\u7AEF\u90E8\u7F72\u6216 tunnel \u6D4B\u8BD5";
737
+ }
738
+ return mode === "socket_mode" ? "Best for this computer; no public URL" : "Best for hosted OpenTag or tunnel testing";
739
+ }
740
+ function bindingMethodLabel(language, method, platform = "lark") {
741
+ if (language === "zh-CN") {
742
+ if (method === "default_project") return "\u9ED8\u8BA4\u4F7F\u7528\u8FD9\u4E2A\u9879\u76EE";
743
+ return platform === "slack" ? "\u7A0D\u540E\u5728 OpenTag \u914D\u7F6E\u91CC\u7ED1\u5B9A" : "\u7A0D\u540E\u5728 Lark \u91CC\u7528 /bind \u7ED1\u5B9A";
744
+ }
745
+ if (method === "default_project") return "Use this project by default";
746
+ return platform === "slack" ? "Bind later from OpenTag config" : "Bind later from Lark with /bind";
747
+ }
748
+ function bindingMethodHint(language, method, platform = "lark") {
749
+ if (language === "zh-CN") {
750
+ if (method === "default_project") return "\u63A8\u8350\uFF0C\u6700\u5FEB\u8DD1\u901A";
751
+ return platform === "slack" ? "\u9002\u5408\u5148\u53EA\u4FDD\u5B58 Slack \u8FDE\u63A5" : "\u9002\u5408\u591A\u4E2A\u9879\u76EE";
752
+ }
753
+ if (method === "default_project") return "Recommended";
754
+ return platform === "slack" ? "Use when you only want to save the Slack connection first" : "Best for multiple projects";
755
+ }
756
+
757
+ // src/setup/defaults.ts
758
+ import { existsSync as existsSync3 } from "fs";
759
+ function defaultBindingMethod(config) {
760
+ const lastSetup = config.preferences?.lastSetup;
761
+ if (lastSetup?.bindingMethod) return lastSetup.bindingMethod;
762
+ if (config.platforms.lark?.defaultProjectBinding === false) return "bind_later";
763
+ if (config.platforms.lark) return "default_project";
764
+ if (config.platforms.slack?.defaultProjectBinding === false) return "bind_later";
765
+ if (config.platforms.slack) return "default_project";
766
+ return void 0;
767
+ }
768
+ function setupDefaultsFromConfig(config) {
769
+ const repository = config.daemon.repositories[0];
770
+ const lark = config.platforms.lark;
771
+ const slack = config.platforms.slack;
772
+ const github = config.platforms.github;
773
+ const lastSetup = config.preferences?.lastSetup;
774
+ const savedLarkCredentials = savedLarkCredentialsFromCliConfig(config);
775
+ const bindingMethod = defaultBindingMethod(config);
776
+ return {
777
+ ...config.preferences?.language ? { language: config.preferences.language } : {},
778
+ ...lastSetup?.platforms?.[0] ? { platform: lastSetup.platforms[0] } : lark ? { platform: "lark" } : slack ? { platform: "slack" } : github ? { platform: "github" } : {},
779
+ ...repository?.checkoutPath ? { projectPath: repository.checkoutPath } : {},
780
+ ...repository?.defaultExecutor ? { executor: repository.defaultExecutor } : {},
781
+ ...lastSetup?.larkSetupMethod ? { larkSetupMethod: lastSetup.larkSetupMethod } : {},
782
+ ...lastSetup?.larkDomain ? { larkDomain: lastSetup.larkDomain } : lark?.domain ? { larkDomain: lark.domain } : {},
783
+ ...bindingMethod ? { bindingMethod } : {},
784
+ ...lastSetup?.slackMode ? { slackMode: lastSetup.slackMode } : slack ? { slackMode: slack.mode ?? "events_api" } : {},
785
+ ...lastSetup?.slackTeamId ? { slackTeamId: lastSetup.slackTeamId } : slack?.teamId ? { slackTeamId: slack.teamId } : {},
786
+ ...lastSetup?.slackChannelId ? { slackChannelId: lastSetup.slackChannelId } : slack?.channelId ? { slackChannelId: slack.channelId } : {},
787
+ ...lastSetup?.slackPort ? { slackPort: lastSetup.slackPort } : slack?.port ? { slackPort: slack.port } : {},
788
+ ...lastSetup?.githubOwner ? { githubOwner: lastSetup.githubOwner } : github?.owner ? { githubOwner: github.owner } : {},
789
+ ...lastSetup?.githubRepo ? { githubRepo: lastSetup.githubRepo } : github?.repo ? { githubRepo: github.repo } : {},
790
+ ...lastSetup?.githubPort ? { githubPort: lastSetup.githubPort } : github?.port ? { githubPort: github.port } : {},
791
+ ...github?.webhookSecret ? { githubWebhookSecret: github.webhookSecret } : {},
792
+ ...github?.webhookPath ? { githubWebhookPath: github.webhookPath } : {},
793
+ ...lastSetup?.githubAutoCreatePullRequest !== void 0 ? { githubAutoCreatePullRequest: lastSetup.githubAutoCreatePullRequest } : config.daemon.allowAutoCreatePullRequest !== void 0 ? { githubAutoCreatePullRequest: config.daemon.allowAutoCreatePullRequest } : {},
794
+ ...savedLarkCredentials ? { savedLarkCredentials } : {}
795
+ };
796
+ }
797
+ function loadSetupDefaults(path = defaultConfigPath()) {
798
+ if (!existsSync3(path)) {
799
+ return {};
800
+ }
801
+ return setupDefaultsFromConfig(readCliConfig(path));
802
+ }
803
+
804
+ // src/setup/guides.ts
805
+ var OFFICIAL_SETUP_LINKS = {
806
+ githubTokenPage: "https://github.com/settings/personal-access-tokens/new",
807
+ githubTokenDocs: "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens",
808
+ githubWebhookDocs: "https://docs.github.com/en/webhooks/using-webhooks/creating-webhooks",
809
+ slackApps: "https://api.slack.com/apps",
810
+ slackSocketModeDocs: "https://docs.slack.dev/apis/events-api/using-socket-mode/",
811
+ slackQuickstartDocs: "https://docs.slack.dev/quickstart/",
812
+ slackSigningSecretDocs: "https://docs.slack.dev/authentication/verifying-requests-from-slack/",
813
+ larkConsole: "https://open.larksuite.com/app",
814
+ feishuConsole: "https://open.feishu.cn/app",
815
+ larkAppIdDocs: "https://open.larksuite.com/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-app-id",
816
+ larkWebSocketDocs: "https://open.larksuite.com/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/use-websocket",
817
+ feishuWebSocketDocs: "https://open.feishu.cn/document/server-docs/event-subscription-guide/event-subscription-configure-/use-websocket?lang=zh-CN"
818
+ };
819
+ function setupNeeds(platform, language) {
820
+ if (language === "zh-CN") {
821
+ switch (platform) {
822
+ case "lark":
823
+ return ["\u63A8\u8350\u76F4\u63A5\u626B\u7801\u521B\u5EFA Personal Agent", "\u624B\u52A8\u914D\u7F6E\u65F6\u9700\u8981 Lark App ID \u548C App Secret"];
824
+ case "slack":
825
+ return ["\u63A8\u8350\u672C\u5730\u4F7F\u7528 Socket Mode", "Socket Mode \u9700\u8981 Slack App-Level Token \u548C Bot User OAuth Token", "Events API \u9700\u8981 Slack Signing Secret \u548C\u516C\u7F51 Request URL", "Slack bot scopes \u9700\u8981 app_mentions:read\u3001chat:write\u3001channels:history", "\u8BA2\u9605 bot events: app_mention\u3001message.channels", "Slack Team ID", "Slack Channel ID", "\u6D4B\u8BD5\u524D\u9700\u8981\u628A Slack app \u9080\u8BF7\u8FDB\u76EE\u6807 channel"];
826
+ case "github":
827
+ return ["GitHub \u4ED3\u5E93 owner/repo", "GitHub token\uFF08\u7528\u4E8E\u56DE\u5199\u8BC4\u8BBA\uFF1B\u4F60\u56DE\u590D apply 1 \u540E\u4E5F\u7528\u4E8E\u521B\u5EFA PR\uFF09", "OpenTag \u4F1A\u81EA\u52A8\u751F\u6210 webhook secret", "\u672C\u5730 webhook \u7AEF\u53E3\uFF0C\u9ED8\u8BA4 3050", "\u9700\u8981\u4E00\u4E2A\u516C\u7F51 tunnel \u8F6C\u53D1 GitHub webhook"];
828
+ case "telegram":
829
+ return [];
830
+ }
831
+ }
832
+ switch (platform) {
833
+ case "lark":
834
+ return ["QR scan is the recommended path", "manual setup needs a Lark App ID and App Secret"];
835
+ case "slack":
836
+ return ["Socket Mode is recommended for local OpenTag", "Socket Mode needs a Slack App-Level Token and Bot User OAuth Token", "Events API needs a Slack Signing Secret and public Request URL", "Slack bot scopes need app_mentions:read, chat:write, channels:history", "Subscribe to bot events: app_mention, message.channels", "Slack Team ID", "Slack Channel ID", "Invite the Slack app to the target channel before testing"];
837
+ case "github":
838
+ return ["GitHub repository owner/repo", "GitHub token for comments and PR creation after you reply `apply 1`", "OpenTag generates the webhook secret", "Local webhook port, default 3050", "A public tunnel is required for GitHub webhook delivery"];
839
+ case "telegram":
840
+ return [];
841
+ }
842
+ }
843
+ function officialSetupLinks(platform, language) {
844
+ if (language === "zh-CN") {
845
+ switch (platform) {
846
+ case "lark":
847
+ return [
848
+ `Lark \u5F00\u53D1\u8005\u540E\u53F0: ${OFFICIAL_SETUP_LINKS.larkConsole}`,
849
+ `\u98DE\u4E66\u5F00\u53D1\u8005\u540E\u53F0: ${OFFICIAL_SETUP_LINKS.feishuConsole}`
850
+ ];
851
+ case "slack":
852
+ return [
853
+ `Slack App \u7BA1\u7406\u9875: ${OFFICIAL_SETUP_LINKS.slackApps}`,
854
+ `Socket Mode \u5B98\u65B9\u6587\u6863: ${OFFICIAL_SETUP_LINKS.slackSocketModeDocs}`
855
+ ];
856
+ case "github":
857
+ return [
858
+ `GitHub token \u521B\u5EFA\u9875: ${OFFICIAL_SETUP_LINKS.githubTokenPage}`,
859
+ `Repository webhook \u5B98\u65B9\u6587\u6863: ${OFFICIAL_SETUP_LINKS.githubWebhookDocs}`
860
+ ];
861
+ case "telegram":
862
+ return [];
863
+ }
864
+ }
865
+ switch (platform) {
866
+ case "lark":
867
+ return [
868
+ `Lark Developer Console: ${OFFICIAL_SETUP_LINKS.larkConsole}`,
869
+ `Feishu Developer Console: ${OFFICIAL_SETUP_LINKS.feishuConsole}`
870
+ ];
871
+ case "slack":
872
+ return [
873
+ `Slack app settings: ${OFFICIAL_SETUP_LINKS.slackApps}`,
874
+ `Socket Mode docs: ${OFFICIAL_SETUP_LINKS.slackSocketModeDocs}`
875
+ ];
876
+ case "github":
877
+ return [
878
+ `GitHub token page: ${OFFICIAL_SETUP_LINKS.githubTokenPage}`,
879
+ `Repository webhook docs: ${OFFICIAL_SETUP_LINKS.githubWebhookDocs}`
880
+ ];
881
+ case "telegram":
882
+ return [];
883
+ }
884
+ }
885
+ function formatPlatformSetupGuide(platform, language) {
886
+ const url = platformSetupGuideUrl(platform, language);
887
+ if (!url) return void 0;
888
+ const descriptor = platformById(platform);
889
+ const needs = setupNeeds(platform, language);
890
+ const officialLinks = officialSetupLinks(platform, language);
891
+ if (language === "zh-CN") {
892
+ return [
893
+ `${descriptor.label} \u914D\u7F6E\u6559\u7A0B:`,
894
+ url,
895
+ "",
896
+ "\u5B98\u65B9\u5165\u53E3:",
897
+ ...officialLinks.map((item) => `- ${item}`),
898
+ "",
899
+ "\u7EE7\u7EED\u586B\u5199\u524D\uFF0C\u5148\u6253\u5F00\u6559\u7A0B\u786E\u8BA4\u8FD9\u4E9B\u503C\u5728\u54EA\u91CC\u62FF\uFF1A",
900
+ ...needs.map((item) => `- ${item}`)
901
+ ].join("\n");
902
+ }
903
+ return [
904
+ `${descriptor.label} setup guide:`,
905
+ url,
906
+ "",
907
+ "Official setup pages:",
908
+ ...officialLinks.map((item) => `- ${item}`),
909
+ "",
910
+ "Open the guide before filling in these values:",
911
+ ...needs.map((item) => `- ${item}`)
912
+ ].join("\n");
913
+ }
914
+ function formatLarkManualCredentialHelp(language, domain) {
915
+ const consoleUrl = domain === "feishu" ? OFFICIAL_SETUP_LINKS.feishuConsole : OFFICIAL_SETUP_LINKS.larkConsole;
916
+ const websocketDocs = domain === "feishu" ? OFFICIAL_SETUP_LINKS.feishuWebSocketDocs : OFFICIAL_SETUP_LINKS.larkWebSocketDocs;
917
+ if (language === "zh-CN") {
918
+ return [
919
+ "\u624B\u52A8 Lark / \u98DE\u4E66\u51ED\u636E\u5728\u54EA\u91CC\u62FF:",
920
+ `- \u5F00\u53D1\u8005\u540E\u53F0: ${consoleUrl}`,
921
+ "- App ID / App Secret: \u6253\u5F00\u4F60\u7684\u5E94\u7528\uFF0C\u8FDB\u5165 Credentials & Basic Info / \u51ED\u8BC1\u4E0E\u57FA\u7840\u4FE1\u606F",
922
+ "- \u4E8B\u4EF6\u63A5\u6536\u65B9\u5F0F: \u4F7F\u7528\u957F\u8FDE\u63A5 / WebSocket",
923
+ `- \u957F\u8FDE\u63A5\u5B98\u65B9\u6587\u6863: ${websocketDocs}`,
924
+ "",
925
+ "\u5982\u679C\u4F60\u6CA1\u6709\u81EA\u5EFA\u5E94\u7528\uFF0C\u5EFA\u8BAE\u8FD4\u56DE\u9009\u62E9\u626B\u7801\u521B\u5EFA Personal Agent\u3002"
926
+ ].join("\n");
927
+ }
928
+ return [
929
+ "Where to find manual Lark / Feishu credentials:",
930
+ `- Developer console: ${consoleUrl}`,
931
+ "- App ID / App Secret: open your app, then go to Credentials & Basic Info",
932
+ "- Event delivery mode: use long connection / WebSocket",
933
+ `- WebSocket docs: ${websocketDocs}`,
934
+ "",
935
+ "If you do not already manage a self-built app, use QR scan instead."
936
+ ].join("\n");
937
+ }
938
+ function formatSlackCredentialHelp(language, mode) {
939
+ if (language === "zh-CN") {
940
+ const modeSpecific2 = mode === "socket_mode" ? [
941
+ `- Socket Mode \u5B98\u65B9\u6587\u6863: ${OFFICIAL_SETUP_LINKS.slackSocketModeDocs}`,
942
+ "- Slack App-Level Token: Basic Information -> App-Level Tokens -> Generate Token and Scopes\uFF0Cscope \u9009 connections:write",
943
+ "- Socket Mode \u4E0D\u9700\u8981 Request URL"
944
+ ] : [
945
+ `- Signing Secret \u5B98\u65B9\u6587\u6863: ${OFFICIAL_SETUP_LINKS.slackSigningSecretDocs}`,
946
+ "- Slack Signing Secret: Basic Information -> App Credentials",
947
+ "- Request URL: \u586B\u4F60\u7684\u516C\u7F51 tunnel\uFF0C\u4F8B\u5982 https://<your-tunnel>/slack/events"
948
+ ];
949
+ return [
950
+ "Slack \u8FD9\u4E9B\u503C\u5728\u54EA\u91CC\u62FF:",
951
+ `- Slack App \u7BA1\u7406\u9875: ${OFFICIAL_SETUP_LINKS.slackApps}`,
952
+ ...modeSpecific2,
953
+ "- Slack Bot User OAuth Token: OAuth & Permissions -> Bot User OAuth Token",
954
+ "- Bot Token Scopes: app_mentions:read, chat:write, channels:history",
955
+ "- Bot Events: app_mention, message.channels",
956
+ "- Team ID / Channel ID: \u7528\u6D4F\u89C8\u5668\u6253\u5F00 Slack channel\uFF0C\u4ECE\u5730\u5740\u91CC\u590D\u5236 T... \u548C C...",
957
+ "- \u6D4B\u8BD5\u524D\u5728\u76EE\u6807 channel \u91CC\u8FD0\u884C /invite @\u4F60\u7684 App \u540D\u79F0\uFF0C\u628A app \u9080\u8BF7\u8FDB channel"
958
+ ].join("\n");
959
+ }
960
+ const modeSpecific = mode === "socket_mode" ? [
961
+ `- Socket Mode docs: ${OFFICIAL_SETUP_LINKS.slackSocketModeDocs}`,
962
+ "- Slack App-Level Token: Basic Information -> App-Level Tokens -> Generate Token and Scopes, then add connections:write",
963
+ "- Socket Mode does not need a Request URL"
964
+ ] : [
965
+ `- Signing Secret docs: ${OFFICIAL_SETUP_LINKS.slackSigningSecretDocs}`,
966
+ "- Slack Signing Secret: Basic Information -> App Credentials",
967
+ "- Request URL: use your public tunnel, for example https://<your-tunnel>/slack/events"
968
+ ];
969
+ return [
970
+ "Where to find these Slack values:",
971
+ `- Slack app settings: ${OFFICIAL_SETUP_LINKS.slackApps}`,
972
+ ...modeSpecific,
973
+ "- Slack Bot User OAuth Token: OAuth & Permissions -> Bot User OAuth Token",
974
+ "- Bot Token Scopes: app_mentions:read, chat:write, channels:history",
975
+ "- Bot Events: app_mention, message.channels",
976
+ "- Team ID / Channel ID: open the Slack channel in a browser and copy the T... and C... values from the URL",
977
+ "- Before testing, run /invite @your app name in the target channel so Slack sends mentions to the app"
978
+ ].join("\n");
979
+ }
980
+ function formatGitHubTokenHelp(language, input) {
981
+ if (language === "zh-CN") {
982
+ const permissions2 = input.autoCreatePullRequest ? ["- Issues: Read and write", "- Pull requests: Read and write", "- Contents: Read and write"] : ["- Issues: Read and write", "- Pull requests: Read and write", "- Contents: \u9ED8\u8BA4 apply 1 \u6D41\u7A0B\u4E0D\u9700\u8981\uFF1Brun branch \u4F1A\u4F7F\u7528\u672C\u673A git remote \u51ED\u636E\u63A8\u9001"];
983
+ return [
984
+ "GitHub token \u5728\u54EA\u91CC\u521B\u5EFA:",
985
+ `- \u76F4\u63A5\u6253\u5F00: ${OFFICIAL_SETUP_LINKS.githubTokenPage}`,
986
+ `- \u5B98\u65B9\u6559\u7A0B: ${OFFICIAL_SETUP_LINKS.githubTokenDocs}`,
987
+ "",
988
+ "\u63A8\u8350\u521B\u5EFA fine-grained personal access token\uFF0C\u53EA\u6388\u6743\u5F53\u524D\u4ED3\u5E93\u3002\u9700\u8981\u6743\u9650:",
989
+ ...permissions2,
990
+ "",
991
+ "GitHub \u53EA\u4F1A\u5C55\u793A token \u4E00\u6B21\uFF0C\u521B\u5EFA\u540E\u9A6C\u4E0A\u590D\u5236\u5E76\u7C98\u8D34\u5230\u4E0B\u4E00\u6B65\u3002"
992
+ ].join("\n");
993
+ }
994
+ const permissions = input.autoCreatePullRequest ? ["- Issues: Read and write", "- Pull requests: Read and write", "- Contents: Read and write"] : ["- Issues: Read and write", "- Pull requests: Read and write", "- Contents: not needed for the default apply-1 flow; branch push uses your local git remote credentials"];
995
+ return [
996
+ "Where to create the GitHub token:",
997
+ `- Direct token page: ${OFFICIAL_SETUP_LINKS.githubTokenPage}`,
998
+ `- Official guide: ${OFFICIAL_SETUP_LINKS.githubTokenDocs}`,
999
+ "",
1000
+ "Create a fine-grained personal access token and limit it to this repository. Required permissions:",
1001
+ ...permissions,
1002
+ "",
1003
+ "GitHub only shows the token once. Copy it immediately, then paste it into the next prompt."
1004
+ ].join("\n");
1005
+ }
1006
+
1007
+ // src/platforms/github/display.ts
1008
+ function githubLocalWebhookUrl(input) {
1009
+ return `http://127.0.0.1:${input.port ?? DEFAULT_GITHUB_WEBHOOK_PORT}${input.webhookPath ?? "/github/webhooks"}`;
1010
+ }
1011
+ function githubPublicWebhookUrlPlaceholder(webhookPath = "/github/webhooks") {
1012
+ return `https://<your-tunnel-host>${webhookPath}`;
1013
+ }
1014
+ function githubWebhooksSettingsUrl(input) {
1015
+ return `https://github.com/${input.owner}/${input.repo}/settings/hooks`;
1016
+ }
1017
+
1018
+ // src/setup/summary.ts
1019
+ function yesNo(value, language) {
1020
+ return language === "zh-CN" ? value ? "\u662F" : "\u5426" : value ? "yes" : "no";
1021
+ }
1022
+ function larkSetupDescription(method, language) {
1023
+ if (language === "zh-CN") {
1024
+ if (method === "saved") return "\u4F7F\u7528\u5DF2\u4FDD\u5B58\u7684 Personal Agent";
1025
+ return method === "scan" ? "\u521B\u5EFA\u65B0\u7684 Personal Agent" : "\u624B\u52A8\u586B\u5199";
1026
+ }
1027
+ if (method === "saved") return "Saved Personal Agent";
1028
+ return method === "scan" ? "Create new Personal Agent" : "Manual credentials";
1029
+ }
1030
+ function formatSetupReview(input, configPath) {
1031
+ const platform = platformById(input.platform);
1032
+ const commonLines = input.language === "zh-CN" ? [
1033
+ "\u8BF7\u786E\u8BA4 OpenTag \u914D\u7F6E\uFF1A",
1034
+ `\u914D\u7F6E\u6587\u4EF6: ${configPath}`,
1035
+ `\u5E73\u53F0: ${platform.label}`,
1036
+ `Coding agent: ${executorLabel(input.executor)}`,
1037
+ `\u9879\u76EE\u8DEF\u5F84: ${input.projectPath}`
1038
+ ] : [
1039
+ "Review your OpenTag setup:",
1040
+ `Config: ${configPath}`,
1041
+ `Platform: ${platform.label}`,
1042
+ `Coding agent: ${executorLabel(input.executor)}`,
1043
+ `Project path: ${input.projectPath}`
1044
+ ];
1045
+ const platformLines = [];
1046
+ if (input.lark) {
1047
+ const larkPersonalAgent = formatLarkPersonalAgentSummary(
1048
+ {
1049
+ ...input.lark,
1050
+ ...input.lark.savedCredentialsSource ? { source: input.lark.savedCredentialsSource } : {}
1051
+ },
1052
+ input.language
1053
+ );
1054
+ platformLines.push(
1055
+ ...input.language === "zh-CN" ? [
1056
+ `Lark \u8FDE\u63A5\u65B9\u5F0F: ${larkSetupDescription(input.lark.setupMethod, input.language)}`,
1057
+ `Personal Agent: ${larkPersonalAgent}`,
1058
+ `Lark \u57DF\u540D: ${input.lark.domain}`,
1059
+ `\u9ED8\u8BA4\u7ED1\u5B9A\u5F53\u524D\u9879\u76EE: ${yesNo(input.lark.bindingMethod === "default_project", input.language)}`
1060
+ ] : [
1061
+ `Lark setup: ${larkSetupDescription(input.lark.setupMethod, input.language)}`,
1062
+ `Personal Agent: ${larkPersonalAgent}`,
1063
+ `Lark domain: ${input.lark.domain}`,
1064
+ `Default project binding: ${yesNo(input.lark.bindingMethod === "default_project", input.language)}`
1065
+ ]
1066
+ );
1067
+ }
1068
+ if (input.slack) {
1069
+ const slackConnectionLines = input.slack.mode === "socket_mode" ? input.language === "zh-CN" ? ["Slack \u8FDE\u63A5\u65B9\u5F0F: \u672C\u5730 Socket Mode", "Slack \u5165\u7AD9: \u901A\u8FC7 Slack WebSocket\uFF0C\u4E0D\u9700\u8981\u516C\u7F51 URL"] : ["Slack connection: Local Socket Mode", "Slack ingress: Slack WebSocket; no public URL required"] : input.language === "zh-CN" ? ["Slack \u8FDE\u63A5\u65B9\u5F0F: \u516C\u7F51 Events API", `Slack Events URL: http://localhost:${input.slack.port ?? DEFAULT_SLACK_EVENTS_PORT}/slack/events`] : ["Slack connection: Public Events API", `Slack Events URL: http://localhost:${input.slack.port ?? DEFAULT_SLACK_EVENTS_PORT}/slack/events`];
1070
+ platformLines.push(
1071
+ ...input.language === "zh-CN" ? [
1072
+ ...slackConnectionLines,
1073
+ `Slack Team ID: ${input.slack.teamId}`,
1074
+ `Slack Channel ID: ${input.slack.channelId}`,
1075
+ `\u9ED8\u8BA4\u7ED1\u5B9A\u5F53\u524D\u9879\u76EE: ${yesNo(input.slack.bindingMethod === "default_project", input.language)}`
1076
+ ] : [
1077
+ ...slackConnectionLines,
1078
+ `Slack Team ID: ${input.slack.teamId}`,
1079
+ `Slack Channel ID: ${input.slack.channelId}`,
1080
+ `Default project binding: ${yesNo(input.slack.bindingMethod === "default_project", input.language)}`
1081
+ ]
1082
+ );
1083
+ }
1084
+ if (input.github) {
1085
+ const webhookPath = input.github.webhookPath;
1086
+ platformLines.push(
1087
+ ...input.language === "zh-CN" ? [
1088
+ `GitHub \u4ED3\u5E93: ${input.github.owner}/${input.github.repo}`,
1089
+ `GitHub \u672C\u5730 webhook: ${githubLocalWebhookUrl({ port: input.github.port, webhookPath })}`,
1090
+ `GitHub Payload URL: ${githubPublicWebhookUrlPlaceholder(webhookPath)}`,
1091
+ "Webhook secret: OpenTag \u4F1A\u81EA\u52A8\u751F\u6210",
1092
+ "\u9ED8\u8BA4 PR \u6D41\u7A0B: \u56DE\u590D apply 1 \u540E\u521B\u5EFA",
1093
+ `run \u540E\u7ACB\u523B\u81EA\u52A8\u521B\u5EFA PR: ${yesNo(input.github.autoCreatePullRequest, input.language)}`
1094
+ ] : [
1095
+ `GitHub repository: ${input.github.owner}/${input.github.repo}`,
1096
+ `GitHub local webhook: ${githubLocalWebhookUrl({ port: input.github.port, webhookPath })}`,
1097
+ `GitHub Payload URL: ${githubPublicWebhookUrlPlaceholder(webhookPath)}`,
1098
+ "Webhook secret: generated by OpenTag",
1099
+ "Default PR flow: create after replying `apply 1`",
1100
+ `Immediate PR after run: ${yesNo(input.github.autoCreatePullRequest, input.language)}`
1101
+ ]
1102
+ );
1103
+ }
1104
+ const lines = [...commonLines, ...platformLines];
1105
+ return lines.join("\n");
1106
+ }
1107
+ function formatSetupComplete(config, configPath) {
1108
+ const repository = config.daemon.repositories[0];
1109
+ const language = config.preferences?.language ?? "en";
1110
+ const github = config.platforms.github;
1111
+ const slack = config.platforms.slack;
1112
+ const githubPort = github?.port ?? DEFAULT_GITHUB_WEBHOOK_PORT;
1113
+ if (language === "zh-CN") {
1114
+ return [
1115
+ "OpenTag \u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002",
1116
+ `\u914D\u7F6E\u6587\u4EF6: ${configPath}`,
1117
+ repository ? `\u9879\u76EE\u8DEF\u5F84: ${repository.checkoutPath}` : void 0,
1118
+ slack ? "" : void 0,
1119
+ slack ? "Slack \u4E0B\u4E00\u6B65\uFF1A" : void 0,
1120
+ slack ? `\u7ED1\u5B9A channel: ${slack.teamId}/${slack.channelId}` : void 0,
1121
+ slack ? "\u6D4B\u8BD5\u524D\u5148\u5728 Slack channel \u91CC\u8FD0\u884C /invite @\u4F60\u7684 App \u540D\u79F0\u3002" : void 0,
1122
+ slack?.mode === "socket_mode" ? "Socket Mode \u4E0D\u9700\u8981\u516C\u7F51 URL\uFF1B\u76F4\u63A5\u4FDD\u6301 opentag start \u8FD0\u884C\u5373\u53EF\u3002" : void 0,
1123
+ slack?.mode !== "socket_mode" && slack ? `Events API \u672C\u5730\u76D1\u542C: http://localhost:${slack.port ?? DEFAULT_SLACK_EVENTS_PORT}/slack/events` : void 0,
1124
+ github ? "" : void 0,
1125
+ github ? "GitHub webhook \u4E0B\u4E00\u6B65\uFF1A" : void 0,
1126
+ github ? `GitHub \u8BBE\u7F6E\u9875: ${githubWebhooksSettingsUrl(github)}` : void 0,
1127
+ github ? `Payload URL: ${githubPublicWebhookUrlPlaceholder(github.webhookPath ?? "/github/webhooks")}` : void 0,
1128
+ github ? `Secret: ${github.webhookSecret}` : void 0,
1129
+ github ? "Content type: application/json" : void 0,
1130
+ github ? "Events: Issue comments, Pull request review comments" : void 0,
1131
+ github ? `\u672C\u5730\u76D1\u542C: ${githubLocalWebhookUrl({ port: github.port, webhookPath: github.webhookPath })}` : void 0,
1132
+ github ? `\u516C\u7F51 URL \u9700\u8981\u7531 tunnel \u6307\u5411\u672C\u5730\u76D1\u542C\u5730\u5740\uFF0C\u4F8B\u5982 ngrok http ${githubPort}\u3002` : void 0,
1133
+ github ? "\u5F53 OpenTag \u7ED9\u51FA create_pull_request \u5EFA\u8BAE\u52A8\u4F5C\u540E\uFF0C\u5728 thread \u91CC\u56DE\u590D apply 1 \u521B\u5EFA PR\u3002" : void 0
1134
+ ].filter((line) => Boolean(line)).join("\n");
1135
+ }
1136
+ return [
1137
+ "OpenTag config saved.",
1138
+ `Config: ${configPath}`,
1139
+ repository ? `Project path: ${repository.checkoutPath}` : void 0,
1140
+ slack ? "" : void 0,
1141
+ slack ? "Slack next steps:" : void 0,
1142
+ slack ? `Bound channel: ${slack.teamId}/${slack.channelId}` : void 0,
1143
+ slack ? "Before testing, run /invite @your app name in that Slack channel." : void 0,
1144
+ slack?.mode === "socket_mode" ? "Socket Mode does not need a public URL; keep opentag start running." : void 0,
1145
+ slack?.mode !== "socket_mode" && slack ? `Events API local listener: http://localhost:${slack.port ?? DEFAULT_SLACK_EVENTS_PORT}/slack/events` : void 0,
1146
+ github ? "" : void 0,
1147
+ github ? "GitHub webhook next steps:" : void 0,
1148
+ github ? `GitHub settings: ${githubWebhooksSettingsUrl(github)}` : void 0,
1149
+ github ? `Payload URL: ${githubPublicWebhookUrlPlaceholder(github.webhookPath ?? "/github/webhooks")}` : void 0,
1150
+ github ? `Secret: ${github.webhookSecret}` : void 0,
1151
+ github ? "Content type: application/json" : void 0,
1152
+ github ? "Events: Issue comments, Pull request review comments" : void 0,
1153
+ github ? `Local listener: ${githubLocalWebhookUrl({ port: github.port, webhookPath: github.webhookPath })}` : void 0,
1154
+ github ? `Point a public tunnel at the local listener, for example: ngrok http ${githubPort}.` : void 0,
1155
+ github ? "When OpenTag shows a create_pull_request action, reply `apply 1` in the thread to create the PR." : void 0
1156
+ ].filter((line) => Boolean(line)).join("\n");
1157
+ }
1158
+
1159
+ // src/setup/flow.ts
1160
+ function parseLarkSetupMethod(value) {
1161
+ if (value === "saved" || value === "scan" || value === "manual") return value;
1162
+ throw new Error("Lark setup method must be saved, scan, or manual.");
1163
+ }
1164
+ function parseLarkDomain(value) {
1165
+ if (value === "lark" || value === "feishu") return value;
1166
+ throw new Error("Lark domain must be lark or feishu.");
1167
+ }
1168
+ function parseSlackSetupMode(value) {
1169
+ const normalized = value.trim().toLowerCase().replace(/-/g, "_");
1170
+ if (normalized === "socket_mode" || normalized === "events_api") return normalized;
1171
+ throw new Error("Slack mode must be socket_mode or events_api.");
1172
+ }
1173
+ function parseBindingMethod(value) {
1174
+ if (value === "default_project" || value === "bind_later") return value;
1175
+ throw new Error("Binding method must be default_project or bind_later.");
1176
+ }
1177
+ function parseGitHubRepository(value) {
1178
+ const trimmed = value.trim().replace(/^github:/, "");
1179
+ const match = trimmed.match(/^([^/\s]+)\/([^/\s]+)$/);
1180
+ if (!match) {
1181
+ throw new Error("GitHub repository must use owner/repo.");
1182
+ }
1183
+ return {
1184
+ owner: match[1],
1185
+ repo: match[2].replace(/\.git$/, "")
1186
+ };
1187
+ }
1188
+ function parsePortInput(value, label) {
1189
+ return value === void 0 ? void 0 : parseLocalPort(value, label);
1190
+ }
1191
+ function githubRepositoryFromRemote(projectPath) {
1192
+ let remote;
1193
+ try {
1194
+ remote = execFileSync("git", ["-C", projectPath, "remote", "get-url", "origin"], {
1195
+ encoding: "utf8",
1196
+ stdio: ["ignore", "pipe", "ignore"]
1197
+ }).trim();
1198
+ } catch {
1199
+ return void 0;
1200
+ }
1201
+ const patterns = [
1202
+ /^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?$/,
1203
+ /^https:\/\/github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?$/,
1204
+ /^ssh:\/\/git@github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?$/
1205
+ ];
1206
+ for (const pattern of patterns) {
1207
+ const match = remote.match(pattern);
1208
+ if (match) {
1209
+ return `${match[1]}/${match[2]}`;
1210
+ }
1211
+ }
1212
+ return void 0;
1213
+ }
1214
+ function nonEmpty(value, label) {
1215
+ const trimmed = value.trim();
1216
+ if (!trimmed) {
1217
+ throw new Error(`${label} is required.`);
1218
+ }
1219
+ return trimmed;
1220
+ }
1221
+ function assertExistingPath(path) {
1222
+ if (!existsSync4(path)) {
1223
+ throw new Error(`Path does not exist: ${path}`);
1224
+ }
1225
+ return path;
1226
+ }
1227
+ function optionalTrimmed(value) {
1228
+ const trimmed = value?.trim();
1229
+ return trimmed ? trimmed : void 0;
1230
+ }
1231
+ function generateGitHubWebhookSecret() {
1232
+ return randomBytes2(32).toString("hex");
1233
+ }
1234
+ function parseGitHubWebhookPath(value) {
1235
+ const trimmed = nonEmpty(value, "GitHub webhook path");
1236
+ if (!trimmed.startsWith("/")) {
1237
+ throw new Error("GitHub webhook path must start with /.");
1238
+ }
1239
+ return trimmed;
1240
+ }
1241
+ function hasManualLarkCredentials(options) {
1242
+ return Boolean(options.larkAppId || options.larkAppSecret || options.larkBotOpenId);
1243
+ }
1244
+ function hasCompleteManualLarkCredentials(options) {
1245
+ return Boolean(options.larkAppId && options.larkAppSecret);
1246
+ }
1247
+ function assertCompleteManualLarkCredentials(options) {
1248
+ if (options.larkAppId && !options.larkAppSecret) {
1249
+ throw new Error("--lark-app-secret is required when --lark-app-id is provided.");
1250
+ }
1251
+ if (options.larkAppSecret && !options.larkAppId) {
1252
+ throw new Error("--lark-app-id is required when --lark-app-secret is provided.");
1253
+ }
1254
+ }
1255
+ function assertNoManualLarkCredentialFlags(options) {
1256
+ if (hasManualLarkCredentials(options)) {
1257
+ throw new Error("--lark-app-id, --lark-app-secret, and --lark-bot-open-id can only be used with --lark-setup manual.");
1258
+ }
1259
+ }
1260
+ function findSavedLarkCredentials(defaults, projectPath) {
1261
+ return defaults.savedLarkCredentials ?? readLegacyLarkCredentials(projectPath);
1262
+ }
1263
+ function shouldReadSavedLarkCredentials(options) {
1264
+ return !options.larkSetup || parseLarkSetupMethod(options.larkSetup) === "saved";
1265
+ }
1266
+ function loadDefaultsForSetup(options, configPath) {
1267
+ try {
1268
+ return loadSetupDefaults(configPath);
1269
+ } catch (error) {
1270
+ if (options.force) {
1271
+ return {};
1272
+ }
1273
+ throw error;
1274
+ }
1275
+ }
1276
+ function defaultLanguage(options, defaults) {
1277
+ return options.language ? parseCliLanguage(options.language) : defaults.language ?? "en";
1278
+ }
1279
+ function formatPlatformStatusForSetup(language, status) {
1280
+ if (language === "zh-CN") {
1281
+ switch (status) {
1282
+ case "setup_ready":
1283
+ return "\u8FD9\u4E2A setup \u5411\u5BFC\u73B0\u5728\u53EF\u914D\u7F6E";
1284
+ case "setup_pending":
1285
+ return "\u9002\u914D\u5668\u5DF2\u6709\uFF0Csetup \u5411\u5BFC\u5F85\u63A5\u5165";
1286
+ case "experimental_setup_pending":
1287
+ return "\u5B9E\u9A8C\u9002\u914D\u5668\uFF0Csetup \u5411\u5BFC\u5F85\u63A5\u5165";
1288
+ }
1289
+ }
1290
+ return formatPlatformStatus(status);
1291
+ }
1292
+ function formatPlatformsForSetup(language) {
1293
+ const lines = PLATFORM_CATALOG.map((platform) => `- ${platform.label}: ${formatPlatformStatusForSetup(language, platform.status)}`);
1294
+ if (language === "zh-CN") {
1295
+ return ["\u8FD9\u4E2A setup \u5411\u5BFC\u5F53\u524D\u53EF\u914D\u7F6E\u7684\u5E73\u53F0\uFF1A", ...lines].join("\n");
1296
+ }
1297
+ return ["This setup wizard can configure:", ...lines].join("\n");
1298
+ }
1299
+ function formatExecutorHint(input) {
1300
+ if (input.executor.devOnly) {
1301
+ const echoHint = input.language === "zh-CN" ? "\u5F00\u53D1\u6D4B\u8BD5\u7528\uFF0C\u4E0D\u4F1A\u8C03\u7528\u771F\u5B9E coding agent" : "dev/test only; no real coding agent";
1302
+ return input.current ? `${input.language === "zh-CN" ? "\u5F53\u524D\u9009\u62E9\uFF0C" : "current, "}${echoHint}` : echoHint;
1303
+ }
1304
+ const availability = input.language === "zh-CN" ? input.available ? "\u5DF2\u68C0\u6D4B\u5230" : "\u672A\u68C0\u6D4B\u5230" : input.available ? "available" : "not found";
1305
+ const current = input.current ? input.language === "zh-CN" ? "\u5F53\u524D\u9009\u62E9\uFF0C" : "current, " : "";
1306
+ const recommended = input.selectedByDefault ? input.language === "zh-CN" ? "\u63A8\u8350\uFF0C" : "recommended, " : "";
1307
+ return `${current || recommended}${availability}`;
1308
+ }
1309
+ async function collectLanguage(options, defaults, prompts) {
1310
+ if (options.language) {
1311
+ return parseCliLanguage(options.language);
1312
+ }
1313
+ return prompts.select({
1314
+ message: t("en", "language"),
1315
+ initialValue: defaultLanguage(options, defaults),
1316
+ options: LANGUAGE_OPTIONS.map((language) => ({
1317
+ value: language.id,
1318
+ label: language.label,
1319
+ hint: language.hint
1320
+ }))
1321
+ });
1322
+ }
1323
+ async function collectPlatform(options, defaults, prompts, language) {
1324
+ prompts.note(formatPlatformsForSetup(language));
1325
+ const selected = options.platform ? parsePlatformId(options.platform) : await prompts.select({
1326
+ message: t(language, "platform"),
1327
+ initialValue: defaults.platform ?? "lark",
1328
+ options: PLATFORM_CATALOG.filter((platform) => platform.startable).map((platform) => ({
1329
+ value: platform.id,
1330
+ label: platform.label,
1331
+ hint: formatPlatformStatusForSetup(language, platform.status)
1332
+ }))
1333
+ });
1334
+ const descriptor = platformById(selected);
1335
+ if (!descriptor.startable) {
1336
+ throw new Error(`${descriptor.label} setup is not available in the OpenTag CLI yet.`);
1337
+ }
1338
+ const guide = formatPlatformSetupGuide(selected, language);
1339
+ if (guide) {
1340
+ prompts.note(guide);
1341
+ }
1342
+ return selected;
1343
+ }
1344
+ async function collectExecutor(options, defaults, prompts, language, env) {
1345
+ if (options.executor) {
1346
+ return parseExecutorId(options.executor);
1347
+ }
1348
+ const detections = detectExecutors(env);
1349
+ const previous = defaults.executor;
1350
+ const initialValue = defaultExecutorId({
1351
+ ...previous ? { previous } : {},
1352
+ detections
1353
+ });
1354
+ return prompts.select({
1355
+ message: t(language, "executor"),
1356
+ initialValue,
1357
+ options: EXECUTOR_CATALOG.map((executor) => {
1358
+ const detection = detections.find((entry) => entry.id === executor.id);
1359
+ return {
1360
+ value: executor.id,
1361
+ label: executor.label,
1362
+ hint: formatExecutorHint({
1363
+ language,
1364
+ executor,
1365
+ available: detection?.available ?? false,
1366
+ current: executor.id === previous,
1367
+ selectedByDefault: executor.id === initialValue
1368
+ })
1369
+ };
1370
+ })
1371
+ });
1372
+ }
1373
+ async function collectProjectPath(options, defaults, prompts, language, cwd) {
1374
+ if (options.project) {
1375
+ return assertExistingPath(options.project);
1376
+ }
1377
+ const initialValue = defaults.projectPath ?? cwd;
1378
+ return prompts.text({
1379
+ message: t(language, "projectPath"),
1380
+ initialValue,
1381
+ placeholder: initialValue,
1382
+ validate(value) {
1383
+ const candidate = value.trim() || initialValue;
1384
+ if (!existsSync4(candidate)) {
1385
+ return `Path does not exist: ${candidate}`;
1386
+ }
1387
+ return void 0;
1388
+ }
1389
+ });
1390
+ }
1391
+ async function collectLarkSetupMethod(options, defaults, prompts, language, savedLarkCredentials) {
1392
+ if (options.larkSetup) {
1393
+ const setupMethod = parseLarkSetupMethod(options.larkSetup);
1394
+ if (setupMethod === "saved" && !savedLarkCredentials) {
1395
+ throw new Error("No saved Lark Personal Agent config was found. Use --lark-setup scan or --lark-setup manual.");
1396
+ }
1397
+ return setupMethod;
1398
+ }
1399
+ if (hasManualLarkCredentials(options)) {
1400
+ return "manual";
1401
+ }
1402
+ const methods = savedLarkCredentials ? ["saved", "scan", "manual"] : ["scan", "manual"];
1403
+ const previous = defaults.larkSetupMethod && methods.includes(defaults.larkSetupMethod) ? defaults.larkSetupMethod : void 0;
1404
+ return prompts.select({
1405
+ message: t(language, "larkSetup"),
1406
+ initialValue: savedLarkCredentials ? "saved" : previous ?? "scan",
1407
+ options: methods.map((method) => ({
1408
+ value: method,
1409
+ label: larkSetupLabel(language, method),
1410
+ hint: method === "saved" && savedLarkCredentials ? formatSavedLarkCredentialsHint(savedLarkCredentials, language) : larkSetupHint(language, method)
1411
+ }))
1412
+ });
1413
+ }
1414
+ async function collectLarkDomain(options, defaults, prompts, language, setupMethod, savedLarkCredentials) {
1415
+ if (setupMethod === "saved") {
1416
+ if (!savedLarkCredentials) {
1417
+ throw new Error("No saved Lark Personal Agent config was found.");
1418
+ }
1419
+ return savedLarkCredentials.domain;
1420
+ }
1421
+ if (options.larkDomain) {
1422
+ return parseLarkDomain(options.larkDomain);
1423
+ }
1424
+ return prompts.select({
1425
+ message: t(language, "larkDomain"),
1426
+ initialValue: defaults.larkDomain ?? "lark",
1427
+ options: [
1428
+ { value: "lark", label: "Lark", hint: "larksuite.com" },
1429
+ { value: "feishu", label: "Feishu", hint: "feishu.cn" }
1430
+ ]
1431
+ });
1432
+ }
1433
+ async function collectLarkCredentials(input) {
1434
+ if (input.setupMethod === "saved") {
1435
+ if (!input.savedLarkCredentials) {
1436
+ throw new Error("No saved Lark Personal Agent config was found.");
1437
+ }
1438
+ return {
1439
+ appId: input.savedLarkCredentials.appId,
1440
+ appSecret: input.savedLarkCredentials.appSecret,
1441
+ ...input.savedLarkCredentials.botOpenId ? { botOpenId: input.savedLarkCredentials.botOpenId } : {}
1442
+ };
1443
+ }
1444
+ if (input.setupMethod === "scan") {
1445
+ assertNoManualLarkCredentialFlags(input.options);
1446
+ const registered = await input.scanLarkPersonalAgent({ domain: input.domain });
1447
+ return {
1448
+ appId: registered.appId,
1449
+ appSecret: registered.appSecret,
1450
+ ...registered.botOpenId ? { botOpenId: registered.botOpenId } : {}
1451
+ };
1452
+ }
1453
+ assertCompleteManualLarkCredentials(input.options);
1454
+ if (!hasCompleteManualLarkCredentials(input.options)) {
1455
+ input.prompts.note(formatLarkManualCredentialHelp(input.language, input.domain));
1456
+ }
1457
+ const appId = nonEmpty(input.options.larkAppId ?? await input.prompts.text({ message: t(input.language, "larkAppId") }), "Lark App ID");
1458
+ const appSecret = nonEmpty(
1459
+ input.options.larkAppSecret ?? await input.prompts.password({
1460
+ message: t(input.language, "larkAppSecret"),
1461
+ validate(value) {
1462
+ if (!value.trim()) return "Lark App Secret is required.";
1463
+ return void 0;
1464
+ }
1465
+ }),
1466
+ "Lark App Secret"
1467
+ );
1468
+ const botOpenIdInput = input.options.larkBotOpenId ?? (hasCompleteManualLarkCredentials(input.options) ? void 0 : await input.prompts.text({
1469
+ message: t(input.language, "larkBotOpenId"),
1470
+ placeholder: "ou_..."
1471
+ }));
1472
+ const botOpenId = optionalTrimmed(botOpenIdInput);
1473
+ return {
1474
+ appId,
1475
+ appSecret,
1476
+ ...botOpenId ? { botOpenId } : {}
1477
+ };
1478
+ }
1479
+ async function collectSlackSetup(options, defaults, prompts, language) {
1480
+ const derivedMode = options.slackMode ? parseSlackSetupMode(options.slackMode) : options.slackAppToken && !options.slackSigningSecret ? "socket_mode" : options.slackSigningSecret && !options.slackAppToken ? "events_api" : void 0;
1481
+ const selectedMode = derivedMode ? derivedMode : await prompts.select({
1482
+ message: t(language, "slackMode"),
1483
+ initialValue: defaults.slackMode ?? "socket_mode",
1484
+ options: ["socket_mode", "events_api"].map((candidate) => ({
1485
+ value: candidate,
1486
+ label: slackModeLabel(language, candidate),
1487
+ hint: slackModeHint(language, candidate)
1488
+ }))
1489
+ });
1490
+ if (selectedMode === "socket_mode" && options.slackPort) {
1491
+ throw new Error("--slack-port can only be used with --slack-mode events_api.");
1492
+ }
1493
+ if (selectedMode === "socket_mode" && (!options.slackAppToken || !options.slackBotToken) || selectedMode === "events_api" && (!options.slackSigningSecret || !options.slackBotToken)) {
1494
+ prompts.note(formatSlackCredentialHelp(language, selectedMode));
1495
+ }
1496
+ const appToken = selectedMode === "socket_mode" ? nonEmpty(
1497
+ options.slackAppToken ?? await prompts.password({ message: t(language, "slackAppToken") }),
1498
+ "Slack App-Level Token"
1499
+ ) : void 0;
1500
+ const signingSecret = selectedMode === "events_api" ? nonEmpty(
1501
+ options.slackSigningSecret ?? await prompts.password({ message: t(language, "slackSigningSecret") }),
1502
+ "Slack Signing Secret"
1503
+ ) : void 0;
1504
+ const botToken = nonEmpty(
1505
+ options.slackBotToken ?? await prompts.password({ message: t(language, "slackBotToken") }),
1506
+ "Slack Bot User OAuth Token"
1507
+ );
1508
+ const appId = optionalTrimmed(
1509
+ options.slackAppId ?? await prompts.text({
1510
+ message: t(language, "slackAppId"),
1511
+ placeholder: "A..."
1512
+ })
1513
+ );
1514
+ const teamId = nonEmpty(
1515
+ options.slackTeamId ?? await prompts.text({
1516
+ message: t(language, "slackTeamId"),
1517
+ placeholder: "T...",
1518
+ ...defaults.slackTeamId ? { initialValue: defaults.slackTeamId } : {}
1519
+ }),
1520
+ "Slack Team ID"
1521
+ );
1522
+ const channelId = nonEmpty(
1523
+ options.slackChannelId ?? await prompts.text({
1524
+ message: t(language, "slackChannelId"),
1525
+ placeholder: "C...",
1526
+ ...defaults.slackChannelId ? { initialValue: defaults.slackChannelId } : {}
1527
+ }),
1528
+ "Slack Channel ID"
1529
+ );
1530
+ const port = selectedMode === "events_api" ? parsePortInput(options.slackPort, "Slack Events API port") ?? (options.yes ? defaults.slackPort ?? DEFAULT_SLACK_EVENTS_PORT : parseLocalPort(
1531
+ await prompts.text({
1532
+ message: t(language, "slackPort"),
1533
+ initialValue: String(defaults.slackPort ?? DEFAULT_SLACK_EVENTS_PORT),
1534
+ placeholder: String(DEFAULT_SLACK_EVENTS_PORT)
1535
+ }),
1536
+ "Slack Events API port"
1537
+ )) : void 0;
1538
+ const bindingMethod = await collectBindingMethod(options, defaults, prompts, language, "slack");
1539
+ return {
1540
+ mode: selectedMode,
1541
+ ...appToken ? { appToken } : {},
1542
+ ...signingSecret ? { signingSecret } : {},
1543
+ botToken,
1544
+ teamId,
1545
+ channelId,
1546
+ bindingMethod,
1547
+ ...appId ? { appId } : {},
1548
+ ...port ? { port } : {}
1549
+ };
1550
+ }
1551
+ async function collectGitHubSetup(options, defaults, prompts, language, projectPath) {
1552
+ const repositoryDefault = options.githubRepository ?? (defaults.githubOwner && defaults.githubRepo ? `${defaults.githubOwner}/${defaults.githubRepo}` : void 0) ?? githubRepositoryFromRemote(projectPath);
1553
+ const repositoryInput = nonEmpty(
1554
+ options.githubRepository ?? await prompts.text({
1555
+ message: t(language, "githubRepository"),
1556
+ ...repositoryDefault ? { initialValue: repositoryDefault, placeholder: repositoryDefault } : { placeholder: "owner/repo" },
1557
+ validate(value) {
1558
+ try {
1559
+ parseGitHubRepository(value);
1560
+ return void 0;
1561
+ } catch (error) {
1562
+ return error instanceof Error ? error.message : String(error);
1563
+ }
1564
+ }
1565
+ }),
1566
+ "GitHub repository"
1567
+ );
1568
+ const repository = parseGitHubRepository(repositoryInput);
1569
+ const autoCreatePullRequest = options.githubAutoCreatePr ?? (options.yes ? defaults.githubAutoCreatePullRequest ?? false : await prompts.confirm({
1570
+ message: t(language, "githubAutoCreatePr"),
1571
+ initialValue: defaults.githubAutoCreatePullRequest ?? false
1572
+ }));
1573
+ if (!options.githubToken) {
1574
+ prompts.note(formatGitHubTokenHelp(language, { autoCreatePullRequest }));
1575
+ }
1576
+ const token = nonEmpty(options.githubToken ?? await prompts.password({ message: t(language, "githubToken") }), "GitHub token");
1577
+ const webhookSecret = options.githubWebhookSecret ? nonEmpty(options.githubWebhookSecret, "GitHub webhook secret") : defaults.githubWebhookSecret ?? generateGitHubWebhookSecret();
1578
+ const port = parsePortInput(options.githubPort, "GitHub webhook port") ?? (options.yes ? defaults.githubPort ?? DEFAULT_GITHUB_WEBHOOK_PORT : parseLocalPort(
1579
+ await prompts.text({
1580
+ message: t(language, "githubPort"),
1581
+ initialValue: String(defaults.githubPort ?? DEFAULT_GITHUB_WEBHOOK_PORT),
1582
+ placeholder: String(DEFAULT_GITHUB_WEBHOOK_PORT)
1583
+ }),
1584
+ "GitHub webhook port"
1585
+ ));
1586
+ return {
1587
+ token,
1588
+ webhookSecret,
1589
+ owner: repository.owner,
1590
+ repo: repository.repo,
1591
+ webhookPath: parseGitHubWebhookPath(options.githubWebhookPath ?? defaults.githubWebhookPath ?? "/github/webhooks"),
1592
+ autoCreatePullRequest,
1593
+ port
1594
+ };
1595
+ }
1596
+ async function collectBindingMethod(options, defaults, prompts, language, platform) {
1597
+ if (options.binding) {
1598
+ const binding = parseBindingMethod(options.binding);
1599
+ if (platform === "slack" && binding === "bind_later") {
1600
+ throw new Error("Slack setup requires a channel binding. Use --binding default_project.");
1601
+ }
1602
+ return binding;
1603
+ }
1604
+ if (platform === "slack") {
1605
+ return "default_project";
1606
+ }
1607
+ const message = t(language, "bindingMethod");
1608
+ return prompts.select({
1609
+ message,
1610
+ initialValue: defaults.bindingMethod ?? "default_project",
1611
+ options: ["default_project", "bind_later"].map((method) => ({
1612
+ value: method,
1613
+ label: bindingMethodLabel(language, method, platform),
1614
+ hint: bindingMethodHint(language, method, platform)
1615
+ }))
1616
+ });
1617
+ }
1618
+ async function collectSetupInput(options, configPath, dependencies) {
1619
+ const defaults = dependencies.defaults ?? loadDefaultsForSetup(options, configPath);
1620
+ const prompts = dependencies.prompts;
1621
+ const cwd = dependencies.cwd ?? process.cwd();
1622
+ prompts.intro(t(defaultLanguage(options, defaults), "intro"));
1623
+ const language = await collectLanguage(options, defaults, prompts);
1624
+ const platform = await collectPlatform(options, defaults, prompts, language);
1625
+ const executor = await collectExecutor(options, defaults, prompts, language, dependencies.env);
1626
+ const projectPath = await collectProjectPath(options, defaults, prompts, language, cwd);
1627
+ const resolvedProjectPath = projectPath.trim() || cwd;
1628
+ const savedLarkCredentials = platform === "lark" && shouldReadSavedLarkCredentials(options) ? findSavedLarkCredentials(defaults, resolvedProjectPath) : void 0;
1629
+ const larkSetupMethod = platform === "lark" ? await collectLarkSetupMethod(options, defaults, prompts, language, savedLarkCredentials) : void 0;
1630
+ const larkDomain = platform === "lark" && larkSetupMethod ? await collectLarkDomain(options, defaults, prompts, language, larkSetupMethod, savedLarkCredentials) : void 0;
1631
+ const larkCredentials = platform === "lark" && larkSetupMethod && larkDomain ? await collectLarkCredentials({
1632
+ options,
1633
+ prompts,
1634
+ language,
1635
+ setupMethod: larkSetupMethod,
1636
+ domain: larkDomain,
1637
+ ...savedLarkCredentials ? { savedLarkCredentials } : {},
1638
+ scanLarkPersonalAgent: dependencies.scanLarkPersonalAgent
1639
+ }) : void 0;
1640
+ const larkBindingMethod = platform === "lark" ? await collectBindingMethod(options, defaults, prompts, language, "lark") : void 0;
1641
+ const slackSetup = platform === "slack" ? await collectSlackSetup(options, defaults, prompts, language) : void 0;
1642
+ const githubSetup = platform === "github" ? await collectGitHubSetup(options, defaults, prompts, language, resolvedProjectPath) : void 0;
1643
+ const setupInput = {
1644
+ language,
1645
+ platform,
1646
+ projectPath: resolvedProjectPath,
1647
+ executor,
1648
+ ...larkCredentials && larkDomain && larkSetupMethod && larkBindingMethod ? {
1649
+ lark: {
1650
+ ...larkCredentials,
1651
+ domain: larkDomain,
1652
+ setupMethod: larkSetupMethod,
1653
+ bindingMethod: larkBindingMethod,
1654
+ ...larkSetupMethod === "saved" && savedLarkCredentials ? { savedCredentialsSource: savedLarkCredentials.source } : {}
1655
+ }
1656
+ } : {},
1657
+ ...slackSetup ? { slack: slackSetup } : {},
1658
+ ...githubSetup ? { github: githubSetup } : {}
1659
+ };
1660
+ prompts.note(formatSetupReview(setupInput, configPath));
1661
+ if (!options.yes) {
1662
+ const confirmed = await prompts.confirm({
1663
+ message: t(language, "confirmSetup"),
1664
+ initialValue: true
1665
+ });
1666
+ if (!confirmed) {
1667
+ throw new Error(t(language, "cancelled"));
1668
+ }
1669
+ }
1670
+ return setupInput;
1671
+ }
1672
+
1673
+ // src/ui/clack.ts
1674
+ import * as p from "@clack/prompts";
1675
+ function cancelled() {
1676
+ p.cancel("OpenTag setup cancelled.");
1677
+ process.exit(0);
1678
+ }
1679
+ function unwrapPromptResult(value) {
1680
+ if (p.isCancel(value)) {
1681
+ cancelled();
1682
+ }
1683
+ return value;
1684
+ }
1685
+ function clackOptions(options) {
1686
+ return options.map((option) => {
1687
+ const clackOption = {
1688
+ value: option.value,
1689
+ label: option.label
1690
+ };
1691
+ return option.hint ? { ...clackOption, hint: option.hint } : clackOption;
1692
+ });
1693
+ }
1694
+ function createClackPromptAdapter() {
1695
+ return {
1696
+ intro(message) {
1697
+ p.intro(message);
1698
+ },
1699
+ outro(message) {
1700
+ p.outro(message);
1701
+ },
1702
+ note(message) {
1703
+ p.log.message(message);
1704
+ },
1705
+ async select(input) {
1706
+ const selected = unwrapPromptResult(
1707
+ await p.select({
1708
+ message: input.message,
1709
+ options: clackOptions(input.options),
1710
+ ...input.initialValue ? { initialValue: input.initialValue } : {}
1711
+ })
1712
+ );
1713
+ return selected;
1714
+ },
1715
+ async text(input) {
1716
+ return unwrapPromptResult(await p.text(input));
1717
+ },
1718
+ async password(input) {
1719
+ return unwrapPromptResult(await p.password({ ...input, mask: "*" }));
1720
+ },
1721
+ async confirm(input) {
1722
+ return unwrapPromptResult(await p.confirm(input));
1723
+ }
1724
+ };
1725
+ }
1726
+
1727
+ // src/platforms/lark/registration-ui.ts
1728
+ import qrcode from "qrcode-terminal";
1729
+ import { registerLarkPersonalAgent } from "@opentag/lark";
1730
+ async function scanLarkPersonalAgent(input, dependencies = {}) {
1731
+ const output = dependencies.output ?? process.stdout;
1732
+ const register = dependencies.register ?? registerLarkPersonalAgent;
1733
+ const showQrCode = dependencies.showQrCode ?? process.env.OPENTAG_SHOW_QR === "1";
1734
+ const registered = await register({
1735
+ domain: input.domain,
1736
+ onQrCode(info) {
1737
+ output.write("\nOpen this URL to create the Lark / Feishu Personal Agent app:\n");
1738
+ output.write(`URL: ${info.url}
1739
+ `);
1740
+ output.write(`This QR code expires in about ${Math.ceil(info.expireIn / 60)} minute(s).
1741
+ `);
1742
+ if (showQrCode) {
1743
+ output.write("\nTerminal QR code:\n");
1744
+ qrcode.generate(info.url, { small: true }, (qr) => {
1745
+ output.write(`${qr}
1746
+ `);
1747
+ });
1748
+ } else {
1749
+ output.write("Terminal QR codes are hidden by default because Lark setup links are large.\n");
1750
+ output.write("Set OPENTAG_SHOW_QR=1 if you prefer scanning a terminal QR code.\n");
1751
+ }
1752
+ output.write("Keep this terminal open. OpenTag will continue automatically after the app is created.\n\n");
1753
+ },
1754
+ onStatus(info) {
1755
+ if (info.status === "slow_down") {
1756
+ output.write(`Lark asked OpenTag to poll more slowly. Next check in ${info.interval ?? "a few"} seconds.
1757
+ `);
1758
+ } else if (info.status === "domain_switched") {
1759
+ output.write("Detected a Lark tenant. Continuing registration on larksuite.com.\n");
1760
+ }
1761
+ },
1762
+ onWarning(message) {
1763
+ output.write(`${message}
1764
+ `);
1765
+ }
1766
+ });
1767
+ output.write("Lark Personal Agent connected.\n");
1768
+ output.write(`App ID: ${registered.appId}
1769
+ `);
1770
+ output.write(`Domain: ${registered.domain}
1771
+ `);
1772
+ if (registered.operatorOpenId) {
1773
+ output.write(`Setup user: ${registered.operatorOpenId}
1774
+ `);
1775
+ }
1776
+ if (registered.botOpenId) {
1777
+ output.write(`Bot: ${registered.botName ?? "OpenTag"} (${registered.botOpenId})
1778
+ `);
1779
+ }
1780
+ output.write("\n");
1781
+ return registered;
1782
+ }
1783
+
1784
+ // src/start.ts
1785
+ import { createServer } from "net";
1786
+ import { createDispatcherAdminClient } from "@opentag/client";
1787
+ import { startGitHubIngress } from "@opentag/github";
1788
+ import { DEFAULT_AGENT_ID, startLarkIngress } from "@opentag/lark";
1789
+ import {
1790
+ createDaemonRuntimeInput,
1791
+ normalizeChannelBindings,
1792
+ serveDaemon,
1793
+ startDispatcher
1794
+ } from "@opentag/local-runtime";
1795
+ import {
1796
+ startSlackIngress,
1797
+ startSlackSocketModeIngress
1798
+ } from "@opentag/slack";
1799
+
1800
+ // src/health.ts
1801
+ async function fetchWithTimeout(input) {
1802
+ const fetchImpl = input.fetchImpl ?? fetch;
1803
+ const controller = new AbortController();
1804
+ const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
1805
+ try {
1806
+ return await fetchImpl(input.url, { signal: controller.signal });
1807
+ } finally {
1808
+ clearTimeout(timeout);
1809
+ }
1810
+ }
1811
+ async function probeDispatcherHealth(input) {
1812
+ const healthUrl = `${input.dispatcherUrl.replace(/\/$/, "")}/healthz`;
1813
+ try {
1814
+ const response = await fetchWithTimeout({
1815
+ url: healthUrl,
1816
+ ...input.fetchImpl ? { fetchImpl: input.fetchImpl } : {},
1817
+ timeoutMs: input.timeoutMs
1818
+ });
1819
+ return response.ok;
1820
+ } catch {
1821
+ return false;
1822
+ }
1823
+ }
1824
+
1825
+ // src/start.ts
1826
+ function dispatcherPortFromUrl(dispatcherUrl) {
1827
+ const url = new URL(dispatcherUrl);
1828
+ if (url.protocol !== "http:" || url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
1829
+ throw new Error("opentag start currently supports only local http dispatcher URLs.");
1830
+ }
1831
+ if (url.pathname !== "/" || url.search || url.hash) {
1832
+ throw new Error("Dispatcher URL must not include a path, query, or hash.");
1833
+ }
1834
+ const port = url.port ? Number(url.port) : 80;
1835
+ if (!Number.isInteger(port) || port <= 0) {
1836
+ throw new Error(`Dispatcher URL has an invalid port: ${dispatcherUrl}`);
1837
+ }
1838
+ return port;
1839
+ }
1840
+ function requireLarkConfig(config) {
1841
+ const lark = config.platforms.lark;
1842
+ if (!lark) {
1843
+ throw new Error("This config has no Lark platform config.");
1844
+ }
1845
+ return lark;
1846
+ }
1847
+ function requireSlackConfig(config) {
1848
+ const slack = config.platforms.slack;
1849
+ if (!slack) {
1850
+ throw new Error("This config has no Slack platform config.");
1851
+ }
1852
+ return slack;
1853
+ }
1854
+ function requireGitHubConfig(config) {
1855
+ const github = config.platforms.github;
1856
+ if (!github) {
1857
+ throw new Error("This config has no GitHub platform config.");
1858
+ }
1859
+ return github;
1860
+ }
1861
+ function hasStartablePlatform(config) {
1862
+ return Boolean(config.platforms.lark || config.platforms.slack || config.platforms.github);
1863
+ }
1864
+ function localStartPortChecks(config) {
1865
+ const checks = [
1866
+ {
1867
+ label: "dispatcher",
1868
+ port: dispatcherPortFromUrl(config.daemon.dispatcherUrl),
1869
+ fix: "Change daemon.dispatcherUrl in the OpenTag config."
1870
+ }
1871
+ ];
1872
+ const slack = config.platforms.slack;
1873
+ if (slack && slackModeFromCliConfig(config) === "events_api") {
1874
+ checks.push({
1875
+ label: "Slack Events API",
1876
+ port: slack.port ?? DEFAULT_SLACK_EVENTS_PORT,
1877
+ fix: "Run `opentag setup --platform slack --slack-mode events_api --slack-port <port> --force`, or edit platforms.slack.port in the OpenTag config."
1878
+ });
1879
+ }
1880
+ const github = config.platforms.github;
1881
+ if (github) {
1882
+ checks.push({
1883
+ label: "GitHub local webhook",
1884
+ port: github.port ?? DEFAULT_GITHUB_WEBHOOK_PORT,
1885
+ fix: "Run `opentag setup --platform github --github-port <port> --force`, or edit platforms.github.port in the OpenTag config."
1886
+ });
1887
+ }
1888
+ return checks;
1889
+ }
1890
+ async function assertPortAvailable(check) {
1891
+ const server = createServer();
1892
+ await new Promise((resolve2, reject) => {
1893
+ server.once("error", (error) => {
1894
+ if (error.code === "EADDRINUSE") {
1895
+ reject(
1896
+ new Error(
1897
+ [
1898
+ `OpenTag cannot start ${check.label} because port ${check.port} is already in use.`,
1899
+ check.fix,
1900
+ `To inspect the current process: lsof -nP -iTCP:${check.port} -sTCP:LISTEN`
1901
+ ].join("\n")
1902
+ )
1903
+ );
1904
+ return;
1905
+ }
1906
+ reject(
1907
+ new Error(
1908
+ [`OpenTag cannot start ${check.label} on port ${check.port}: ${error.message}`, check.fix].join("\n")
1909
+ )
1910
+ );
1911
+ });
1912
+ server.listen(check.port, "127.0.0.1", () => {
1913
+ server.close((error) => {
1914
+ if (error) {
1915
+ reject(error);
1916
+ return;
1917
+ }
1918
+ resolve2();
1919
+ });
1920
+ });
1921
+ });
1922
+ }
1923
+ async function assertStartPortsAvailable(config) {
1924
+ const checks = localStartPortChecks(config);
1925
+ const seen = /* @__PURE__ */ new Map();
1926
+ for (const check of checks) {
1927
+ const existing = seen.get(check.port);
1928
+ if (existing) {
1929
+ throw new Error(
1930
+ [
1931
+ `OpenTag cannot start because ${existing.label} and ${check.label} both use port ${check.port}.`,
1932
+ `${existing.label}: ${existing.fix}`,
1933
+ `${check.label}: ${check.fix}`
1934
+ ].join("\n")
1935
+ );
1936
+ }
1937
+ seen.set(check.port, check);
1938
+ }
1939
+ for (const check of checks) {
1940
+ await assertPortAvailable(check);
1941
+ }
1942
+ }
1943
+ function dispatcherRuntimeInputFromCliConfig(config) {
1944
+ if (!hasStartablePlatform(config)) {
1945
+ throw new Error("This config has no startable platform. Run `opentag setup` and choose Lark, Slack, or GitHub.");
1946
+ }
1947
+ const lark = config.platforms.lark;
1948
+ const slack = config.platforms.slack;
1949
+ const github = config.platforms.github;
1950
+ if (github && !config.daemon.githubToken) {
1951
+ throw new Error("GitHub platform requires daemon.githubToken for callbacks.");
1952
+ }
1953
+ if (github && !config.daemon.preparePullRequestBranch && !config.daemon.allowAutoCreatePullRequest) {
1954
+ throw new Error(
1955
+ "GitHub platform requires daemon.preparePullRequestBranch=true unless legacy daemon.allowAutoCreatePullRequest is enabled. Run `opentag setup` and choose GitHub to update this config."
1956
+ );
1957
+ }
1958
+ return {
1959
+ port: dispatcherPortFromUrl(config.daemon.dispatcherUrl),
1960
+ databasePath: config.state.databasePath,
1961
+ ...config.daemon.pairingToken ? { pairingToken: config.daemon.pairingToken } : {},
1962
+ ...config.daemon.githubToken ? { githubToken: config.daemon.githubToken } : {},
1963
+ ...lark ? {
1964
+ lark: {
1965
+ appId: lark.appId,
1966
+ appSecret: lark.appSecret,
1967
+ domain: lark.domain
1968
+ }
1969
+ } : {},
1970
+ ...slack ? { slackBotToken: slack.botToken } : {}
1971
+ };
1972
+ }
1973
+ function defaultRepoBindingFromConfig(config) {
1974
+ if (config.platforms.lark?.defaultProjectBinding === false) return void 0;
1975
+ if (config.daemon.repositories.length !== 1) return void 0;
1976
+ const repository = config.daemon.repositories[0];
1977
+ if (!repository) return void 0;
1978
+ return {
1979
+ repoProvider: repository.provider,
1980
+ owner: repository.owner,
1981
+ repo: repository.repo
1982
+ };
1983
+ }
1984
+ function larkIngressConfigFromCliConfig(config) {
1985
+ const lark = requireLarkConfig(config);
1986
+ const defaultRepoBinding = defaultRepoBindingFromConfig(config);
1987
+ return {
1988
+ appId: lark.appId,
1989
+ appSecret: lark.appSecret,
1990
+ dispatcherUrl: config.daemon.dispatcherUrl,
1991
+ domain: lark.domain,
1992
+ agentId: DEFAULT_AGENT_ID,
1993
+ ...config.daemon.pairingToken ? { dispatcherToken: config.daemon.pairingToken } : {},
1994
+ ...lark.botOpenId ? { botOpenId: lark.botOpenId } : {},
1995
+ ...defaultRepoBinding ? { defaultRepoBinding } : {}
1996
+ };
1997
+ }
1998
+ function slackModeFromCliConfig(config) {
1999
+ const slack = requireSlackConfig(config);
2000
+ return slack.mode ?? "events_api";
2001
+ }
2002
+ function slackIngressConfigFromCliConfig(config) {
2003
+ const slack = requireSlackConfig(config);
2004
+ if (!slack.signingSecret) {
2005
+ throw new Error("Slack Events API mode requires platforms.slack.signingSecret.");
2006
+ }
2007
+ return {
2008
+ signingSecret: slack.signingSecret,
2009
+ dispatcherUrl: config.daemon.dispatcherUrl,
2010
+ ...config.daemon.pairingToken ? { dispatcherToken: config.daemon.pairingToken } : {},
2011
+ ...slack.appId ? { appId: slack.appId } : {},
2012
+ ...slack.port ? { port: slack.port } : {}
2013
+ };
2014
+ }
2015
+ function slackSocketModeIngressConfigFromCliConfig(config) {
2016
+ const slack = requireSlackConfig(config);
2017
+ if (!slack.appToken) {
2018
+ throw new Error("Slack Socket Mode requires platforms.slack.appToken.");
2019
+ }
2020
+ return {
2021
+ appToken: slack.appToken,
2022
+ dispatcherUrl: config.daemon.dispatcherUrl,
2023
+ ...config.daemon.pairingToken ? { dispatcherToken: config.daemon.pairingToken } : {},
2024
+ ...slack.appId ? { appId: slack.appId } : {}
2025
+ };
2026
+ }
2027
+ function githubIngressConfigFromCliConfig(config) {
2028
+ const github = requireGitHubConfig(config);
2029
+ return {
2030
+ webhookSecret: github.webhookSecret,
2031
+ dispatcherUrl: config.daemon.dispatcherUrl,
2032
+ ...config.daemon.pairingToken ? { dispatcherToken: config.daemon.pairingToken } : {},
2033
+ port: github.port ?? DEFAULT_GITHUB_WEBHOOK_PORT,
2034
+ ...github.webhookPath ? { webhookPath: github.webhookPath } : {}
2035
+ };
2036
+ }
2037
+ async function bootstrapLocalDispatcher(config, client) {
2038
+ const admin = client ?? createDispatcherAdminClient({
2039
+ dispatcherUrl: config.daemon.dispatcherUrl,
2040
+ runnerId: config.daemon.runnerId,
2041
+ ...config.daemon.pairingToken ? { pairingToken: config.daemon.pairingToken } : {}
2042
+ });
2043
+ await admin.registerRunner(config.daemon.runnerId);
2044
+ for (const repository of config.daemon.repositories) {
2045
+ await admin.bindRepository({
2046
+ provider: repository.provider,
2047
+ owner: repository.owner,
2048
+ repo: repository.repo,
2049
+ checkoutPath: repository.checkoutPath,
2050
+ ...repository.defaultExecutor ? { defaultExecutor: repository.defaultExecutor } : {},
2051
+ ...repository.baseBranch ? { baseBranch: repository.baseBranch } : {},
2052
+ ...repository.pushRemote ? { pushRemote: repository.pushRemote } : {},
2053
+ ...repository.worktreeRoot ? { worktreeRoot: repository.worktreeRoot } : {},
2054
+ ...repository.keepWorktree ? { keepWorktree: repository.keepWorktree } : {}
2055
+ });
2056
+ }
2057
+ for (const binding of normalizeChannelBindings(config.daemon)) {
2058
+ await admin.bindChannel({
2059
+ provider: binding.provider,
2060
+ accountId: binding.accountId,
2061
+ conversationId: binding.conversationId,
2062
+ repoProvider: binding.repoProvider,
2063
+ owner: binding.owner,
2064
+ repo: binding.repo,
2065
+ ...binding.metadata ? { metadata: binding.metadata } : {}
2066
+ });
2067
+ }
2068
+ }
2069
+ async function waitForDispatcher(input) {
2070
+ const attempts = input.attempts ?? 60;
2071
+ const delayMs = input.delayMs ?? 500;
2072
+ const timeoutMs = input.timeoutMs ?? 1e3;
2073
+ const healthUrl = `${input.dispatcherUrl.replace(/\/$/, "")}/healthz`;
2074
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
2075
+ const healthy = await probeDispatcherHealth({
2076
+ dispatcherUrl: input.dispatcherUrl,
2077
+ ...input.fetchImpl ? { fetchImpl: input.fetchImpl } : {},
2078
+ timeoutMs
2079
+ });
2080
+ if (healthy) return;
2081
+ await new Promise((resolve2) => setTimeout(resolve2, delayMs));
2082
+ }
2083
+ throw new Error(`Dispatcher did not become healthy at ${healthUrl}.`);
2084
+ }
2085
+ function waitForAbort(signal) {
2086
+ if (signal.aborted) return Promise.resolve();
2087
+ return new Promise((resolve2) => {
2088
+ signal.addEventListener("abort", () => resolve2(), { once: true });
2089
+ });
2090
+ }
2091
+ function shouldRethrowAbortReason(input) {
2092
+ return !input.shutdownRequested && input.reason instanceof Error;
2093
+ }
2094
+ async function runStartCommand(options) {
2095
+ const configPath = options.config ?? defaultConfigPath();
2096
+ const config = readCliConfig(configPath);
2097
+ ensurePrivateDirectory(config.state.directory);
2098
+ ensurePrivateDirectory(config.state.worktreeRoot);
2099
+ await assertStartPortsAvailable(config);
2100
+ const abortController = new AbortController();
2101
+ const dispatcher = startDispatcher(dispatcherRuntimeInputFromCliConfig(config));
2102
+ const ingresses = [];
2103
+ let shutdownRequested = false;
2104
+ const onSignal = () => {
2105
+ shutdownRequested = true;
2106
+ abortController.abort();
2107
+ };
2108
+ process.once("SIGINT", onSignal);
2109
+ process.once("SIGTERM", onSignal);
2110
+ try {
2111
+ await waitForDispatcher({ dispatcherUrl: config.daemon.dispatcherUrl });
2112
+ await bootstrapLocalDispatcher(config);
2113
+ const daemonPromise = serveDaemon({
2114
+ ...createDaemonRuntimeInput(config.daemon),
2115
+ signal: abortController.signal
2116
+ });
2117
+ if (config.platforms.lark) {
2118
+ const handle = startLarkIngress(larkIngressConfigFromCliConfig(config));
2119
+ ingresses.push({ platform: "lark", handle });
2120
+ handle.startPromise.catch((error) => {
2121
+ if (!abortController.signal.aborted) {
2122
+ abortController.abort(error);
2123
+ }
2124
+ });
2125
+ }
2126
+ if (config.platforms.slack) {
2127
+ if (slackModeFromCliConfig(config) === "socket_mode") {
2128
+ const handle = startSlackSocketModeIngress(slackSocketModeIngressConfigFromCliConfig(config));
2129
+ ingresses.push({ platform: "slack", mode: "socket_mode", handle });
2130
+ handle.startPromise.catch((error) => {
2131
+ if (!abortController.signal.aborted) {
2132
+ abortController.abort(error);
2133
+ }
2134
+ });
2135
+ } else {
2136
+ const handle = startSlackIngress(slackIngressConfigFromCliConfig(config));
2137
+ ingresses.push({ platform: "slack", mode: "events_api", url: handle.url, handle });
2138
+ }
2139
+ }
2140
+ if (config.platforms.github) {
2141
+ const handle = startGitHubIngress(githubIngressConfigFromCliConfig(config));
2142
+ ingresses.push({ platform: "github", url: handle.url, webhookPath: handle.webhookPath, handle });
2143
+ }
2144
+ daemonPromise.catch((error) => {
2145
+ if (!abortController.signal.aborted) {
2146
+ abortController.abort(error);
2147
+ }
2148
+ });
2149
+ console.log("OpenTag is running.");
2150
+ console.log(`Config: ${configPath}`);
2151
+ console.log(`Dispatcher: ${config.daemon.dispatcherUrl}`);
2152
+ for (const ingress of ingresses) {
2153
+ if (ingress.platform === "slack") {
2154
+ const slack = config.platforms.slack;
2155
+ if (ingress.mode === "socket_mode") {
2156
+ console.log("Slack: using Socket Mode");
2157
+ console.log(`Slack channel binding: ${slack.teamId}/${slack.channelId}`);
2158
+ console.log("Before testing, invite the Slack app to that channel with /invite @your app name.");
2159
+ } else {
2160
+ console.log(`Slack Events: ${ingress.url}/slack/events`);
2161
+ console.log(`Slack channel binding: ${slack.teamId}/${slack.channelId}`);
2162
+ console.log("Before testing, invite the Slack app to that channel with /invite @your app name.");
2163
+ }
2164
+ } else if (ingress.platform === "github") {
2165
+ const github = config.platforms.github;
2166
+ console.log(`GitHub local webhook: ${githubLocalWebhookUrl({ port: github.port, webhookPath: ingress.webhookPath })}`);
2167
+ console.log(`GitHub Payload URL: ${githubPublicWebhookUrlPlaceholder(ingress.webhookPath)}`);
2168
+ console.log(`GitHub settings: ${githubWebhooksSettingsUrl(github)}`);
2169
+ console.log(`Tunnel example: ngrok http ${github.port ?? DEFAULT_GITHUB_WEBHOOK_PORT}`);
2170
+ } else {
2171
+ console.log("Lark / Feishu: connected through Personal Agent long connection");
2172
+ }
2173
+ }
2174
+ console.log("Press Ctrl-C to stop.");
2175
+ await waitForAbort(abortController.signal);
2176
+ const reason = abortController.signal.reason;
2177
+ if (shouldRethrowAbortReason({ shutdownRequested, reason })) {
2178
+ throw reason;
2179
+ }
2180
+ } finally {
2181
+ process.off("SIGINT", onSignal);
2182
+ process.off("SIGTERM", onSignal);
2183
+ abortController.abort();
2184
+ await Promise.allSettled([...ingresses].reverse().map((ingress) => ingress.handle.close()));
2185
+ await dispatcher.close();
2186
+ }
2187
+ }
2188
+
2189
+ // src/commands/setup.ts
2190
+ function startPromptMessage(language) {
2191
+ return language === "zh-CN" ? "\u73B0\u5728\u542F\u52A8 OpenTag\uFF1F" : "Start OpenTag now?";
2192
+ }
2193
+ function setupCompleteMessage(language) {
2194
+ return language === "zh-CN" ? "OpenTag \u8BBE\u7F6E\u5B8C\u6210\u3002" : "OpenTag setup complete.";
2195
+ }
2196
+ function startingMessage(language) {
2197
+ return language === "zh-CN" ? "\u6B63\u5728\u542F\u52A8 OpenTag..." : "Starting OpenTag...";
2198
+ }
2199
+ async function runSetupCommand(options, dependencies = {}) {
2200
+ const env = dependencies.env ?? process.env;
2201
+ const configPath = options.config ?? defaultConfigPath(env);
2202
+ if (options.yes && existsSync5(configPath) && !options.force) {
2203
+ throw new Error(`OpenTag config already exists at ${configPath}. Use --force with --yes to overwrite it.`);
2204
+ }
2205
+ const prompts = dependencies.prompts ?? createClackPromptAdapter();
2206
+ const setupInput = await collectSetupInput(options, configPath, {
2207
+ prompts,
2208
+ scanLarkPersonalAgent: dependencies.scanLarkPersonalAgent ?? scanLarkPersonalAgent,
2209
+ ...dependencies.cwd ? { cwd: dependencies.cwd } : {},
2210
+ ...dependencies.env ? { env: dependencies.env } : {},
2211
+ ...dependencies.defaults ? { defaults: dependencies.defaults } : {}
2212
+ });
2213
+ const config = createSetupConfig(setupInput, env);
2214
+ ensurePrivateDirectory(config.state.directory);
2215
+ ensurePrivateDirectory(config.state.worktreeRoot);
2216
+ writeCliConfigAtomic(configPath, config);
2217
+ prompts.note(formatSetupComplete(config, configPath));
2218
+ const shouldStart = options.start ?? (!options.yes ? await prompts.confirm({
2219
+ message: startPromptMessage(config.preferences?.language),
2220
+ initialValue: true
2221
+ }) : false);
2222
+ if (shouldStart) {
2223
+ prompts.outro(startingMessage(config.preferences?.language));
2224
+ await (dependencies.startOpenTag ?? runStartCommand)({ config: configPath });
2225
+ } else {
2226
+ prompts.outro(setupCompleteMessage(config.preferences?.language));
2227
+ }
2228
+ }
2229
+
2230
+ // src/status.ts
2231
+ async function getStatusSummary(input = {}) {
2232
+ const configPath = input.configPath ?? defaultConfigPath();
2233
+ const config = readCliConfig(configPath);
2234
+ return statusFromConfig({ config, configPath, ...input.fetchImpl ? { fetchImpl: input.fetchImpl } : {} });
2235
+ }
2236
+ async function statusFromConfig(input) {
2237
+ const dispatcher = await probeDispatcherHealth({
2238
+ dispatcherUrl: input.config.daemon.dispatcherUrl,
2239
+ ...input.fetchImpl ? { fetchImpl: input.fetchImpl } : {},
2240
+ timeoutMs: input.healthTimeoutMs ?? 1e3
2241
+ }) ? "online" : "offline";
2242
+ return {
2243
+ configPath: input.configPath,
2244
+ dispatcher,
2245
+ dispatcherUrl: input.config.daemon.dispatcherUrl,
2246
+ runnerId: input.config.daemon.runnerId,
2247
+ repositories: input.config.daemon.repositories.map((repository) => {
2248
+ return `${repository.provider}:${repository.owner}/${repository.repo} -> ${repository.checkoutPath}`;
2249
+ }),
2250
+ platforms: Object.entries(input.config.platforms).filter(([, value]) => value !== void 0).map(([key]) => key)
2251
+ };
2252
+ }
2253
+ function formatStatus(summary) {
2254
+ return [
2255
+ `Config: ${summary.configPath}`,
2256
+ `Dispatcher: ${summary.dispatcher} (${summary.dispatcherUrl})`,
2257
+ `Runner: ${summary.runnerId}`,
2258
+ `Platforms: ${summary.platforms.length ? summary.platforms.join(", ") : "none"}`,
2259
+ "Project Targets:",
2260
+ ...summary.repositories.length ? summary.repositories.map((repository) => ` ${repository}`) : [" none"]
2261
+ ].join("\n");
2262
+ }
2263
+ async function runStatusCommand(options) {
2264
+ console.log(formatStatus(await getStatusSummary({ ...options.config ? { configPath: options.config } : {} })));
2265
+ }
2266
+
2267
+ // src/index.ts
2268
+ var program = new Command();
2269
+ function handleError(error) {
2270
+ console.error(formatCliConfigError(error));
2271
+ process.exit(1);
2272
+ }
2273
+ program.name(process.env.OPENTAG_CLI_NAME?.trim() || "opentag").description("OpenTag CLI");
2274
+ program.command("setup").description("Create a local OpenTag config").option("--platform <platform>", "Platform to configure").option("--config <path>", "Config file path").option("--project <path>", "Project checkout path").option("--language <language>", "Setup language: en or zh-CN").option("--executor <executor>", "Default executor: echo, codex, or claude-code").option("--lark-setup <method>", "Lark setup method: saved, scan, or manual").option("--lark-app-id <id>", "Lark app id").option("--lark-app-secret <secret>", "Lark app secret").option("--lark-domain <domain>", "Lark domain: lark or feishu").option("--lark-bot-open-id <openId>", "Lark bot open id for group mentions").option("--slack-mode <mode>", "Slack connection mode: socket_mode or events_api").option("--slack-app-token <token>", "Slack app-level token for Socket Mode").option("--slack-signing-secret <secret>", "Slack signing secret").option("--slack-bot-token <token>", "Slack bot user OAuth token").option("--slack-app-id <id>", "Slack app id").option("--slack-team-id <id>", "Slack team id").option("--slack-channel-id <id>", "Slack channel id").option("--slack-port <port>", "Local Slack Events API port").option("--github-token <token>", "GitHub token for comments and apply-1 pull requests").option("--github-webhook-secret <secret>", "GitHub webhook secret; generated when omitted").option("--github-repository <ownerRepo>", "GitHub repository as owner/repo").option("--github-webhook-path <path>", "GitHub webhook path").option("--github-port <port>", "Local GitHub webhook port").option("--github-auto-create-pr", "Create pull requests immediately after runs").option("--no-github-auto-create-pr", "Use the default apply-1 pull request flow").option("--binding <method>", "Binding method: default_project or bind_later").option("--force", "Overwrite an existing config").option("--start", "Start OpenTag immediately after setup").option("--no-start", "Do not ask to start OpenTag after setup").option("-y, --yes", "Skip setup confirmation").action(async (options) => {
2275
+ try {
2276
+ await runSetupCommand(options);
2277
+ } catch (error) {
2278
+ handleError(error);
2279
+ }
2280
+ });
2281
+ program.command("start").description("Start the local OpenTag stack").option("--config <path>", "Config file path").action(async (options) => {
2282
+ try {
2283
+ await runStartCommand(options);
2284
+ } catch (error) {
2285
+ handleError(error);
2286
+ }
2287
+ });
2288
+ program.command("status").description("Show the local OpenTag status").option("--config <path>", "Config file path").action(async (options) => {
2289
+ try {
2290
+ await runStatusCommand(options);
2291
+ } catch (error) {
2292
+ handleError(error);
2293
+ }
2294
+ });
2295
+ program.command("doctor").description("Check dispatcher, bindings, checkouts, and executors").option("--config <path>", "Config file path").action(async (options) => {
2296
+ try {
2297
+ await runDoctorCommand(options);
2298
+ } catch (error) {
2299
+ handleError(error);
2300
+ }
2301
+ });
2302
+ program.command("platforms").description("List OpenTag platform setup support").action(() => {
2303
+ runPlatformsCommand();
2304
+ });
2305
+ program.command("executors").description("List available coding agents").action(() => {
2306
+ runExecutorsCommand();
2307
+ });
2308
+ var configCommand = program.command("config").description("Inspect OpenTag config");
2309
+ configCommand.command("path").description("Print the OpenTag config path").action(() => {
2310
+ console.log(defaultConfigPath());
2311
+ });
2312
+ configCommand.command("show").description("Print the OpenTag config with secrets redacted").option("--config <path>", "Config file path").action((options) => {
2313
+ try {
2314
+ console.log(JSON.stringify(redactedCliConfig(readCliConfig(options.config ?? defaultConfigPath())), null, 2));
2315
+ } catch (error) {
2316
+ handleError(error);
2317
+ }
2318
+ });
2319
+ await program.parseAsync(process.argv);
2320
+ //# sourceMappingURL=index.js.map