sanjang 0.3.3 → 0.3.5

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.
@@ -0,0 +1,27 @@
1
+ import type { ChangeReport, ChangeReportFile, ChangeReportWarning } from "../types.ts";
2
+ type FileCategory = "ui" | "api" | "config" | "test" | "docs" | "other";
3
+ /**
4
+ * Classify a file path into one of the known categories.
5
+ * Rules are checked in priority order: test > ui > api > docs > config > other
6
+ */
7
+ export declare function categorizeFile(filePath: string): FileCategory;
8
+ /**
9
+ * Detect warnings from a list of categorized files.
10
+ * Returns deduplicated warnings (one per type).
11
+ */
12
+ export declare function detectWarnings(files: ChangeReportFile[]): ChangeReportWarning[];
13
+ /**
14
+ * Build a ChangeReport from raw file list (path + status).
15
+ * summary and humanDescription are set to null — call generateReportSummary to enrich.
16
+ */
17
+ export declare function buildChangeReport(rawFiles: {
18
+ path: string;
19
+ status: ChangeReportFile["status"];
20
+ }[]): ChangeReport;
21
+ /**
22
+ * Enrich a ChangeReport with AI-generated category-level descriptions.
23
+ * Each category gets human-readable bullet points explaining what changed.
24
+ * Tries `claude -p --model haiku` and falls back to file-based summary.
25
+ */
26
+ export declare function generateReportSummary(diffStat: string, diff: string, report: ChangeReport): ChangeReport;
27
+ export {};
@@ -0,0 +1,233 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { basename, extname } from "node:path";
3
+ /**
4
+ * Classify a file path into one of the known categories.
5
+ * Rules are checked in priority order: test > ui > api > docs > config > other
6
+ */
7
+ export function categorizeFile(filePath) {
8
+ const name = basename(filePath);
9
+ const ext = extname(filePath).toLowerCase();
10
+ const lower = filePath.toLowerCase();
11
+ // 1. test
12
+ if (/\.(test|spec)\.[^.]+$/.test(lower))
13
+ return "test";
14
+ if (/[/\\](test|tests|__tests__|__mocks__|fixtures)[/\\]/.test(lower))
15
+ return "test";
16
+ if (/^(test|tests|__tests__|__mocks__|fixtures)[/\\]/.test(lower))
17
+ return "test";
18
+ // 2. ui
19
+ const uiExts = new Set([
20
+ ".css",
21
+ ".scss",
22
+ ".less",
23
+ ".sass",
24
+ ".styl",
25
+ ".html",
26
+ ".htm",
27
+ ".svg",
28
+ ".tsx",
29
+ ".jsx",
30
+ ".vue",
31
+ ".svelte",
32
+ ]);
33
+ if (uiExts.has(ext))
34
+ return "ui";
35
+ if (/[/\\](pages|views|components|layouts|styles|public)[/\\]/.test(lower))
36
+ return "ui";
37
+ if (/^(pages|views|components|layouts|styles|public)[/\\]/.test(lower))
38
+ return "ui";
39
+ // 3. api
40
+ if (/[/\\](api|routes|controllers|handlers|middleware|graphql|resolvers|schema)[/\\]/.test(lower))
41
+ return "api";
42
+ if (/^(api|routes|controllers|handlers|middleware|graphql|resolvers|schema)[/\\]/.test(lower))
43
+ return "api";
44
+ // files named "server" (any extension)
45
+ if (/[/\\]server\.[^/\\]+$/.test(lower) || /^server\.[^/\\]+$/.test(lower))
46
+ return "api";
47
+ // 4. docs
48
+ if (ext === ".md")
49
+ return "docs";
50
+ if (/[/\\]docs[/\\]/.test(lower) || /^docs[/\\]/.test(lower))
51
+ return "docs";
52
+ if (/^(README|CHANGELOG|LICENSE|CONTRIBUTING)(\.|$)/i.test(name))
53
+ return "docs";
54
+ // 5. config
55
+ const configPrefixes = [
56
+ "package",
57
+ "tsconfig",
58
+ "jest.config",
59
+ "vite.config",
60
+ "next.config",
61
+ "webpack.config",
62
+ "biome",
63
+ ".eslint",
64
+ ".prettier",
65
+ ".babel",
66
+ ];
67
+ const nameLower = name.toLowerCase();
68
+ if (configPrefixes.some((p) => nameLower.startsWith(p.toLowerCase())))
69
+ return "config";
70
+ if (name.startsWith("."))
71
+ return "config";
72
+ if ([".json", ".yaml", ".yml", ".toml"].includes(ext))
73
+ return "config";
74
+ // 6. other
75
+ return "other";
76
+ }
77
+ /**
78
+ * Detect warnings from a list of categorized files.
79
+ * Returns deduplicated warnings (one per type).
80
+ */
81
+ export function detectWarnings(files) {
82
+ const seen = new Set();
83
+ const warnings = [];
84
+ const add = (type, message, file) => {
85
+ if (seen.has(type))
86
+ return;
87
+ seen.add(type);
88
+ warnings.push({ type, message, file });
89
+ };
90
+ for (const f of files) {
91
+ const p = f.path.toLowerCase();
92
+ const name = basename(f.path);
93
+ // env
94
+ if (/\.env($|\.)/.test(name.toLowerCase())) {
95
+ add("env", "환경 변수 파일이 변경되었습니다", f.path);
96
+ }
97
+ // db
98
+ if (/migrat|schema|\.sql|prisma/.test(p)) {
99
+ add("db", "데이터베이스 스키마 또는 마이그레이션이 변경되었습니다", f.path);
100
+ }
101
+ // infra
102
+ if (/dockerfile|docker-compose|\.github\/|deploy\/|[/\\]k8s[/\\]|^k8s[/\\]|terraform|infrastructure/.test(p)) {
103
+ add("infra", "인프라 설정이 변경되었습니다", f.path);
104
+ }
105
+ // config (package manager files)
106
+ if (/package\.json|package-lock|yarn\.lock|pnpm-lock/.test(p)) {
107
+ add("config", "패키지 의존성이 변경되었습니다", f.path);
108
+ }
109
+ // security
110
+ if (/auth|security|token|secret|credential|password/.test(p)) {
111
+ add("security", "보안 관련 파일이 변경되었습니다", f.path);
112
+ }
113
+ }
114
+ return warnings;
115
+ }
116
+ /**
117
+ * Build a ChangeReport from raw file list (path + status).
118
+ * summary and humanDescription are set to null — call generateReportSummary to enrich.
119
+ */
120
+ export function buildChangeReport(rawFiles) {
121
+ const files = rawFiles.map((f) => ({
122
+ path: f.path,
123
+ status: f.status,
124
+ category: categorizeFile(f.path),
125
+ }));
126
+ const byCategory = {};
127
+ for (const f of files) {
128
+ const cat = f.category;
129
+ if (!byCategory[cat])
130
+ byCategory[cat] = [];
131
+ byCategory[cat].push(f);
132
+ }
133
+ const warnings = detectWarnings(files);
134
+ // 기본 카테고리 설명 — AI 없이도 즉시 표시
135
+ const categoryDetails = {};
136
+ for (const [cat, catFiles] of Object.entries(byCategory)) {
137
+ categoryDetails[cat] = catFiles.map((f) => {
138
+ const name = f.path.split("/").pop() || f.path;
139
+ return f.status === "새 파일" ? `${name} 추가됨` : `${name} 수정됨`;
140
+ });
141
+ }
142
+ return {
143
+ files,
144
+ totalCount: files.length,
145
+ byCategory,
146
+ warnings,
147
+ summary: null,
148
+ humanDescription: null,
149
+ categoryDetails,
150
+ };
151
+ }
152
+ /**
153
+ * Enrich a ChangeReport with AI-generated category-level descriptions.
154
+ * Each category gets human-readable bullet points explaining what changed.
155
+ * Tries `claude -p --model haiku` and falls back to file-based summary.
156
+ */
157
+ export function generateReportSummary(diffStat, diff, report) {
158
+ const categoryNames = {
159
+ ui: "화면",
160
+ api: "서버/API",
161
+ config: "설정",
162
+ test: "테스트",
163
+ docs: "문서",
164
+ other: "기타",
165
+ };
166
+ const categoryList = Object.keys(report.byCategory)
167
+ .map((cat) => `${categoryNames[cat] || cat}: ${(report.byCategory[cat] ?? []).length}개`)
168
+ .join(", ");
169
+ const fallbackSummary = `${categoryList} 변경`;
170
+ // Build per-category diff sections for the prompt
171
+ const categoryDiffSections = Object.entries(report.byCategory)
172
+ .map(([cat, files]) => {
173
+ const fileList = files.map((f) => ` ${f.status} ${f.path}`).join("\n");
174
+ return `[${categoryNames[cat] || cat}]\n${fileList}`;
175
+ })
176
+ .join("\n\n");
177
+ const prompt = `너는 비개발자에게 코드 변경사항을 설명하는 도우미야.
178
+ 아래 git diff를 분석하고, 카테고리별로 "실제로 뭐가 바뀌었는지"를 설명해.
179
+
180
+ 규칙:
181
+ - 파일명이 아니라 사용자 관점에서 뭐가 바뀌었는지 설명 (예: "로그인 버튼이 보라색으로 바뀌었어요")
182
+ - 각 항목은 한국어, '~했어요/~됐어요' 체, 한 줄
183
+ - 새 파일이면 "추가됐어요", 수정이면 구체적으로 뭐가 바뀌었는지
184
+
185
+ 카테고리별 파일:
186
+ ${categoryDiffSections}
187
+
188
+ diff:
189
+ ${diff.slice(0, 4000)}
190
+
191
+ JSON으로만 응답해 (다른 텍스트 없이):
192
+ {"summary": "전체 한 줄 요약 (30자 이내)", "categories": {"ui": ["설명1", "설명2"], "api": ["설명1"], ...}}
193
+
194
+ categories의 키는 반드시 다음 중 하나: ${Object.keys(report.byCategory).join(", ")}`;
195
+ try {
196
+ const result = spawnSync("claude", ["-p", prompt], {
197
+ encoding: "utf8",
198
+ timeout: 30_000,
199
+ });
200
+ if (result.status === 0 && result.stdout) {
201
+ const output = result.stdout.trim();
202
+ const jsonMatch = output.match(/\{[\s\S]*"summary"[\s\S]*"categories"[\s\S]*\}/);
203
+ if (jsonMatch) {
204
+ const parsed = JSON.parse(jsonMatch[0]);
205
+ if (parsed.summary && parsed.categories) {
206
+ return {
207
+ ...report,
208
+ summary: parsed.summary,
209
+ humanDescription: null,
210
+ categoryDetails: parsed.categories,
211
+ };
212
+ }
213
+ }
214
+ }
215
+ }
216
+ catch {
217
+ // Fall through to fallback
218
+ }
219
+ // Fallback: 파일 기반 설명 생성
220
+ const fallbackDetails = {};
221
+ for (const [cat, files] of Object.entries(report.byCategory)) {
222
+ fallbackDetails[cat] = files.map((f) => {
223
+ const name = f.path.split("/").pop() || f.path;
224
+ return f.status === "새 파일" ? `${name} 파일이 추가됐어요` : `${name} 파일이 수정됐어요`;
225
+ });
226
+ }
227
+ return {
228
+ ...report,
229
+ summary: fallbackSummary,
230
+ humanDescription: null,
231
+ categoryDetails: fallbackDetails,
232
+ };
233
+ }
@@ -14,9 +14,7 @@ function checkPortConflict(processInfo) {
14
14
  name: "port-conflict",
15
15
  status: hit ? "error" : "ok",
16
16
  detail: hit ? "다른 프로그램과 충돌이 발생했습니다." : "정상.",
17
- guide: hit
18
- ? '"중지" → "시작"을 눌러보세요. 계속되면 "삭제" 후 다시 만들어보세요.'
19
- : null,
17
+ guide: hit ? '"중지" → "시작"을 눌러보세요. 계속되면 "삭제" 후 다시 만들어보세요.' : null,
20
18
  };
