sanjang 0.3.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.
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/bin/__tests__/sanjang.test.ts +42 -0
- package/bin/sanjang.js +17 -0
- package/bin/sanjang.ts +144 -0
- package/dashboard/app.js +1888 -0
- package/dashboard/app.test.js +2 -0
- package/dashboard/index.html +275 -0
- package/dashboard/style.css +2112 -0
- package/lib/config.ts +337 -0
- package/lib/engine/cache.ts +218 -0
- package/lib/engine/config-hotfix.ts +161 -0
- package/lib/engine/conflict.ts +33 -0
- package/lib/engine/diagnostics.ts +81 -0
- package/lib/engine/naming.ts +93 -0
- package/lib/engine/ports.ts +61 -0
- package/lib/engine/pr.ts +71 -0
- package/lib/engine/process.ts +283 -0
- package/lib/engine/self-heal.ts +130 -0
- package/lib/engine/smart-init.ts +136 -0
- package/lib/engine/smart-pr.ts +130 -0
- package/lib/engine/snapshot.ts +45 -0
- package/lib/engine/state.ts +60 -0
- package/lib/engine/suggest.ts +169 -0
- package/lib/engine/warp.ts +47 -0
- package/lib/engine/watcher.ts +40 -0
- package/lib/engine/worktree.ts +100 -0
- package/lib/server.ts +1560 -0
- package/lib/types.ts +130 -0
- package/package.json +48 -0
- package/templates/sanjang.config.js +32 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ko">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>산장</title>
|
|
7
|
+
<script>fetch('/api/project').then(r=>r.json()).then(d=>{document.getElementById('project-name').textContent='— '+d.name;document.title='산장 — '+d.name})</script>
|
|
8
|
+
<link rel="stylesheet" href="style.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
|
|
12
|
+
<!-- Header -->
|
|
13
|
+
<header>
|
|
14
|
+
<h1>산장 <span id="project-name" style="font-weight:400;color:var(--text-muted);font-size:0.6em"></span></h1>
|
|
15
|
+
<button class="btn btn-primary" onclick="document.getElementById('quickstart-input').focus();document.getElementById('quickstart-input').scrollIntoView({behavior:'smooth'})">+ 새 캠프</button>
|
|
16
|
+
</header>
|
|
17
|
+
|
|
18
|
+
<!-- Portal Home -->
|
|
19
|
+
<div id="portal">
|
|
20
|
+
<!-- 새로 시작 (핵심 액션, 맨 위) -->
|
|
21
|
+
<div class="portal-section">
|
|
22
|
+
<div class="portal-quickstart">
|
|
23
|
+
<input class="form-input portal-quickstart-input" id="quickstart-input"
|
|
24
|
+
type="text" placeholder="뭘 하고 싶어? (예: 로그인 버튼 색상 변경)"
|
|
25
|
+
autocomplete="off" spellcheck="false"
|
|
26
|
+
onkeydown="if(event.key==='Enter')quickStart()">
|
|
27
|
+
<button class="btn btn-primary" onclick="quickStart()">시작</button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- 이어하기 (최대 3개) -->
|
|
32
|
+
<div class="portal-section" id="portal-work-section" style="display:none">
|
|
33
|
+
<h2 class="portal-section-title">이어하기</h2>
|
|
34
|
+
<div id="portal-work" class="portal-work-list"></div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- 추천 작업 (최대 5개, 이어하기와 중복 제거) -->
|
|
38
|
+
<div class="portal-section" id="portal-suggestions-section" style="display:none">
|
|
39
|
+
<h2 class="portal-section-title">이런 걸 해볼 수 있어요</h2>
|
|
40
|
+
<div id="portal-suggestions" class="portal-work-list"></div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- 기존 캠프가 있으면 아래에 카드 그리드 표시 -->
|
|
44
|
+
<div id="portal-camps-section" class="portal-section hidden">
|
|
45
|
+
<h2 class="portal-section-title">캠프</h2>
|
|
46
|
+
<div class="grid" id="grid"></div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Workspace View (full-screen preview + slide panel) -->
|
|
51
|
+
<div id="workspace" class="workspace hidden">
|
|
52
|
+
<!-- Top bar — minimal, over the preview -->
|
|
53
|
+
<div class="ws-topbar">
|
|
54
|
+
<button class="btn btn-ghost btn-sm" onclick="exitWorkspace()">← 목록</button>
|
|
55
|
+
<span class="workspace-title" id="ws-title"></span>
|
|
56
|
+
<span class="workspace-status" id="ws-status"></span>
|
|
57
|
+
<div class="ws-mini-char" id="ws-mini-char"></div>
|
|
58
|
+
<div class="ws-quest" id="ws-quest">
|
|
59
|
+
<div class="ws-quest-step ws-quest-active" id="ws-step-work"><div class="ws-quest-dot"></div><span>작업</span></div>
|
|
60
|
+
<div class="ws-quest-line"></div>
|
|
61
|
+
<div class="ws-quest-step" id="ws-step-save"><div class="ws-quest-dot"></div><span>세이브</span></div>
|
|
62
|
+
<div class="ws-quest-line"></div>
|
|
63
|
+
<div class="ws-quest-step" id="ws-step-ship"><div class="ws-quest-dot"></div><span>보내기</span></div>
|
|
64
|
+
</div>
|
|
65
|
+
<div style="flex:1"></div>
|
|
66
|
+
<button class="btn btn-sub btn-sm" id="ws-terminal-btn" onclick="wsOpenTerminal()">💻 터미널</button>
|
|
67
|
+
<button class="btn btn-sub btn-sm" onclick="togglePanel()">⛰ 패널</button>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Preview — full screen -->
|
|
71
|
+
<div class="ws-preview-full" id="ws-preview"></div>
|
|
72
|
+
|
|
73
|
+
<!-- Slide panel — right side -->
|
|
74
|
+
<div class="ws-panel" id="ws-panel">
|
|
75
|
+
<div class="ws-panel-content">
|
|
76
|
+
<div class="workspace-section ws-unsaved-section" id="ws-unsaved-section">
|
|
77
|
+
<div id="ws-changes-summary-text" class="ws-summary-text"></div>
|
|
78
|
+
<div class="ws-save-row">
|
|
79
|
+
<button class="btn btn-save" id="ws-save-btn" onclick="wsSave()">💾 세이브하기</button>
|
|
80
|
+
<label class="ws-autosave-toggle" title="5분간 변경 없으면 자동 세이브">
|
|
81
|
+
<input type="checkbox" id="ws-autosave-check" onchange="toggleAutosave(this.checked)">
|
|
82
|
+
<span class="ws-autosave-label">자동</span>
|
|
83
|
+
</label>
|
|
84
|
+
</div>
|
|
85
|
+
<details class="ws-file-details">
|
|
86
|
+
<summary>파일 보기</summary>
|
|
87
|
+
<div id="ws-changes"></div>
|
|
88
|
+
</details>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="workspace-section">
|
|
91
|
+
<h3>📜 세이브 기록</h3>
|
|
92
|
+
<div id="ws-actions"></div>
|
|
93
|
+
</div>
|
|
94
|
+
<details class="workspace-section ws-log-details">
|
|
95
|
+
<summary>🔴 브라우저 에러 <span class="ws-error-badge" id="ws-browser-error-badge" style="display:none">0</span></summary>
|
|
96
|
+
<div class="ws-browser-error-panel" id="ws-browser-errors">
|
|
97
|
+
<span style="color:var(--text-muted);font-size:12px">에러 없음</span>
|
|
98
|
+
</div>
|
|
99
|
+
</details>
|
|
100
|
+
<details class="workspace-section ws-log-details">
|
|
101
|
+
<summary>📜 로그</summary>
|
|
102
|
+
<div class="ws-log-panel" id="ws-log"><pre></pre></div>
|
|
103
|
+
</details>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="ws-panel-actions">
|
|
106
|
+
<div class="workspace-btn-row">
|
|
107
|
+
<button class="btn btn-accent btn-sm" id="ws-ship-btn" onclick="wsShip()">팀에 보내기</button>
|
|
108
|
+
<button class="btn btn-sub btn-sm" onclick="wsSnap()">📸</button>
|
|
109
|
+
<button class="btn btn-sub btn-sm" onclick="wsSync()">⬇️</button>
|
|
110
|
+
<button class="btn btn-sub btn-sm" onclick="wsReset()">🔄</button>
|
|
111
|
+
<button class="btn btn-danger-ghost btn-sm" onclick="wsDelete()">삭제</button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- New Camp Modal -->
|
|
118
|
+
<div class="modal-backdrop" id="new-pg-modal">
|
|
119
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="new-pg-modal-title">
|
|
120
|
+
<h2 id="new-pg-modal-title">새 캠프</h2>
|
|
121
|
+
|
|
122
|
+
<div class="form-group">
|
|
123
|
+
<label class="form-label" for="new-pg-name">이름</label>
|
|
124
|
+
<input
|
|
125
|
+
class="form-input"
|
|
126
|
+
id="new-pg-name"
|
|
127
|
+
type="text"
|
|
128
|
+
placeholder="예: my-feature"
|
|
129
|
+
autocomplete="off"
|
|
130
|
+
spellcheck="false"
|
|
131
|
+
>
|
|
132
|
+
<span class="form-hint">소문자, 숫자, 하이픈만 사용 가능</span>
|
|
133
|
+
<span id="new-pg-name-error" style="color: var(--status-error-fg); font-size: 12px;"></span>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div class="form-group">
|
|
137
|
+
<label class="form-label" for="new-pg-branch">브랜치</label>
|
|
138
|
+
<div class="branch-picker" id="branch-picker">
|
|
139
|
+
<input
|
|
140
|
+
class="form-input"
|
|
141
|
+
id="new-pg-branch"
|
|
142
|
+
type="text"
|
|
143
|
+
placeholder="브랜치 이름 검색..."
|
|
144
|
+
autocomplete="off"
|
|
145
|
+
spellcheck="false"
|
|
146
|
+
>
|
|
147
|
+
<div class="branch-dropdown" id="branch-dropdown"></div>
|
|
148
|
+
</div>
|
|
149
|
+
<span class="form-hint" id="branch-count"></span>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div class="modal-actions">
|
|
153
|
+
<button class="btn btn-ghost" onclick="closeNewModal()">취소</button>
|
|
154
|
+
<button class="btn btn-primary" id="create-pg-btn" onclick="createPg()">생성</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<!-- Snapshot Modal -->
|
|
160
|
+
<div class="modal-backdrop" id="snap-modal">
|
|
161
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="snap-modal-title">
|
|
162
|
+
<h2 id="snap-modal-title">스냅샷</h2>
|
|
163
|
+
|
|
164
|
+
<div class="form-group">
|
|
165
|
+
<label class="form-label" for="snap-label-input">새 스냅샷 저장</label>
|
|
166
|
+
<div class="snap-save-row">
|
|
167
|
+
<input
|
|
168
|
+
class="form-input"
|
|
169
|
+
id="snap-label-input"
|
|
170
|
+
type="text"
|
|
171
|
+
placeholder="이름 (선택사항)"
|
|
172
|
+
autocomplete="off"
|
|
173
|
+
>
|
|
174
|
+
<button class="btn btn-primary btn-sm" onclick="saveSnap()">저장</button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div class="form-group">
|
|
179
|
+
<span class="form-label">저장된 스냅샷</span>
|
|
180
|
+
<div class="snap-list" id="snap-list">
|
|
181
|
+
<div class="snap-empty">스냅샷이 없습니다</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div class="modal-actions">
|
|
186
|
+
<button class="btn btn-ghost" onclick="closeSnapModal()">닫기</button>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<!-- Changes Modal -->
|
|
192
|
+
<div class="modal-backdrop" id="changes-modal">
|
|
193
|
+
<div class="modal" role="dialog" aria-modal="true">
|
|
194
|
+
<h2 id="changes-modal-title">변경 내역</h2>
|
|
195
|
+
<div class="changes-file-list" id="changes-file-list"></div>
|
|
196
|
+
<div class="modal-actions">
|
|
197
|
+
<button class="btn btn-ghost" onclick="closeChangesModal()">닫기</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<!-- Ship Modal -->
|
|
203
|
+
<div class="modal-backdrop" id="ship-modal">
|
|
204
|
+
<div class="modal" role="dialog" aria-modal="true">
|
|
205
|
+
<h2>팀에 보내기</h2>
|
|
206
|
+
<p id="ship-file-count" style="font-size:13px;color:var(--text-muted);margin-bottom:12px"></p>
|
|
207
|
+
|
|
208
|
+
<div class="form-group">
|
|
209
|
+
<label class="form-label" for="ship-message">뭘 바꿨나요? (한 줄로)</label>
|
|
210
|
+
<input
|
|
211
|
+
class="form-input"
|
|
212
|
+
id="ship-message"
|
|
213
|
+
type="text"
|
|
214
|
+
placeholder="예: 대시보드 타이틀을 스페인어로 변경"
|
|
215
|
+
autocomplete="off"
|
|
216
|
+
>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<!-- Ship progress steps -->
|
|
220
|
+
<div id="ship-steps" class="ship-steps hidden">
|
|
221
|
+
<div class="ship-step" id="ship-step-test">🧪 테스트 확인 중...</div>
|
|
222
|
+
<div class="ship-step" id="ship-step-push">📦 코드 정리 + 올리기</div>
|
|
223
|
+
<div class="ship-step" id="ship-step-pr">📋 PR 생성 중...</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div class="modal-actions">
|
|
227
|
+
<button class="btn btn-ghost" onclick="closeShipModal()">취소</button>
|
|
228
|
+
<button class="btn btn-primary" id="ship-btn" onclick="shipPg()">보내기</button>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<!-- Ship Result (shown after successful ship) -->
|
|
234
|
+
<div class="modal-backdrop" id="ship-result-modal">
|
|
235
|
+
<div class="modal" role="dialog" aria-modal="true">
|
|
236
|
+
<h2>보내기 완료!</h2>
|
|
237
|
+
<div id="ship-result-content"></div>
|
|
238
|
+
<div class="modal-actions">
|
|
239
|
+
<button class="btn btn-primary" onclick="closeShipResultModal()">확인</button>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<!-- Conflict Modal -->
|
|
245
|
+
<div class="modal-backdrop" id="conflict-modal">
|
|
246
|
+
<div class="modal" role="dialog" aria-modal="true">
|
|
247
|
+
<h2>충돌이 발생했어요</h2>
|
|
248
|
+
<p style="font-size:13px;color:var(--text-muted);margin-bottom:12px">
|
|
249
|
+
팀의 변경사항과 내 변경사항이 같은 부분을 수정해서 충돌이 생겼어요.
|
|
250
|
+
</p>
|
|
251
|
+
<div id="conflict-files" style="margin-bottom:16px"></div>
|
|
252
|
+
<p style="font-size:13px;margin-bottom:16px">어떻게 할까요?</p>
|
|
253
|
+
<div style="display:flex;flex-direction:column;gap:8px">
|
|
254
|
+
<button class="btn btn-primary" onclick="resolveConflict('claude')">
|
|
255
|
+
Claude에게 맡기기 (추천)
|
|
256
|
+
</button>
|
|
257
|
+
<button class="btn btn-ghost" onclick="resolveConflict('ours')">
|
|
258
|
+
내 것 유지하기
|
|
259
|
+
</button>
|
|
260
|
+
<button class="btn btn-ghost" onclick="resolveConflict('theirs')">
|
|
261
|
+
팀 것으로 덮기
|
|
262
|
+
</button>
|
|
263
|
+
<button class="btn btn-ghost" onclick="resolveAbort()" style="color:var(--text-muted)">
|
|
264
|
+
취소 (원래대로 되돌리기)
|
|
265
|
+
</button>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<!-- Toast container -->
|
|
271
|
+
<div id="toast-container"></div>
|
|
272
|
+
|
|
273
|
+
<script src="app.js"></script>
|
|
274
|
+
</body>
|
|
275
|
+
</html>
|