sanjang 0.3.4 โ 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.
- package/README.md +25 -13
- package/dashboard/app.js +401 -27
- package/dashboard/index.html +16 -2
- package/dashboard/style.css +83 -5
- package/dist/bin/sanjang.js +8 -6
- package/dist/lib/config.js +3 -5
- package/dist/lib/engine/change-report.d.ts +27 -0
- package/dist/lib/engine/change-report.js +233 -0
- package/dist/lib/engine/diagnostics.js +2 -6
- package/dist/lib/engine/main-server.d.ts +15 -0
- package/dist/lib/engine/main-server.js +111 -0
- package/dist/lib/engine/naming.js +11 -2
- package/dist/lib/engine/pr.js +1 -1
- package/dist/lib/engine/process.js +4 -1
- package/dist/lib/engine/self-heal.js +16 -5
- package/dist/lib/engine/smart-init.js +7 -6
- package/dist/lib/engine/state.js +1 -1
- package/dist/lib/engine/suggest.js +1 -4
- package/dist/lib/engine/warp.d.ts +1 -1
- package/dist/lib/engine/warp.js +1 -1
- package/dist/lib/server.js +241 -49
- package/dist/lib/types.d.ts +19 -0
- package/package.json +2 -2
package/dashboard/index.html
CHANGED
|
@@ -77,11 +77,17 @@
|
|
|
77
77
|
</div>
|
|
78
78
|
<div style="flex:1"></div>
|
|
79
79
|
<button class="btn btn-sub btn-sm" id="ws-terminal-btn" onclick="wsOpenTerminal()">๐ป ํฐ๋ฏธ๋</button>
|
|
80
|
+
<button class="btn btn-sub btn-sm" id="ws-compare-btn" onclick="toggleCompare()" title="์๋ณธ๊ณผ ๋น๊ต">๐ ๋น๊ต</button>
|
|
80
81
|
<button class="btn btn-sub btn-sm" onclick="togglePanel()">โฐ ํจ๋</button>
|
|
81
82
|
</div>
|
|
82
83
|
|
|
83
|
-
<!-- Preview โ full screen -->
|
|
84
|
-
<div class="ws-preview-
|
|
84
|
+
<!-- Preview โ full screen with optional split -->
|
|
85
|
+
<div class="ws-preview-container" id="ws-preview-container">
|
|
86
|
+
<div class="ws-preview-full" id="ws-preview"></div>
|
|
87
|
+
<div class="ws-preview-main hidden" id="ws-preview-main">
|
|
88
|
+
<div class="ws-preview-label">๐๏ธ ์๋ณธ (main)</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
85
91
|
|
|
86
92
|
<!-- Slide panel โ right side -->
|
|
87
93
|
<div class="ws-panel" id="ws-panel">
|
|
@@ -100,6 +106,12 @@
|
|
|
100
106
|
<div id="ws-changes"></div>
|
|
101
107
|
</details>
|
|
102
108
|
</div>
|
|
109
|
+
<div class="workspace-section ws-report-section" id="ws-report-section" style="display:none">
|
|
110
|
+
<h3>๐ ๋ณ๊ฒฝ ๋ฆฌํฌํธ</h3>
|
|
111
|
+
<div class="ws-report-summary" id="ws-report-summary"></div>
|
|
112
|
+
<div class="ws-report-warnings" id="ws-report-warnings"></div>
|
|
113
|
+
<div class="ws-report-categories" id="ws-report-categories"></div>
|
|
114
|
+
</div>
|
|
103
115
|
<div class="workspace-section">
|
|
104
116
|
<h3>๐ ์ธ์ด๋ธ ๊ธฐ๋ก</h3>
|
|
105
117
|
<div id="ws-actions"></div>
|
|
@@ -109,6 +121,7 @@
|
|
|
109
121
|
<div class="ws-browser-error-panel" id="ws-browser-errors">
|
|
110
122
|
<span style="color:var(--text-muted);font-size:12px">์๋ฌ ์์</span>
|
|
111
123
|
</div>
|
|
124
|
+
<button class="btn btn-fix" id="ws-fix-btn" onclick="copyFixPrompt()" style="display:none">๐ฉน ๊ณ ์ณ์ค โ ํด๋ฆฝ๋ณด๋ ๋ณต์ฌ</button>
|
|
112
125
|
</details>
|
|
113
126
|
<details class="workspace-section ws-log-details">
|
|
114
127
|
<summary>๐ ๋ก๊ทธ</summary>
|
|
@@ -219,6 +232,7 @@
|
|
|
219
232
|
<p id="ship-file-count" style="font-size:13px;color:var(--text-muted);margin-bottom:12px"></p>
|
|
220
233
|
|
|
221
234
|
<div class="form-group">
|
|
235
|
+
<div id="ship-report-preview" class="ship-report-preview" style="display:none"></div>
|
|
222
236
|
<label class="form-label" for="ship-message">๋ญ ๋ฐ๊ฟจ๋์? (ํ ์ค๋ก)</label>
|
|
223
237
|
<input
|
|
224
238
|
class="form-input"
|
package/dashboard/style.css
CHANGED
|
@@ -1090,6 +1090,23 @@ header h1::before {
|
|
|
1090
1090
|
word-break: break-all;
|
|
1091
1091
|
}
|
|
1092
1092
|
|
|
1093
|
+
.btn-fix {
|
|
1094
|
+
display: block;
|
|
1095
|
+
width: 100%;
|
|
1096
|
+
margin-top: 8px;
|
|
1097
|
+
padding: 8px 12px;
|
|
1098
|
+
background: linear-gradient(135deg, #ef4444 0%, #f97316 100%);
|
|
1099
|
+
color: #fff;
|
|
1100
|
+
border: none;
|
|
1101
|
+
border-radius: 6px;
|
|
1102
|
+
font-size: 13px;
|
|
1103
|
+
font-weight: 600;
|
|
1104
|
+
cursor: pointer;
|
|
1105
|
+
transition: opacity 0.15s, transform 0.1s;
|
|
1106
|
+
}
|
|
1107
|
+
.btn-fix:hover { opacity: 0.9; transform: translateY(-1px); }
|
|
1108
|
+
.btn-fix:active { transform: translateY(0); }
|
|
1109
|
+
|
|
1093
1110
|
.ws-error-badge {
|
|
1094
1111
|
background: #ef4444;
|
|
1095
1112
|
color: white;
|
|
@@ -1707,11 +1724,8 @@ header.hidden {
|
|
|
1707
1724
|
margin-top: 8px;
|
|
1708
1725
|
}
|
|
1709
1726
|
.ws-commit-item {
|
|
1710
|
-
display:
|
|
1711
|
-
|
|
1712
|
-
align-items: baseline;
|
|
1713
|
-
gap: 8px;
|
|
1714
|
-
padding: 4px 0;
|
|
1727
|
+
display: block;
|
|
1728
|
+
padding: 0;
|
|
1715
1729
|
font-size: 13px;
|
|
1716
1730
|
border-bottom: 1px solid var(--border);
|
|
1717
1731
|
}
|
|
@@ -1720,6 +1734,10 @@ header.hidden {
|
|
|
1720
1734
|
}
|
|
1721
1735
|
.ws-commit-msg {
|
|
1722
1736
|
color: var(--text-primary);
|
|
1737
|
+
white-space: nowrap;
|
|
1738
|
+
overflow: hidden;
|
|
1739
|
+
text-overflow: ellipsis;
|
|
1740
|
+
min-width: 0;
|
|
1723
1741
|
}
|
|
1724
1742
|
.ws-commit-date {
|
|
1725
1743
|
color: var(--text-muted);
|
|
@@ -2096,6 +2114,9 @@ header.hidden {
|
|
|
2096
2114
|
font-size: 11px;
|
|
2097
2115
|
color: #e4e8f0;
|
|
2098
2116
|
white-space: nowrap;
|
|
2117
|
+
max-width: min(320px, calc(100vw - 120px));
|
|
2118
|
+
overflow: hidden;
|
|
2119
|
+
text-overflow: ellipsis;
|
|
2099
2120
|
animation: bubble-float 3s ease-in-out infinite;
|
|
2100
2121
|
}
|
|
2101
2122
|
|
|
@@ -2169,3 +2190,60 @@ header.hidden {
|
|
|
2169
2190
|
font-size: 11px;
|
|
2170
2191
|
color: var(--text-muted);
|
|
2171
2192
|
}
|
|
2193
|
+
|
|
2194
|
+
/* Change Report */
|
|
2195
|
+
.ws-report-section { border-top: 1px solid var(--border); padding-top: 12px; }
|
|
2196
|
+
.ws-report-desc { font-size: 14px; line-height: 1.6; color: var(--text); margin-bottom: 8px; white-space: pre-line; }
|
|
2197
|
+
.ws-report-warning { display: flex; align-items: center; gap: 6px; padding: 6px 8px; margin-bottom: 4px; background: rgba(255, 180, 0, 0.08); border-radius: 6px; font-size: 13px; color: var(--text-muted); }
|
|
2198
|
+
.ws-report-warning-icon { font-size: 14px; flex-shrink: 0; }
|
|
2199
|
+
.ws-report-categories { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
|
|
2200
|
+
.ws-report-cat-group { }
|
|
2201
|
+
.ws-report-cat-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
|
2202
|
+
.ws-report-cat-label { font-size: 13px; font-weight: 600; color: var(--text); }
|
|
2203
|
+
.ws-report-cat-count { font-size: 11px; color: var(--text-muted); background: var(--surface); padding: 1px 6px; border-radius: 8px; }
|
|
2204
|
+
.ws-report-cat-items { list-style: none; margin: 0; padding: 0; }
|
|
2205
|
+
.ws-report-cat-items li { font-size: 13px; color: var(--text-muted); padding: 2px 0 2px 16px; position: relative; line-height: 1.5; }
|
|
2206
|
+
.ws-report-cat-items li::before { content: 'โข'; position: absolute; left: 4px; color: var(--text-muted); }
|
|
2207
|
+
.ws-report-nav-item { cursor: pointer; transition: color 0.15s, background 0.15s; border-radius: 4px; }
|
|
2208
|
+
.ws-report-nav-item:hover { color: var(--accent) !important; background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
2209
|
+
.ws-report-nav-hint { font-size: 11px; color: var(--accent); opacity: 0; transition: opacity 0.15s; margin-left: 4px; }
|
|
2210
|
+
.ws-report-nav-item:hover .ws-report-nav-hint { opacity: 1; }
|
|
2211
|
+
.ws-report-saved { opacity: 0.7; }
|
|
2212
|
+
.ws-report-saved-desc { font-size: 13px; color: var(--text-muted); }
|
|
2213
|
+
|
|
2214
|
+
/* Commit report (details/summary dropdown) */
|
|
2215
|
+
details.ws-commit-item { border-radius: 6px; margin-bottom: 4px; }
|
|
2216
|
+
details.ws-commit-item > summary.ws-commit-summary { display: flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; padding: 6px 8px; border-radius: 6px; flex-wrap: nowrap; }
|
|
2217
|
+
details.ws-commit-item > summary.ws-commit-summary::-webkit-details-marker { display: none; }
|
|
2218
|
+
details.ws-commit-item > summary.ws-commit-summary:hover { background: var(--surface); }
|
|
2219
|
+
.ws-commit-arrow { font-size: 10px; color: var(--text-muted); flex-shrink: 0; transition: transform 0.15s; display: inline-block; }
|
|
2220
|
+
details.ws-commit-item[open] .ws-commit-arrow { transform: rotate(90deg); }
|
|
2221
|
+
.ws-commit-report { margin: 4px 8px 8px 8px; padding: 10px 12px; background: var(--surface); border-radius: 6px; font-size: 12px; width: auto; }
|
|
2222
|
+
.ws-commit-report-loading, .ws-commit-report-empty { color: var(--text-muted); }
|
|
2223
|
+
.ws-commit-cat { margin-bottom: 6px; }
|
|
2224
|
+
.ws-commit-cat:last-child { margin-bottom: 0; }
|
|
2225
|
+
.ws-commit-cat-label { font-size: 11px; font-weight: 600; color: var(--text-muted); display: block; margin-bottom: 2px; }
|
|
2226
|
+
.ws-commit-cat-item { color: var(--text-muted); padding-left: 12px; position: relative; line-height: 1.5; }
|
|
2227
|
+
.ws-commit-cat-item::before { content: 'โข'; position: absolute; left: 3px; color: var(--text-muted); }
|
|
2228
|
+
|
|
2229
|
+
/* Split-view compare */
|
|
2230
|
+
.ws-preview-container { position: relative; flex: 1; display: flex; }
|
|
2231
|
+
.ws-preview-container .ws-preview-full { flex: 1; }
|
|
2232
|
+
.ws-preview-main { display: none; flex: 1; flex-direction: column; border-left: 2px solid var(--accent); position: relative; }
|
|
2233
|
+
.ws-preview-main.hidden { display: none !important; }
|
|
2234
|
+
.ws-split-view .ws-preview-full,
|
|
2235
|
+
.ws-split-view .ws-preview-main { flex: 1; display: flex; flex-direction: column; }
|
|
2236
|
+
.ws-preview-label { position: absolute; top: 8px; left: 8px; z-index: 10; background: var(--surface); padding: 2px 8px; border-radius: 4px; font-size: 11px; color: var(--text-muted); pointer-events: none; }
|
|
2237
|
+
.ws-preview-loading { display: flex; align-items: center; justify-content: center; flex: 1; color: var(--text-muted); font-size: 14px; }
|
|
2238
|
+
.btn-active { background: var(--accent) !important; color: white !important; }
|
|
2239
|
+
.ws-split-view .ws-preview-full { position: relative; }
|
|
2240
|
+
.ws-split-view .ws-preview-full::before {
|
|
2241
|
+
content: 'โบ ๋ด ์บ ํ';
|
|
2242
|
+
position: absolute; top: 8px; left: 8px; z-index: 10;
|
|
2243
|
+
background: var(--surface); padding: 2px 8px; border-radius: 4px;
|
|
2244
|
+
font-size: 11px; color: var(--text-muted); pointer-events: none;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
/* Ship report preview */
|
|
2248
|
+
.ship-report-preview { margin-bottom: 12px; padding: 10px; background: var(--surface); border-radius: 6px; font-size: 13px; line-height: 1.5; }
|
|
2249
|
+
.ship-report-desc { margin-bottom: 8px; }
|
package/dist/bin/sanjang.js
CHANGED
|
@@ -14,7 +14,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
14
14
|
i++;
|
|
15
15
|
}
|
|
16
16
|
if (args[i] === "--port" && args[i + 1]) {
|
|
17
|
-
port = parseInt(args[i + 1]);
|
|
17
|
+
port = parseInt(args[i + 1], 10);
|
|
18
18
|
i++;
|
|
19
19
|
}
|
|
20
20
|
if (args[i] === "--force") {
|
|
@@ -53,8 +53,8 @@ if (command === "init") {
|
|
|
53
53
|
rl.question(" ์ด๋ค ์ฑ์ ๋์ธ๊น์? [๋ฒํธ]: ", resolve);
|
|
54
54
|
});
|
|
55
55
|
rl.close();
|
|
56
|
-
const idx = parseInt(answer) - 1;
|
|
57
|
-
if (idx < 0 || idx >= apps.length || isNaN(idx)) {
|
|
56
|
+
const idx = parseInt(answer, 10) - 1;
|
|
57
|
+
if (idx < 0 || idx >= apps.length || Number.isNaN(idx)) {
|
|
58
58
|
console.error("โฐ ์๋ชป๋ ์ ํ์
๋๋ค.");
|
|
59
59
|
process.exit(1);
|
|
60
60
|
}
|
|
@@ -147,10 +147,12 @@ else {
|
|
|
147
147
|
console.log("");
|
|
148
148
|
const { createInterface } = await import("node:readline");
|
|
149
149
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
150
|
-
const answer = await new Promise((r) => {
|
|
150
|
+
const answer = await new Promise((r) => {
|
|
151
|
+
rl.question(" ์ด๋ค ์ฑ์ ๋์ธ๊น์? [๋ฒํธ]: ", r);
|
|
152
|
+
});
|
|
151
153
|
rl.close();
|
|
152
|
-
const idx = parseInt(answer) - 1;
|
|
153
|
-
if (idx < 0 || idx >= apps.length || isNaN(idx)) {
|
|
154
|
+
const idx = parseInt(answer, 10) - 1;
|
|
155
|
+
if (idx < 0 || idx >= apps.length || Number.isNaN(idx)) {
|
|
154
156
|
console.error("โฐ ์๋ชป๋ ์ ํ์
๋๋ค.");
|
|
155
157
|
process.exit(1);
|
|
156
158
|
}
|
package/dist/lib/config.js
CHANGED
|
@@ -230,13 +230,11 @@ function detectTurboMainApp(root) {
|
|
|
230
230
|
const port = portMatch?.[1] ? parseInt(portMatch[1], 10) : 3000;
|
|
231
231
|
candidates.push({ name: entry.name, port });
|
|
232
232
|
}
|
|
233
|
-
catch {
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
233
|
+
catch { }
|
|
236
234
|
}
|
|
237
235
|
}
|
|
238
236
|
// Prefer app with explicit port, then first candidate
|
|
239
|
-
return candidates.find(c => c.port !== 3000) ?? candidates[0] ?? null;
|
|
237
|
+
return candidates.find((c) => c.port !== 3000) ?? candidates[0] ?? null;
|
|
240
238
|
}
|
|
241
239
|
function detectPackageManager(root) {
|
|
242
240
|
if (existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock")))
|
|
@@ -272,7 +270,7 @@ export function generateConfig(projectRoot, options = {}) {
|
|
|
272
270
|
// Exclude .env.example, .env.test, .env.template
|
|
273
271
|
(f) => !f.includes("example") && !f.includes("template") && !f.includes(".test"));
|
|
274
272
|
// Detect potential issues
|
|
275
|
-
const
|
|
273
|
+
const _issues = detectSetupIssues(detectRoot);
|
|
276
274
|
const lines = [
|
|
277
275
|
"export default {",
|
|
278
276
|
` // ${detected.framework} detected`,
|
|
@@ -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", [
|
|
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 ?? "")
|
|
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;
|
package/dist/lib/engine/pr.js
CHANGED
|
@@ -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
|
-
|
|
30
|
+
`The author described the change as: "${message}"`,
|
|
31
31
|
"",
|
|
32
32
|
"Here is the diff stat:",
|
|
33
33
|
diffStat || "(no stat)",
|