21
19
  }
22
20
  function checkFrontendExit(processInfo) {
@@ -47,9 +45,7 @@ function checkFePort(pg) {
47
45
  return {
48
46
  name: "fe-status",
49
47
  status: output?.length ? "ok" : "warn",
50
- detail: output?.length
51
- ? "Frontend 서버가 실행 중입니다."
52
- : "Frontend 서버가 응답하지 않습니다.",
48
+ detail: output?.length ? "Frontend 서버가 실행 중입니다." : "Frontend 서버가 응답하지 않습니다.",
53
49
  guide: !output?.length ? '"시작" 버튼을 눌러보세요.' : null,
54
50
  };
55
51
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Main branch dev server manager.
3
+ * Runs a dev server from the project root for side-by-side comparison preview.
4
+ */
5
+ import type { SanjangConfig } from "../types.ts";
6
+ interface MainServerState {
7
+ status: "stopped" | "starting" | "running" | "error";
8
+ port: number | null;
9
+ error: string | null;
10
+ }
11
+ export declare function getMainServerState(): MainServerState;
12
+ export declare function getMainServerLogs(): string[];
13
+ export declare function startMainServer(projectRoot: string, config: SanjangConfig, onReady?: (port: number) => void): Promise<void>;
14
+ export declare function stopMainServer(): void;
15
+ export {};
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Main branch dev server manager.
3
+ * Runs a dev server from the project root for side-by-side comparison preview.
4
+ */
5
+ import { spawn } from "node:child_process";
6
+ import { createConnection } from "node:net";
7
+ let state = { status: "stopped", port: null, error: null };
8
+ let proc = null;
9
+ let logs = [];
10
+ export function getMainServerState() {
11
+ return { ...state };
12
+ }
13
+ export function getMainServerLogs() {
14
+ return [...logs];
15
+ }
16
+ export async function startMainServer(projectRoot, config, onReady) {
17
+ if (state.status === "running" || state.status === "starting")
18
+ return;
19
+ state = { status: "starting", port: null, error: null };
20
+ logs = [];
21
+ const basePort = config.dev.port + 100;
22
+ const cmdParts = config.dev.command.split(/\s+/);
23
+ const cmd = cmdParts[0];
24
+ const args = cmdParts.slice(1);
25
+ if (config.dev.portFlag) {
26
+ args.push(config.dev.portFlag, String(basePort));
27
+ }
28
+ const cwd = config.dev.cwd
29
+ ? config.dev.cwd.startsWith("/")
30
+ ? config.dev.cwd
31
+ : `${projectRoot}/${config.dev.cwd}`
32
+ : projectRoot;
33
+ proc = spawn(cmd, args, {
34
+ cwd,
35
+ stdio: ["ignore", "pipe", "pipe"],
36
+ env: { ...process.env, ...config.dev.env, FORCE_COLOR: "0" },
37
+ shell: true,
38
+ });
39
+ proc.stdout?.on("data", (chunk) => {
40
+ const line = chunk.toString();
41
+ logs.push(line);
42
+ if (logs.length > 100)
43
+ logs.shift();
44
+ });
45
+ proc.stderr?.on("data", (chunk) => {
46
+ const line = chunk.toString();
47
+ logs.push(line);
48
+ if (logs.length > 100)
49
+ logs.shift();
50
+ });
51
+ proc.on("close", (code) => {
52
+ if (state.status !== "stopped") {
53
+ state = { status: "stopped", port: null, error: code ? `exit ${code}` : null };
54
+ }
55
+ proc = null;
56
+ });
57
+ proc.on("error", (err) => {
58
+ state = { status: "error", port: null, error: err.message };
59
+ proc = null;
60
+ });
61
+ const detectedPort = await detectMainPort(logs, basePort, 15_000);
62
+ if (detectedPort) {
63
+ state = { status: "running", port: detectedPort, error: null };
64
+ onReady?.(detectedPort);
65
+ }
66
+ else {
67
+ state = { status: "error", port: null, error: "포트를 감지하지 못했어요" };
68
+ }
69
+ }
70
+ export function stopMainServer() {
71
+ state = { status: "stopped", port: null, error: null };
72
+ if (proc) {
73
+ proc.kill("SIGTERM");
74
+ proc = null;
75
+ }
76
+ logs = [];
77
+ }
78
+ function detectMainPort(logLines, _fallbackPort, timeoutMs) {
79
+ return new Promise((resolve) => {
80
+ const deadline = Date.now() + timeoutMs;
81
+ const portRe = /https?:\/\/localhost:(\d+)/;
82
+ function check() {
83
+ for (const line of logLines) {
84
+ const match = portRe.exec(line);
85
+ if (match?.[1]) {
86
+ const port = parseInt(match[1], 10);
87
+ const sock = createConnection({ port, host: "localhost" });
88
+ sock.once("connect", () => {
89
+ sock.destroy();
90
+ resolve(port);
91
+ });
92
+ sock.once("error", () => {
93
+ sock.destroy();
94
+ if (Date.now() < deadline)
95
+ setTimeout(check, 1000);
96
+ else
97
+ resolve(port);
98
+ });
99
+ return;
100
+ }
101
+ }
102
+ if (Date.now() >= deadline) {
103
+ resolve(null);
104
+ }
105
+ else {
106
+ setTimeout(check, 1000);
107
+ }
108
+ }
109
+ check();
110
+ });
111
+ }
@@ -5,10 +5,19 @@ import { spawnSync } from "node:child_process";
5
5
  */
6
6
  export function aiSlugify(description) {
7
7
  try {
8
- const result = spawnSync("claude", ["-p", "--model", "haiku", `Convert this task description to a kebab-case English slug. Rules: 3-5 words, max 30 chars, lowercase, descriptive (not just one word), no explanation, output ONLY the slug.\n\nExample: "로그인 버튼 색상 변경" → "login-button-color-change"\nExample: "대시보드 차트 추가" → "dashboard-chart-add"\n\nTask: "${description}"`], { encoding: "utf8", stdio: "pipe", timeout: 10_000 });
8
+ const result = spawnSync("claude", [
9
+ "-p",
10
+ "--model",
11
+ "haiku",
12
+ `Convert this task description to a kebab-case English slug. Rules: 3-5 words, max 30 chars, lowercase, descriptive (not just one word), no explanation, output ONLY the slug.\n\nExample: "로그인 버튼 색상 변경" → "login-button-color-change"\nExample: "대시보드 차트 추가" → "dashboard-chart-add"\n\nTask: "${description}"`,
13
+ ], { encoding: "utf8", stdio: "pipe", timeout: 10_000 });
9
14
  if (result.status !== 0)
10
15
  return null;
11
- const slug = (result.stdout ?? "").trim().toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "");
16
+ const slug = (result.stdout ?? "")
17
+ .trim()
18
+ .toLowerCase()
19
+ .replace(/[^a-z0-9-]/g, "")
20
+ .replace(/^-+|-+$/g, "");
12
21
  if (!slug || slug.length > 30)
13
22
  return null;
14
23
  return slug;
@@ -27,7 +27,7 @@ export function buildFallbackPrBody({ message, actions, diffStat }) {
27
27
  export function buildClaudePrPrompt({ message, diffStat, diff }) {
28
28
  return [
29
29
  "You are writing a GitHub Pull Request description.",
30
- 'The author described the change as: "' + message + '"',
30
+ `The author described the change as: "${message}"`,
31
31
  "",
32
32
  "Here is the diff stat:",
33
33
  diffStat || "(no stat)",
@@ -24,7 +24,10 @@ function detectPortFromStdout(logs, timeoutMs) {
24
24
  const port = parseInt(match[1], 10);
25
25
  // Wait briefly for the port to actually be ready
26
26
  const sock = createConnection({ port, host: "localhost" });
27
- sock.once("connect", () => { sock.destroy(); resolve(port); });
27
+ sock.once("connect", () => {
28
+ sock.destroy();
29
+ resolve(port);
30
+ });
28
31
  sock.once("error", () => {
29
32
  sock.destroy();
30
33
  // Port printed but not ready yet, retry
@@ -1,6 +1,6 @@
1
- import { existsSync, copyFileSync, mkdirSync } from "node:fs";
2
- import { join, dirname } from "node:path";
3
1
  import { execSync } from "node:child_process";
2
+ import { copyFileSync, existsSync, mkdirSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
4
  const PATTERNS = [
5
5
  {
6
6
  test: /Cannot find module|MODULE_NOT_FOUND/i,
@@ -64,7 +64,12 @@ export function executeHeal(action, campPath, projectRoot, setupCommand, copyFil
64
64
  if (!setupCommand)
65
65
  return { action, success: false, detail: "설치 명령이 없습니다." };
66
66
  try {
67
- execSync(setupCommand, { cwd: campPath, stdio: "pipe", timeout: 120_000, shell: true });
67
+ execSync(setupCommand, {
68
+ cwd: campPath,
69
+ stdio: "pipe",
70
+ timeout: 120_000,
71
+ shell: true,
72
+ });
68
73
  return { action, success: true };
69
74
  }
70
75
  catch {
@@ -82,10 +87,16 @@ export function executeHeal(action, campPath, projectRoot, setupCommand, copyFil
82
87
  copyFileSync(src, dst);
83
88
  copied++;
84
89
  }
85
- catch { /* skip */ }
90
+ catch {
91
+ /* skip */
92
+ }
86
93
  }
87
94
  }
88
- return { action, success: copied > 0, detail: copied > 0 ? `${copied}개 파일 복사됨` : "복사할 파일이 없습니다." };
95
+ return {
96
+ action,
97
+ success: copied > 0,
98
+ detail: copied > 0 ? `${copied}개 파일 복사됨` : "복사할 파일이 없습니다.",
99
+ };
89
100
  }
90
101
  case "restart":
91
102
  // Restart is handled by the caller (stop + start)
@@ -32,7 +32,7 @@ export function deepFindEnvFiles(projectRoot, maxDepth = 4) {
32
32
  export function detectSetupIssues(projectRoot) {
33
33
  const issues = [];
34
34
  // 1. Check for env variable references without .env files
35
- const hasEnvFile = ENV_PATTERNS.some(f => existsSync(join(projectRoot, f)));
35
+ const hasEnvFile = ENV_PATTERNS.some((f) => existsSync(join(projectRoot, f)));
36
36
  if (!hasEnvFile) {
37
37
  const envRefFound = scanForEnvReferences(projectRoot);
38
38
  if (envRefFound) {
@@ -52,7 +52,7 @@ export function detectSetupIssues(projectRoot) {
52
52
  }
53
53
  // 3. Check for missing lockfile
54
54
  const lockfiles = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb", "bun.lock"];
55
- if (!lockfiles.some(f => existsSync(join(projectRoot, f)))) {
55
+ if (!lockfiles.some((f) => existsSync(join(projectRoot, f)))) {
56
56
  issues.push({
57
57
  type: "missing-lockfile",
58
58
  message: "lockfile이 없습니다. 의존성 설치가 느릴 수 있습니다.",
@@ -97,7 +97,10 @@ function scanForEnvReferences(dir, depth = 0) {
97
97
  continue;
98
98
  }
99
99
  }
100
- if (entry.isDirectory() && !SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".") && entry.name !== "node_modules") {
100
+ if (entry.isDirectory() &&
101
+ !SKIP_DIRS.has(entry.name) &&
102
+ !entry.name.startsWith(".") &&
103
+ entry.name !== "node_modules") {
101
104
  if (scanForEnvReferences(join(dir, entry.name), depth + 1))
102
105
  return true;
103
106
  }
@@ -129,9 +132,7 @@ function countTurboApps(root) {
129
132
  if (scripts?.dev)
130
133
  count++;
131
134
  }
132
- catch {
133
- continue;
134
- }
135
+ catch { }
135
136
  }
136
137
  }
137
138
  return count;
@@ -29,7 +29,7 @@ function read() {
29
29
  function write(records) {
30
30
  ensureDir();
31
31
  // Atomic write: write to temp file then rename to prevent corruption
32
- const tmp = stateFile() + ".tmp";
32
+ const tmp = `${stateFile()}.tmp`;
33
33
  writeFileSync(tmp, JSON.stringify(records, null, 2), "utf8");
34
34
  renameSync(tmp, stateFile());
35
35
  }
@@ -97,10 +97,7 @@ async function fetchRecentCommits(cwd) {
97
97
  export async function suggestTasks(projectRoot) {
98
98
  const results = [];
99
99
  // gh-dependent fetches — tolerate failure (gh not installed / no repo)
100
- const [issues, prs] = await Promise.allSettled([
101
- fetchIssues(projectRoot),
102
- fetchMyPrs(projectRoot),
103
- ]);
100
+ const [issues, prs] = await Promise.allSettled([fetchIssues(projectRoot), fetchMyPrs(projectRoot)]);
104
101
  // PRs first — most actionable ("이어하기")
105
102
  if (prs.status === "fulfilled") {
106
103
  results.push(...prs.value);
@@ -15,7 +15,7 @@ export declare function detectWarp(): WarpDetectResult;
15
15
  * Opens as a new tab in the existing Warp window (not a new window).
16
16
  * The tab title naturally shows the directory name (= camp name).
17
17
  */
18
- export declare function openWarpTab(campName: string, worktreePath: string): WarpOpenResult;
18
+ export declare function openWarpTab(_campName: string, worktreePath: string): WarpOpenResult;
19
19
  /**
20
20
  * No-op cleanup (launch config removed — using open -a instead).
21
21
  */
@@ -12,7 +12,7 @@ export function detectWarp() {
12
12
  * Opens as a new tab in the existing Warp window (not a new window).
13
13
  * The tab title naturally shows the directory name (= camp name).
14
14
  */
15
- export function openWarpTab(campName, worktreePath) {
15
+ export function openWarpTab(_campName, worktreePath) {
16
16
  const { installed } = detectWarp();
17
17
  if (!installed) {
18
18
  return { opened: false, terminal: null, path: worktreePath };