@pollit/twin-dev-bot 0.0.1

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 (110) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +415 -0
  3. package/bin/twindevbot.js +22 -0
  4. package/dist/action-payload-store.d.ts +22 -0
  5. package/dist/action-payload-store.js +54 -0
  6. package/dist/active-runners.d.ts +44 -0
  7. package/dist/active-runners.js +114 -0
  8. package/dist/channel-store.d.ts +16 -0
  9. package/dist/channel-store.js +91 -0
  10. package/dist/claude/active-runners.d.ts +44 -0
  11. package/dist/claude/active-runners.js +114 -0
  12. package/dist/claude/claude-runner.d.ts +57 -0
  13. package/dist/claude/claude-runner.js +210 -0
  14. package/dist/claude/session-manager.d.ts +62 -0
  15. package/dist/claude/session-manager.js +247 -0
  16. package/dist/claude-runner.d.ts +57 -0
  17. package/dist/claude-runner.js +210 -0
  18. package/dist/cli.d.ts +2 -0
  19. package/dist/cli.js +271 -0
  20. package/dist/config.d.ts +9 -0
  21. package/dist/config.js +49 -0
  22. package/dist/conversation-store.d.ts +53 -0
  23. package/dist/conversation-store.js +173 -0
  24. package/dist/core/config.d.ts +9 -0
  25. package/dist/core/config.js +49 -0
  26. package/dist/core/logger.d.ts +34 -0
  27. package/dist/core/logger.js +110 -0
  28. package/dist/core/paths.d.ts +11 -0
  29. package/dist/core/paths.js +18 -0
  30. package/dist/core/platform.d.ts +18 -0
  31. package/dist/core/platform.js +33 -0
  32. package/dist/daemon/index.d.ts +3 -0
  33. package/dist/daemon/index.js +14 -0
  34. package/dist/daemon/macos.d.ts +8 -0
  35. package/dist/daemon/macos.js +150 -0
  36. package/dist/daemon/types.d.ts +9 -0
  37. package/dist/daemon/types.js +1 -0
  38. package/dist/daemon/windows.d.ts +8 -0
  39. package/dist/daemon/windows.js +137 -0
  40. package/dist/handlers/claude-command.d.ts +2 -0
  41. package/dist/handlers/claude-command.js +634 -0
  42. package/dist/handlers/claude-runner-setup.d.ts +16 -0
  43. package/dist/handlers/claude-runner-setup.js +445 -0
  44. package/dist/handlers/index.d.ts +3 -0
  45. package/dist/handlers/index.js +3 -0
  46. package/dist/handlers/init-handlers.d.ts +2 -0
  47. package/dist/handlers/init-handlers.js +189 -0
  48. package/dist/handlers/question-handlers.d.ts +2 -0
  49. package/dist/handlers/question-handlers.js +835 -0
  50. package/dist/i18n/en.d.ts +150 -0
  51. package/dist/i18n/en.js +163 -0
  52. package/dist/i18n/index.d.ts +20 -0
  53. package/dist/i18n/index.js +31 -0
  54. package/dist/i18n/ko.d.ts +1 -0
  55. package/dist/i18n/ko.js +141 -0
  56. package/dist/logger.d.ts +34 -0
  57. package/dist/logger.js +110 -0
  58. package/dist/multi-select-state.d.ts +58 -0
  59. package/dist/multi-select-state.js +151 -0
  60. package/dist/paths.d.ts +11 -0
  61. package/dist/paths.js +18 -0
  62. package/dist/pending-questions.d.ts +53 -0
  63. package/dist/pending-questions.js +139 -0
  64. package/dist/platform.d.ts +18 -0
  65. package/dist/platform.js +33 -0
  66. package/dist/progress-tracker.d.ts +47 -0
  67. package/dist/progress-tracker.js +218 -0
  68. package/dist/question-blocks.d.ts +27 -0
  69. package/dist/question-blocks.js +235 -0
  70. package/dist/server.d.ts +1 -0
  71. package/dist/server.js +83 -0
  72. package/dist/session-manager.d.ts +62 -0
  73. package/dist/session-manager.js +247 -0
  74. package/dist/setup.d.ts +5 -0
  75. package/dist/setup.js +132 -0
  76. package/dist/slack/progress-tracker.d.ts +47 -0
  77. package/dist/slack/progress-tracker.js +218 -0
  78. package/dist/slack/question-blocks.d.ts +27 -0
  79. package/dist/slack/question-blocks.js +235 -0
  80. package/dist/stores/action-payload-store.d.ts +22 -0
  81. package/dist/stores/action-payload-store.js +54 -0
  82. package/dist/stores/channel-store.d.ts +16 -0
  83. package/dist/stores/channel-store.js +91 -0
  84. package/dist/stores/multi-select-state.d.ts +58 -0
  85. package/dist/stores/multi-select-state.js +151 -0
  86. package/dist/stores/pending-questions.d.ts +53 -0
  87. package/dist/stores/pending-questions.js +139 -0
  88. package/dist/stores/workspace-store.d.ts +27 -0
  89. package/dist/stores/workspace-store.js +160 -0
  90. package/dist/templates.d.ts +23 -0
  91. package/dist/templates.js +292 -0
  92. package/dist/types/claude-stream.d.ts +116 -0
  93. package/dist/types/claude-stream.js +3 -0
  94. package/dist/types/conversation.d.ts +16 -0
  95. package/dist/types/conversation.js +4 -0
  96. package/dist/types/index.d.ts +2 -0
  97. package/dist/types/index.js +2 -0
  98. package/dist/types/slack.d.ts +51 -0
  99. package/dist/types/slack.js +1 -0
  100. package/dist/utils/display-width.d.ts +8 -0
  101. package/dist/utils/display-width.js +33 -0
  102. package/dist/utils/safe-async.d.ts +6 -0
  103. package/dist/utils/safe-async.js +14 -0
  104. package/dist/utils/slack-message.d.ts +73 -0
  105. package/dist/utils/slack-message.js +220 -0
  106. package/dist/utils/slack-rate-limit.d.ts +5 -0
  107. package/dist/utils/slack-rate-limit.js +49 -0
  108. package/dist/workspace-store.d.ts +27 -0
  109. package/dist/workspace-store.js +160 -0
  110. package/package.json +51 -0
@@ -0,0 +1,110 @@
1
+ import { createWriteStream } from "fs";
2
+ const LOG_LEVELS = {
3
+ debug: 0,
4
+ info: 1,
5
+ warn: 2,
6
+ error: 3,
7
+ };
8
+ function safeStringify(data) {
9
+ if (data instanceof Error) {
10
+ return JSON.stringify({ message: data.message, stack: data.stack });
11
+ }
12
+ try {
13
+ return JSON.stringify(data);
14
+ }
15
+ catch {
16
+ return "[unserializable]";
17
+ }
18
+ }
19
+ class Logger {
20
+ level;
21
+ context;
22
+ static logStream = null;
23
+ constructor(level = "info", context) {
24
+ this.level = level;
25
+ this.context = context;
26
+ }
27
+ static enableFileLogging(filePath) {
28
+ if (Logger.logStream) {
29
+ Logger.logStream.end();
30
+ }
31
+ Logger.logStream = createWriteStream(filePath, { flags: "a" });
32
+ Logger.logStream.on("error", (err) => {
33
+ console.error("Log file write error:", err.message);
34
+ });
35
+ }
36
+ shouldLog(level) {
37
+ return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
38
+ }
39
+ formatMessage(level, message, data) {
40
+ const timestamp = new Date().toISOString();
41
+ const prefix = this.context ? `[${this.context}] ` : "";
42
+ const dataStr = data !== undefined ? ` ${safeStringify(data)}` : "";
43
+ return `${timestamp} ${level.toUpperCase().padEnd(5)} ${prefix}${message}${dataStr}`;
44
+ }
45
+ output(formatted) {
46
+ console.error(formatted);
47
+ Logger.logStream?.write(formatted + "\n");
48
+ }
49
+ debug(message, data) {
50
+ if (this.shouldLog("debug")) {
51
+ this.output(this.formatMessage("debug", message, data));
52
+ }
53
+ }
54
+ info(message, data) {
55
+ if (this.shouldLog("info")) {
56
+ this.output(this.formatMessage("info", message, data));
57
+ }
58
+ }
59
+ warn(message, data) {
60
+ if (this.shouldLog("warn")) {
61
+ this.output(this.formatMessage("warn", message, data));
62
+ }
63
+ }
64
+ error(message, error) {
65
+ if (this.shouldLog("error")) {
66
+ const errorData = error instanceof Error
67
+ ? { message: error.message, stack: error.stack }
68
+ : error;
69
+ this.output(this.formatMessage("error", message, errorData));
70
+ }
71
+ }
72
+ child(context) {
73
+ const childContext = this.context ? `${this.context}:${context}` : context;
74
+ return new Logger(this.level, childContext);
75
+ }
76
+ }
77
+ const VALID_LOG_LEVELS = new Set(Object.keys(LOG_LEVELS));
78
+ function parseLogLevel(envValue) {
79
+ if (envValue && VALID_LOG_LEVELS.has(envValue)) {
80
+ return envValue;
81
+ }
82
+ if (envValue) {
83
+ console.error(`Invalid LOG_LEVEL "${envValue}". Valid values: ${[...VALID_LOG_LEVELS].join(", ")}. Falling back to "info".`);
84
+ }
85
+ return "info";
86
+ }
87
+ export const logger = new Logger(parseLogLevel(process.env.LOG_LEVEL));
88
+ export function createLogger(context) {
89
+ return logger.child(context);
90
+ }
91
+ export function enableFileLogging(filePath) {
92
+ Logger.enableFileLogging(filePath);
93
+ }
94
+ /**
95
+ * Slack Bolt용 로거 어댑터.
96
+ * Bolt 내부 로그를 커스텀 로거로 라우팅하여
97
+ * 타임스탬프 + stderr 출력을 일관되게 유지한다.
98
+ */
99
+ export function createBoltLogger() {
100
+ let boltLog = logger.child("bolt");
101
+ return {
102
+ debug(...msg) { boltLog.debug(msg.map(String).join(" ")); },
103
+ info(...msg) { boltLog.info(msg.map(String).join(" ")); },
104
+ warn(...msg) { boltLog.warn(msg.map(String).join(" ")); },
105
+ error(...msg) { boltLog.error(msg.map(String).join(" ")); },
106
+ setLevel() { },
107
+ getLevel() { return "info"; },
108
+ setName(name) { boltLog = logger.child(name); },
109
+ };
110
+ }
@@ -0,0 +1,11 @@
1
+ export declare const ENV_FILE: string;
2
+ export declare const DATA_DIR: string;
3
+ export declare const SESSIONS_FILE: string;
4
+ export declare const WORKSPACES_FILE: string;
5
+ export declare const CHANNELS_FILE: string;
6
+ export declare const LOG_DIR: string;
7
+ export declare const LOG_OUT: string;
8
+ export declare const LOG_ERR: string;
9
+ export declare const PID_FILE: string;
10
+ /** DATA_DIR, LOG_DIR 가 없으면 생성. 서버/데몬 기동 시에만 호출할 것 */
11
+ export declare function ensureDirs(): void;
@@ -0,0 +1,18 @@
1
+ import { mkdirSync } from "fs";
2
+ import { join } from "path";
3
+ // 프로젝트 루트: 프로세스 실행 디렉토리 기준
4
+ const PROJECT_ROOT = process.cwd();
5
+ export const ENV_FILE = join(PROJECT_ROOT, ".env");
6
+ export const DATA_DIR = join(PROJECT_ROOT, "data");
7
+ export const SESSIONS_FILE = join(DATA_DIR, "sessions.json");
8
+ export const WORKSPACES_FILE = join(DATA_DIR, "workspaces.json");
9
+ export const CHANNELS_FILE = join(DATA_DIR, "channels.json");
10
+ export const LOG_DIR = join(PROJECT_ROOT, "logs");
11
+ export const LOG_OUT = join(LOG_DIR, "twindevbot.out.log");
12
+ export const LOG_ERR = join(LOG_DIR, "twindevbot.err.log");
13
+ export const PID_FILE = join(DATA_DIR, "twindevbot.pid");
14
+ /** DATA_DIR, LOG_DIR 가 없으면 생성. 서버/데몬 기동 시에만 호출할 것 */
15
+ export function ensureDirs() {
16
+ mkdirSync(DATA_DIR, { recursive: true });
17
+ mkdirSync(LOG_DIR, { recursive: true });
18
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Cross-platform home directory.
3
+ * Replaces all instances of: process.env.HOME || "/root"
4
+ */
5
+ export declare function getHomeDir(): string;
6
+ /**
7
+ * Cross-platform tilde expansion.
8
+ * Replaces ~/ (or ~\) with the user's home directory.
9
+ */
10
+ export declare function expandTilde(p: string): string;
11
+ /**
12
+ * Default base directory for projects.
13
+ */
14
+ export declare function getDefaultBaseDir(): string;
15
+ /**
16
+ * Whether the current platform supports daemon management.
17
+ */
18
+ export declare function isDaemonSupported(): boolean;
@@ -0,0 +1,33 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ /**
4
+ * Cross-platform home directory.
5
+ * Replaces all instances of: process.env.HOME || "/root"
6
+ */
7
+ export function getHomeDir() {
8
+ return homedir();
9
+ }
10
+ /**
11
+ * Cross-platform tilde expansion.
12
+ * Replaces ~/ (or ~\) with the user's home directory.
13
+ */
14
+ export function expandTilde(p) {
15
+ if (p === "~")
16
+ return getHomeDir();
17
+ if (p.startsWith("~/") || p.startsWith("~\\")) {
18
+ return join(getHomeDir(), p.slice(2));
19
+ }
20
+ return p;
21
+ }
22
+ /**
23
+ * Default base directory for projects.
24
+ */
25
+ export function getDefaultBaseDir() {
26
+ return join(getHomeDir(), "Desktop");
27
+ }
28
+ /**
29
+ * Whether the current platform supports daemon management.
30
+ */
31
+ export function isDaemonSupported() {
32
+ return process.platform === "darwin" || process.platform === "win32";
33
+ }
@@ -0,0 +1,3 @@
1
+ import type { DaemonManager } from "./types.js";
2
+ export type { DaemonManager } from "./types.js";
3
+ export declare function createDaemonManager(): Promise<DaemonManager>;
@@ -0,0 +1,14 @@
1
+ export async function createDaemonManager() {
2
+ switch (process.platform) {
3
+ case "darwin": {
4
+ const { MacOSDaemonManager } = await import("./macos.js");
5
+ return new MacOSDaemonManager();
6
+ }
7
+ case "win32": {
8
+ const { WindowsDaemonManager } = await import("./windows.js");
9
+ return new WindowsDaemonManager();
10
+ }
11
+ default:
12
+ throw new Error(`Daemon management is not supported on ${process.platform}`);
13
+ }
14
+ }
@@ -0,0 +1,8 @@
1
+ import type { DaemonManager } from "./types.js";
2
+ export declare class MacOSDaemonManager implements DaemonManager {
3
+ start(): void;
4
+ stop(): void;
5
+ status(): void;
6
+ isRunning(): boolean;
7
+ getLogViewCommand(logPath: string): string;
8
+ }
@@ -0,0 +1,150 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { getHomeDir } from "../core/platform.js";
5
+ import { LOG_OUT, LOG_ERR } from "../core/paths.js";
6
+ import { t } from "../i18n/index.js";
7
+ const LABEL = "com.twin-dev-bot";
8
+ const PLIST_DIR = join(getHomeDir(), "Library", "LaunchAgents");
9
+ const PLIST_PATH = join(PLIST_DIR, `${LABEL}.plist`);
10
+ const GUI_DOMAIN = `gui/${process.getuid()}`;
11
+ function getNodePath() {
12
+ try {
13
+ return execSync("which node", { encoding: "utf-8" }).trim();
14
+ }
15
+ catch {
16
+ return "/usr/local/bin/node";
17
+ }
18
+ }
19
+ function getTwindevbotPath() {
20
+ try {
21
+ return execSync("which twindevbot", { encoding: "utf-8" }).trim();
22
+ }
23
+ catch {
24
+ return process.argv[1];
25
+ }
26
+ }
27
+ function escapeXml(str) {
28
+ return str
29
+ .replace(/&/g, "&amp;")
30
+ .replace(/</g, "&lt;")
31
+ .replace(/>/g, "&gt;")
32
+ .replace(/"/g, "&quot;")
33
+ .replace(/'/g, "&apos;");
34
+ }
35
+ function buildPlist() {
36
+ const nodePath = getNodePath();
37
+ const cliPath = getTwindevbotPath();
38
+ return `<?xml version="1.0" encoding="UTF-8"?>
39
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
40
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
41
+ <plist version="1.0">
42
+ <dict>
43
+ <key>Label</key>
44
+ <string>${escapeXml(LABEL)}</string>
45
+ <key>ProgramArguments</key>
46
+ <array>
47
+ <string>${escapeXml(nodePath)}</string>
48
+ <string>${escapeXml(cliPath)}</string>
49
+ <string>start</string>
50
+ </array>
51
+ <key>RunAtLoad</key>
52
+ <true/>
53
+ <key>KeepAlive</key>
54
+ <true/>
55
+ <key>WorkingDirectory</key>
56
+ <string>${escapeXml(process.cwd())}</string>
57
+ <key>EnvironmentVariables</key>
58
+ <dict>
59
+ <key>PATH</key>
60
+ <string>${escapeXml(process.env.PATH ?? "")}</string>
61
+ </dict>
62
+ <key>StandardOutPath</key>
63
+ <string>${escapeXml(LOG_OUT)}</string>
64
+ <key>StandardErrorPath</key>
65
+ <string>${escapeXml(LOG_ERR)}</string>
66
+ </dict>
67
+ </plist>`;
68
+ }
69
+ export class MacOSDaemonManager {
70
+ start() {
71
+ if (!existsSync(PLIST_DIR)) {
72
+ mkdirSync(PLIST_DIR, { recursive: true });
73
+ }
74
+ // plist 파일 유무와 관계없이 항상 bootout 시도 (수동 삭제된 경우 대비)
75
+ try {
76
+ execSync(`launchctl bootout ${GUI_DOMAIN}/${LABEL}`, { stdio: "ignore" });
77
+ }
78
+ catch {
79
+ // 등록되어 있지 않으면 무시
80
+ }
81
+ const plist = buildPlist();
82
+ writeFileSync(PLIST_PATH, plist);
83
+ console.log(t("cli.daemon.plistCreated", { path: PLIST_PATH }));
84
+ try {
85
+ execSync(`launchctl bootstrap ${GUI_DOMAIN} "${PLIST_PATH}"`);
86
+ console.log(t("cli.daemon.started"));
87
+ console.log("");
88
+ console.log(` Status: twindevbot status`);
89
+ console.log(` Stop: twindevbot stop`);
90
+ console.log(` Logs: ${this.getLogViewCommand(LOG_ERR)}`);
91
+ }
92
+ catch (err) {
93
+ console.error(t("cli.daemon.failedToStart"), err);
94
+ process.exit(1);
95
+ }
96
+ }
97
+ stop() {
98
+ if (!existsSync(PLIST_PATH)) {
99
+ console.log(t("cli.daemon.notInstalled"));
100
+ return;
101
+ }
102
+ try {
103
+ execSync(`launchctl bootout ${GUI_DOMAIN}/${LABEL}`);
104
+ }
105
+ catch {
106
+ // 이미 중지되어 있을 수 있음
107
+ }
108
+ unlinkSync(PLIST_PATH);
109
+ console.log(t("cli.daemon.stopped"));
110
+ }
111
+ status() {
112
+ if (!existsSync(PLIST_PATH)) {
113
+ console.log(t("cli.status.notInstalled"));
114
+ console.log(t("cli.status.notInstalledHint"));
115
+ return;
116
+ }
117
+ try {
118
+ const output = execSync(`launchctl list "${LABEL}"`, {
119
+ encoding: "utf-8",
120
+ });
121
+ const pidMatch = output.match(/"PID"\s*=\s*(\d+)/);
122
+ if (pidMatch) {
123
+ console.log(t("cli.status.running", { pid: pidMatch[1] }));
124
+ }
125
+ else {
126
+ console.log(t("cli.status.registered"));
127
+ }
128
+ console.log(` Logs: ${this.getLogViewCommand(LOG_ERR)}`);
129
+ }
130
+ catch {
131
+ console.log(t("cli.status.notRunning"));
132
+ console.log(t("cli.status.checkLogs"));
133
+ console.log(` tail -50 "${LOG_ERR}"`);
134
+ }
135
+ }
136
+ isRunning() {
137
+ if (!existsSync(PLIST_PATH))
138
+ return false;
139
+ try {
140
+ const output = execSync(`launchctl list "${LABEL}"`, { encoding: "utf-8" });
141
+ return !!output.match(/"PID"\s*=\s*(\d+)/);
142
+ }
143
+ catch {
144
+ return false;
145
+ }
146
+ }
147
+ getLogViewCommand(logPath) {
148
+ return `tail -f "${logPath}"`;
149
+ }
150
+ }
@@ -0,0 +1,9 @@
1
+ export interface DaemonManager {
2
+ start(): void;
3
+ stop(): void;
4
+ status(): void;
5
+ /** 데몬이 현재 실행 중인지 확인 */
6
+ isRunning(): boolean;
7
+ /** 플랫폼에 맞는 로그 조회 명령어 */
8
+ getLogViewCommand(logPath: string): string;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { DaemonManager } from "./types.js";
2
+ export declare class WindowsDaemonManager implements DaemonManager {
3
+ start(): void;
4
+ stop(): void;
5
+ status(): void;
6
+ isRunning(): boolean;
7
+ getLogViewCommand(logPath: string): string;
8
+ }
@@ -0,0 +1,137 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { LOG_DIR, LOG_OUT, LOG_ERR, PID_FILE } from "../core/paths.js";
5
+ import { t } from "../i18n/index.js";
6
+ const TASK_NAME = "TwinDevBot";
7
+ const WRAPPER_SCRIPT = join(LOG_DIR, "twindevbot-daemon.bat");
8
+ function getNodePath() {
9
+ try {
10
+ return execSync("where node", { encoding: "utf-8" }).trim().split(/\r?\n/)[0];
11
+ }
12
+ catch {
13
+ return process.execPath;
14
+ }
15
+ }
16
+ function getTwindevbotPath() {
17
+ try {
18
+ return execSync("where twindevbot", { encoding: "utf-8" }).trim().split(/\r?\n/)[0];
19
+ }
20
+ catch {
21
+ return process.argv[1];
22
+ }
23
+ }
24
+ export class WindowsDaemonManager {
25
+ start() {
26
+ // 기존 태스크가 있으면 제거
27
+ try {
28
+ execSync(`schtasks /delete /tn "${TASK_NAME}" /f`, { stdio: "ignore" });
29
+ }
30
+ catch {
31
+ // 태스크가 없을 수 있음
32
+ }
33
+ const nodePath = getNodePath();
34
+ const cliPath = getTwindevbotPath();
35
+ // stdout/stderr를 로그 파일로 리다이렉션하는 래퍼 스크립트 생성
36
+ // Task Scheduler는 기본적으로 System32 디렉토리에서 실행되므로 프로젝트 루트로 cd 필요
37
+ const projectRoot = process.cwd();
38
+ const scriptContent = `@echo off\r\ncd /d "${projectRoot}"\r\n"${nodePath}" "${cliPath}" start 1>>"${LOG_OUT}" 2>>"${LOG_ERR}"\r\n`;
39
+ writeFileSync(WRAPPER_SCRIPT, scriptContent);
40
+ const command = `"\\"${WRAPPER_SCRIPT}\\""`;
41
+ try {
42
+ execSync(`schtasks /create /tn "${TASK_NAME}" /tr ${command} /sc onlogon /rl limited /f`);
43
+ console.log(t("cli.daemon.taskCreated", { name: TASK_NAME }));
44
+ // 태스크를 즉시 실행
45
+ execSync(`schtasks /run /tn "${TASK_NAME}"`, { stdio: "ignore" });
46
+ console.log(t("cli.daemon.started"));
47
+ console.log("");
48
+ console.log(` Status: twindevbot status`);
49
+ console.log(` Stop: twindevbot stop`);
50
+ console.log(` Logs: ${this.getLogViewCommand(LOG_ERR)}`);
51
+ }
52
+ catch (err) {
53
+ console.error(t("cli.daemon.failedToStart"), err);
54
+ process.exit(1);
55
+ }
56
+ }
57
+ stop() {
58
+ // PID 파일로 프로세스 트리 전체 종료 (자식 Claude 프로세스 포함)
59
+ // schtasks /end는 최상위 프로세스만 종료하므로 자식 프로세스가 고아가 됨
60
+ if (existsSync(PID_FILE)) {
61
+ try {
62
+ const pid = readFileSync(PID_FILE, "utf-8").trim();
63
+ if (!/^\d+$/.test(pid)) {
64
+ throw new Error("Invalid PID");
65
+ }
66
+ execSync(`taskkill /pid ${pid} /T /F`, { stdio: "ignore" });
67
+ }
68
+ catch {
69
+ // 프로세스가 이미 종료되었을 수 있음
70
+ }
71
+ try {
72
+ unlinkSync(PID_FILE);
73
+ }
74
+ catch {
75
+ // 파일 삭제 실패 무시
76
+ }
77
+ }
78
+ // 실행 중인 태스크 종료 (PID 파일이 없는 경우 대비)
79
+ try {
80
+ execSync(`schtasks /end /tn "${TASK_NAME}"`, { stdio: "ignore" });
81
+ }
82
+ catch {
83
+ // 실행 중이 아닐 수 있음
84
+ }
85
+ // 태스크 삭제
86
+ try {
87
+ execSync(`schtasks /delete /tn "${TASK_NAME}" /f`);
88
+ console.log(t("cli.daemon.stopped"));
89
+ }
90
+ catch {
91
+ console.log(t("cli.daemon.notInstalled"));
92
+ }
93
+ // 래퍼 스크립트 정리
94
+ if (existsSync(WRAPPER_SCRIPT)) {
95
+ unlinkSync(WRAPPER_SCRIPT);
96
+ }
97
+ }
98
+ status() {
99
+ try {
100
+ const output = execSync(`schtasks /query /tn "${TASK_NAME}" /v /fo list`, { encoding: "utf-8" });
101
+ const statusMatch = output.match(/Status:\s*(.+)/);
102
+ if (statusMatch && statusMatch[1].trim() === "Running") {
103
+ let pid = "-";
104
+ if (existsSync(PID_FILE)) {
105
+ try {
106
+ pid = readFileSync(PID_FILE, "utf-8").trim();
107
+ }
108
+ catch {
109
+ // PID 파일 읽기 실패
110
+ }
111
+ }
112
+ console.log(t("cli.status.running", { pid }));
113
+ }
114
+ else {
115
+ console.log(t("cli.status.registered"));
116
+ }
117
+ console.log(` Logs: ${this.getLogViewCommand(LOG_ERR)}`);
118
+ }
119
+ catch {
120
+ console.log(t("cli.status.notInstalled"));
121
+ console.log(t("cli.status.notInstalledHint"));
122
+ }
123
+ }
124
+ isRunning() {
125
+ try {
126
+ const output = execSync(`schtasks /query /tn "${TASK_NAME}" /v /fo list`, { encoding: "utf-8" });
127
+ const statusMatch = output.match(/Status:\s*(.+)/);
128
+ return !!(statusMatch && statusMatch[1].trim() === "Running");
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ }
134
+ getLogViewCommand(logPath) {
135
+ return `Get-Content "${logPath}" -Wait`;
136
+ }
137
+ }
@@ -0,0 +1,2 @@
1
+ import type { App } from "@slack/bolt";
2
+ export declare function registerClaudeCommand(app: App): void;