sanjang 0.3.4 → 0.3.6
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 +941 -38
- package/dashboard/index.html +151 -38
- package/dashboard/style.css +374 -7
- package/dist/bin/sanjang.js +152 -7
- package/dist/lib/config.d.ts +5 -0
- package/dist/lib/config.js +29 -5
- package/dist/lib/engine/change-report.d.ts +27 -0
- package/dist/lib/engine/change-report.js +233 -0
- package/dist/lib/engine/conflict.d.ts +13 -0
- package/dist/lib/engine/conflict.js +41 -0
- package/dist/lib/engine/diagnostics.js +2 -6
- package/dist/lib/engine/main-server.d.ts +19 -0
- package/dist/lib/engine/main-server.js +181 -0
- package/dist/lib/engine/naming.js +11 -2
- package/dist/lib/engine/ports.d.ts +2 -2
- package/dist/lib/engine/ports.js +33 -23
- package/dist/lib/engine/pr.js +1 -1
- package/dist/lib/engine/process-utils.d.ts +11 -0
- package/dist/lib/engine/process-utils.js +65 -0
- package/dist/lib/engine/process.d.ts +2 -0
- package/dist/lib/engine/process.js +27 -39
- package/dist/lib/engine/self-heal.js +16 -5
- package/dist/lib/engine/smart-init.js +7 -6
- package/dist/lib/engine/state.js +14 -5
- 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/engine/worktree.d.ts +2 -0
- package/dist/lib/engine/worktree.js +30 -8
- package/dist/lib/server.d.ts +1 -0
- package/dist/lib/server.js +701 -94
- package/dist/lib/types.d.ts +25 -0
- package/package.json +2 -2
package/dashboard/style.css
CHANGED
|
@@ -7,6 +7,18 @@
|
|
|
7
7
|
|
|
8
8
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@400;500;600;700&display=swap');
|
|
9
9
|
|
|
10
|
+
.sr-only {
|
|
11
|
+
position: absolute;
|
|
12
|
+
width: 1px;
|
|
13
|
+
height: 1px;
|
|
14
|
+
padding: 0;
|
|
15
|
+
margin: -1px;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
clip: rect(0,0,0,0);
|
|
18
|
+
white-space: nowrap;
|
|
19
|
+
border: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
*, *::before, *::after {
|
|
11
23
|
box-sizing: border-box;
|
|
12
24
|
margin: 0;
|
|
@@ -684,6 +696,35 @@ header h1::before {
|
|
|
684
696
|
letter-spacing: -0.01em;
|
|
685
697
|
}
|
|
686
698
|
|
|
699
|
+
.new-camp-tabs {
|
|
700
|
+
display: flex;
|
|
701
|
+
gap: 0;
|
|
702
|
+
border-bottom: 1px solid var(--border);
|
|
703
|
+
margin: -8px 0 0;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.new-camp-tab {
|
|
707
|
+
flex: 1;
|
|
708
|
+
padding: 8px 12px;
|
|
709
|
+
background: none;
|
|
710
|
+
border: none;
|
|
711
|
+
border-bottom: 2px solid transparent;
|
|
712
|
+
color: var(--text-muted);
|
|
713
|
+
font-size: 13px;
|
|
714
|
+
cursor: pointer;
|
|
715
|
+
transition: color 0.15s, border-color 0.15s;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.new-camp-tab:hover {
|
|
719
|
+
color: var(--text-primary);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.new-camp-tab.active {
|
|
723
|
+
color: var(--text-primary);
|
|
724
|
+
border-bottom-color: var(--accent);
|
|
725
|
+
font-weight: 500;
|
|
726
|
+
}
|
|
727
|
+
|
|
687
728
|
/* Form elements */
|
|
688
729
|
.form-group {
|
|
689
730
|
display: flex;
|
|
@@ -968,6 +1009,49 @@ header h1::before {
|
|
|
968
1009
|
font-weight: 600;
|
|
969
1010
|
}
|
|
970
1011
|
|
|
1012
|
+
/* Camp switcher dropdown */
|
|
1013
|
+
.ws-camp-switcher { position: relative; }
|
|
1014
|
+
.ws-camp-switch-btn {
|
|
1015
|
+
background: none;
|
|
1016
|
+
border: 1px solid transparent;
|
|
1017
|
+
border-radius: 6px;
|
|
1018
|
+
padding: 4px 8px;
|
|
1019
|
+
font-size: 14px;
|
|
1020
|
+
font-weight: 600;
|
|
1021
|
+
color: var(--text);
|
|
1022
|
+
cursor: pointer;
|
|
1023
|
+
}
|
|
1024
|
+
.ws-camp-switch-btn:hover { border-color: var(--border-subtle); background: var(--surface); }
|
|
1025
|
+
.ws-camp-switch-btn::after { content: ' ▾'; font-size: 10px; color: var(--text-muted); }
|
|
1026
|
+
.ws-camp-dropdown {
|
|
1027
|
+
display: none;
|
|
1028
|
+
position: absolute;
|
|
1029
|
+
top: 100%;
|
|
1030
|
+
left: 0;
|
|
1031
|
+
z-index: 100;
|
|
1032
|
+
min-width: 220px;
|
|
1033
|
+
background: var(--bg);
|
|
1034
|
+
border: 1px solid var(--border-subtle);
|
|
1035
|
+
border-radius: 8px;
|
|
1036
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
|
1037
|
+
padding: 4px;
|
|
1038
|
+
margin-top: 4px;
|
|
1039
|
+
}
|
|
1040
|
+
.ws-camp-dropdown.open { display: block; }
|
|
1041
|
+
.ws-camp-dd-item {
|
|
1042
|
+
display: flex;
|
|
1043
|
+
align-items: center;
|
|
1044
|
+
gap: 8px;
|
|
1045
|
+
padding: 8px 10px;
|
|
1046
|
+
border-radius: 6px;
|
|
1047
|
+
cursor: pointer;
|
|
1048
|
+
font-size: 13px;
|
|
1049
|
+
}
|
|
1050
|
+
.ws-camp-dd-item:hover { background: var(--surface); }
|
|
1051
|
+
.ws-camp-dd-name { font-weight: 500; color: var(--text); }
|
|
1052
|
+
.ws-camp-dd-branch { color: var(--text-muted); font-size: 11px; margin-left: auto; }
|
|
1053
|
+
.ws-camp-dd-empty { padding: 12px; color: var(--text-muted); font-size: 12px; text-align: center; }
|
|
1054
|
+
|
|
971
1055
|
/* Preview — full screen behind topbar */
|
|
972
1056
|
.ws-preview-full {
|
|
973
1057
|
position: absolute;
|
|
@@ -1074,6 +1158,7 @@ header h1::before {
|
|
|
1074
1158
|
padding: 3px 0;
|
|
1075
1159
|
border-bottom: 1px solid var(--border-subtle);
|
|
1076
1160
|
display: flex;
|
|
1161
|
+
flex-wrap: wrap;
|
|
1077
1162
|
gap: 6px;
|
|
1078
1163
|
align-items: baseline;
|
|
1079
1164
|
}
|
|
@@ -1090,13 +1175,113 @@ header h1::before {
|
|
|
1090
1175
|
word-break: break-all;
|
|
1091
1176
|
}
|
|
1092
1177
|
|
|
1093
|
-
.ws-error-
|
|
1178
|
+
.ws-error-stack {
|
|
1179
|
+
width: 100%;
|
|
1180
|
+
margin-top: 2px;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.ws-error-stack summary {
|
|
1184
|
+
cursor: pointer;
|
|
1185
|
+
color: var(--text-muted);
|
|
1186
|
+
font-size: 10px;
|
|
1187
|
+
user-select: none;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
.ws-error-stack pre {
|
|
1191
|
+
margin: 4px 0 0;
|
|
1192
|
+
padding: 6px 8px;
|
|
1193
|
+
background: rgba(0,0,0,0.3);
|
|
1194
|
+
border-radius: 4px;
|
|
1195
|
+
font-size: 10px;
|
|
1196
|
+
line-height: 1.5;
|
|
1197
|
+
color: var(--text-secondary);
|
|
1198
|
+
white-space: pre-wrap;
|
|
1199
|
+
word-break: break-all;
|
|
1200
|
+
max-height: 150px;
|
|
1201
|
+
overflow-y: auto;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
.btn-fix {
|
|
1205
|
+
display: block;
|
|
1206
|
+
width: 100%;
|
|
1207
|
+
margin-top: 8px;
|
|
1208
|
+
padding: 8px 12px;
|
|
1209
|
+
background: linear-gradient(135deg, #ef4444 0%, #f97316 100%);
|
|
1210
|
+
color: #fff;
|
|
1211
|
+
border: none;
|
|
1212
|
+
border-radius: 6px;
|
|
1213
|
+
font-size: 13px;
|
|
1214
|
+
font-weight: 600;
|
|
1215
|
+
cursor: pointer;
|
|
1216
|
+
transition: opacity 0.15s, transform 0.1s;
|
|
1217
|
+
}
|
|
1218
|
+
.btn-fix:hover { opacity: 0.9; transform: translateY(-1px); }
|
|
1219
|
+
.btn-fix:active { transform: translateY(0); }
|
|
1220
|
+
|
|
1221
|
+
.ws-error-badge, .ws-devtab-badge {
|
|
1094
1222
|
background: #ef4444;
|
|
1095
1223
|
color: white;
|
|
1096
1224
|
font-size: 10px;
|
|
1097
1225
|
padding: 1px 6px;
|
|
1098
1226
|
border-radius: 10px;
|
|
1099
|
-
margin-left:
|
|
1227
|
+
margin-left: 4px;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/* Devtools tabs */
|
|
1231
|
+
.ws-devtabs {
|
|
1232
|
+
display: flex;
|
|
1233
|
+
gap: 0;
|
|
1234
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
1235
|
+
margin-bottom: 8px;
|
|
1236
|
+
}
|
|
1237
|
+
.ws-devtab-btn {
|
|
1238
|
+
background: none;
|
|
1239
|
+
border: none;
|
|
1240
|
+
border-bottom: 2px solid transparent;
|
|
1241
|
+
padding: 6px 10px;
|
|
1242
|
+
font-size: 12px;
|
|
1243
|
+
color: var(--text-muted);
|
|
1244
|
+
cursor: pointer;
|
|
1245
|
+
white-space: nowrap;
|
|
1246
|
+
}
|
|
1247
|
+
.ws-devtab-btn:hover { color: var(--text); }
|
|
1248
|
+
.ws-devtab-btn.ws-devtab-active { color: var(--text); border-bottom-color: var(--accent); }
|
|
1249
|
+
.ws-devtab-panel { max-height: 300px; overflow-y: auto; }
|
|
1250
|
+
|
|
1251
|
+
/* Console panel */
|
|
1252
|
+
.ws-console-panel { padding: 4px 8px; font-size: 11px; font-family: var(--font-mono); }
|
|
1253
|
+
.ws-console-item { padding: 2px 0; border-bottom: 1px solid var(--border-subtle); word-break: break-all; }
|
|
1254
|
+
.ws-console-log { color: var(--text); }
|
|
1255
|
+
.ws-console-warn { color: #f59e0b; }
|
|
1256
|
+
.ws-console-info { color: #3b82f6; }
|
|
1257
|
+
|
|
1258
|
+
/* Network panel */
|
|
1259
|
+
.ws-network-panel { padding: 4px 8px; font-size: 11px; font-family: var(--font-mono); }
|
|
1260
|
+
.ws-net-item { display: flex; align-items: center; gap: 6px; padding: 3px 0; border-bottom: 1px solid var(--border-subtle); }
|
|
1261
|
+
.ws-net-method { font-weight: 600; font-size: 10px; padding: 1px 4px; border-radius: 3px; flex-shrink: 0; }
|
|
1262
|
+
.ws-net-method-get { color: #22c55e; }
|
|
1263
|
+
.ws-net-method-post { color: #3b82f6; }
|
|
1264
|
+
.ws-net-method-put { color: #f59e0b; }
|
|
1265
|
+
.ws-net-method-delete { color: #ef4444; }
|
|
1266
|
+
.ws-net-method-patch { color: #a855f7; }
|
|
1267
|
+
.ws-net-url { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
|
|
1268
|
+
.ws-net-status { font-weight: 600; flex-shrink: 0; }
|
|
1269
|
+
.ws-net-ok { color: #22c55e; }
|
|
1270
|
+
.ws-net-warn { color: #f59e0b; }
|
|
1271
|
+
.ws-net-err { color: #ef4444; }
|
|
1272
|
+
.ws-net-dur { color: var(--text-muted); flex-shrink: 0; }
|
|
1273
|
+
|
|
1274
|
+
/* Test runner panel */
|
|
1275
|
+
.ws-test-panel { padding: 4px 8px; font-size: 12px; }
|
|
1276
|
+
.ws-test-status { padding: 8px; font-weight: 600; border-radius: 6px; margin-bottom: 8px; }
|
|
1277
|
+
.ws-test-running { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); }
|
|
1278
|
+
.ws-test-pass { background: #dcfce7; color: #166534; }
|
|
1279
|
+
.ws-test-fail { background: #fef2f2; color: #991b1b; }
|
|
1280
|
+
.ws-test-output { max-height: 250px; overflow: auto; font-family: var(--font-mono); font-size: 11px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; padding: 8px; background: var(--surface); border-radius: 6px; }
|
|
1281
|
+
.ws-test-badge { font-size: 10px; }
|
|
1282
|
+
@media (prefers-color-scheme: dark) {
|
|
1283
|
+
.ws-test-pass { background: #052e16; color: #86efac; }
|
|
1284
|
+
.ws-test-fail { background: #450a0a; color: #fca5a5; }
|
|
1100
1285
|
}
|
|
1101
1286
|
|
|
1102
1287
|
.ws-file-item {
|
|
@@ -1106,6 +1291,62 @@ header h1::before {
|
|
|
1106
1291
|
padding: 3px 0;
|
|
1107
1292
|
font-size: 13px;
|
|
1108
1293
|
}
|
|
1294
|
+
/* Stale camp banner + cleanup */
|
|
1295
|
+
.portal-stale-banner {
|
|
1296
|
+
display: flex;
|
|
1297
|
+
align-items: center;
|
|
1298
|
+
justify-content: space-between;
|
|
1299
|
+
padding: 10px 16px;
|
|
1300
|
+
margin: 0 16px;
|
|
1301
|
+
background: color-mix(in srgb, #f59e0b 12%, transparent);
|
|
1302
|
+
border: 1px solid color-mix(in srgb, #f59e0b 30%, transparent);
|
|
1303
|
+
border-radius: 8px;
|
|
1304
|
+
font-size: 13px;
|
|
1305
|
+
color: var(--text);
|
|
1306
|
+
}
|
|
1307
|
+
.ws-stale-item {
|
|
1308
|
+
display: flex;
|
|
1309
|
+
align-items: center;
|
|
1310
|
+
gap: 8px;
|
|
1311
|
+
padding: 8px 0;
|
|
1312
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
1313
|
+
font-size: 13px;
|
|
1314
|
+
cursor: pointer;
|
|
1315
|
+
}
|
|
1316
|
+
.ws-stale-item:last-child { border-bottom: none; }
|
|
1317
|
+
.ws-stale-item input { flex-shrink: 0; }
|
|
1318
|
+
|
|
1319
|
+
/* Conflict resolution */
|
|
1320
|
+
.conflict-file { border: 1px solid var(--border-subtle); border-radius: 8px; margin-bottom: 8px; overflow: hidden; transition: opacity 0.3s; }
|
|
1321
|
+
.conflict-file-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: var(--surface); }
|
|
1322
|
+
.conflict-file-header code { font-size: 12px; }
|
|
1323
|
+
.conflict-file-actions { display: flex; gap: 4px; }
|
|
1324
|
+
.conflict-section { display: flex; gap: 0; }
|
|
1325
|
+
.conflict-side { flex: 1; padding: 8px; font-size: 11px; overflow: auto; }
|
|
1326
|
+
.conflict-side pre { margin: 0; white-space: pre-wrap; word-break: break-all; font-family: var(--font-mono); font-size: 11px; }
|
|
1327
|
+
.conflict-side-label { font-size: 10px; font-weight: 600; margin-bottom: 4px; }
|
|
1328
|
+
.conflict-ours { background: color-mix(in srgb, #3b82f6 10%, transparent); border-right: 1px solid var(--border-subtle); }
|
|
1329
|
+
.conflict-ours .conflict-side-label { color: #3b82f6; }
|
|
1330
|
+
.conflict-theirs { background: color-mix(in srgb, #f59e0b 10%, transparent); }
|
|
1331
|
+
.conflict-theirs .conflict-side-label { color: #f59e0b; }
|
|
1332
|
+
|
|
1333
|
+
.ws-file-clickable { cursor: pointer; border-radius: 4px; padding: 3px 4px; }
|
|
1334
|
+
.ws-file-clickable:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
1335
|
+
|
|
1336
|
+
/* Diff modal */
|
|
1337
|
+
.modal-wide { max-width: 720px; width: 90vw; }
|
|
1338
|
+
.diff-content { max-height: 60vh; overflow: auto; }
|
|
1339
|
+
.diff-header { font-size: 13px; font-weight: 600; color: var(--text-muted); padding: 8px 0 4px; }
|
|
1340
|
+
.diff-pre { margin: 0; font-size: 12px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; }
|
|
1341
|
+
.diff-line { padding: 0 8px; }
|
|
1342
|
+
.diff-line-add { background: #dcfce7; color: #166534; }
|
|
1343
|
+
.diff-line-del { background: #fef2f2; color: #991b1b; }
|
|
1344
|
+
.diff-line-hunk { background: var(--surface); color: var(--accent); font-weight: 600; margin-top: 8px; }
|
|
1345
|
+
.diff-line-meta { color: var(--text-muted); font-weight: 600; }
|
|
1346
|
+
@media (prefers-color-scheme: dark) {
|
|
1347
|
+
.diff-line-add { background: #052e16; color: #86efac; }
|
|
1348
|
+
.diff-line-del { background: #450a0a; color: #fca5a5; }
|
|
1349
|
+
}
|
|
1109
1350
|
|
|
1110
1351
|
.ws-action-item {
|
|
1111
1352
|
font-size: 13px;
|
|
@@ -1707,11 +1948,8 @@ header.hidden {
|
|
|
1707
1948
|
margin-top: 8px;
|
|
1708
1949
|
}
|
|
1709
1950
|
.ws-commit-item {
|
|
1710
|
-
display:
|
|
1711
|
-
|
|
1712
|
-
align-items: baseline;
|
|
1713
|
-
gap: 8px;
|
|
1714
|
-
padding: 4px 0;
|
|
1951
|
+
display: block;
|
|
1952
|
+
padding: 0;
|
|
1715
1953
|
font-size: 13px;
|
|
1716
1954
|
border-bottom: 1px solid var(--border);
|
|
1717
1955
|
}
|
|
@@ -1720,6 +1958,10 @@ header.hidden {
|
|
|
1720
1958
|
}
|
|
1721
1959
|
.ws-commit-msg {
|
|
1722
1960
|
color: var(--text-primary);
|
|
1961
|
+
white-space: nowrap;
|
|
1962
|
+
overflow: hidden;
|
|
1963
|
+
text-overflow: ellipsis;
|
|
1964
|
+
min-width: 0;
|
|
1723
1965
|
}
|
|
1724
1966
|
.ws-commit-date {
|
|
1725
1967
|
color: var(--text-muted);
|
|
@@ -2096,6 +2338,9 @@ header.hidden {
|
|
|
2096
2338
|
font-size: 11px;
|
|
2097
2339
|
color: #e4e8f0;
|
|
2098
2340
|
white-space: nowrap;
|
|
2341
|
+
max-width: min(320px, calc(100vw - 120px));
|
|
2342
|
+
overflow: hidden;
|
|
2343
|
+
text-overflow: ellipsis;
|
|
2099
2344
|
animation: bubble-float 3s ease-in-out infinite;
|
|
2100
2345
|
}
|
|
2101
2346
|
|
|
@@ -2169,3 +2414,125 @@ header.hidden {
|
|
|
2169
2414
|
font-size: 11px;
|
|
2170
2415
|
color: var(--text-muted);
|
|
2171
2416
|
}
|
|
2417
|
+
|
|
2418
|
+
/* Change Report */
|
|
2419
|
+
.ws-report-section { border-top: 1px solid var(--border); padding-top: 12px; }
|
|
2420
|
+
.ws-report-desc { font-size: 14px; line-height: 1.6; color: var(--text); margin-bottom: 8px; white-space: pre-line; }
|
|
2421
|
+
.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); }
|
|
2422
|
+
.ws-report-warning-icon { font-size: 14px; flex-shrink: 0; }
|
|
2423
|
+
.ws-report-categories { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
|
|
2424
|
+
.ws-report-cat-group { }
|
|
2425
|
+
.ws-report-cat-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
|
2426
|
+
.ws-report-cat-label { font-size: 13px; font-weight: 600; color: var(--text); }
|
|
2427
|
+
.ws-report-cat-count { font-size: 11px; color: var(--text-muted); background: var(--surface); padding: 1px 6px; border-radius: 8px; }
|
|
2428
|
+
.ws-report-cat-items { list-style: none; margin: 0; padding: 0; }
|
|
2429
|
+
.ws-report-cat-items li { font-size: 13px; color: var(--text-muted); padding: 2px 0 2px 16px; position: relative; line-height: 1.5; }
|
|
2430
|
+
.ws-report-cat-items li::before { content: '•'; position: absolute; left: 4px; color: var(--text-muted); }
|
|
2431
|
+
.ws-report-nav-item { cursor: pointer; transition: color 0.15s, background 0.15s; border-radius: 4px; }
|
|
2432
|
+
.ws-report-nav-item:hover { color: var(--accent) !important; background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
2433
|
+
.ws-report-nav-hint { font-size: 11px; color: var(--accent); opacity: 0; transition: opacity 0.15s; margin-left: 4px; }
|
|
2434
|
+
.ws-report-nav-item:hover .ws-report-nav-hint { opacity: 1; }
|
|
2435
|
+
.ws-report-saved { opacity: 0.7; }
|
|
2436
|
+
.ws-report-saved-desc { font-size: 13px; color: var(--text-muted); }
|
|
2437
|
+
|
|
2438
|
+
/* Commit report (details/summary dropdown) */
|
|
2439
|
+
details.ws-commit-item { border-radius: 6px; margin-bottom: 4px; }
|
|
2440
|
+
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; }
|
|
2441
|
+
details.ws-commit-item > summary.ws-commit-summary::-webkit-details-marker { display: none; }
|
|
2442
|
+
details.ws-commit-item > summary.ws-commit-summary:hover { background: var(--surface); }
|
|
2443
|
+
.ws-commit-arrow { font-size: 10px; color: var(--text-muted); flex-shrink: 0; transition: transform 0.15s; display: inline-block; }
|
|
2444
|
+
details.ws-commit-item[open] .ws-commit-arrow { transform: rotate(90deg); }
|
|
2445
|
+
.ws-commit-report { margin: 4px 8px 8px 8px; padding: 10px 12px; background: var(--surface); border-radius: 6px; font-size: 12px; width: auto; }
|
|
2446
|
+
.ws-commit-report-loading, .ws-commit-report-empty { color: var(--text-muted); }
|
|
2447
|
+
.ws-commit-cat { margin-bottom: 6px; }
|
|
2448
|
+
.ws-commit-cat:last-child { margin-bottom: 0; }
|
|
2449
|
+
.ws-commit-cat-label { font-size: 11px; font-weight: 600; color: var(--text-muted); display: block; margin-bottom: 2px; }
|
|
2450
|
+
.ws-commit-cat-item { color: var(--text-muted); padding-left: 12px; position: relative; line-height: 1.5; }
|
|
2451
|
+
.ws-commit-cat-item::before { content: '•'; position: absolute; left: 3px; color: var(--text-muted); }
|
|
2452
|
+
|
|
2453
|
+
/* Preview toolbar — URL bar + viewport switcher */
|
|
2454
|
+
.ws-preview-toolbar {
|
|
2455
|
+
display: flex;
|
|
2456
|
+
align-items: center;
|
|
2457
|
+
gap: 8px;
|
|
2458
|
+
padding: 4px 8px;
|
|
2459
|
+
background: var(--bg);
|
|
2460
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
2461
|
+
flex-shrink: 0;
|
|
2462
|
+
}
|
|
2463
|
+
.ws-url-bar {
|
|
2464
|
+
display: flex;
|
|
2465
|
+
align-items: center;
|
|
2466
|
+
gap: 4px;
|
|
2467
|
+
flex: 1;
|
|
2468
|
+
min-width: 0;
|
|
2469
|
+
}
|
|
2470
|
+
.ws-url-back, .ws-url-refresh {
|
|
2471
|
+
background: none;
|
|
2472
|
+
border: 1px solid var(--border-subtle);
|
|
2473
|
+
border-radius: 4px;
|
|
2474
|
+
width: 28px;
|
|
2475
|
+
height: 28px;
|
|
2476
|
+
cursor: pointer;
|
|
2477
|
+
font-size: 14px;
|
|
2478
|
+
color: var(--text-muted);
|
|
2479
|
+
display: flex;
|
|
2480
|
+
align-items: center;
|
|
2481
|
+
justify-content: center;
|
|
2482
|
+
flex-shrink: 0;
|
|
2483
|
+
}
|
|
2484
|
+
.ws-url-back:hover, .ws-url-refresh:hover { background: var(--surface); color: var(--text); }
|
|
2485
|
+
.ws-url-input {
|
|
2486
|
+
flex: 1;
|
|
2487
|
+
min-width: 0;
|
|
2488
|
+
height: 28px;
|
|
2489
|
+
padding: 0 8px;
|
|
2490
|
+
border: 1px solid var(--border-subtle);
|
|
2491
|
+
border-radius: 4px;
|
|
2492
|
+
background: var(--surface);
|
|
2493
|
+
color: var(--text);
|
|
2494
|
+
font-family: var(--font-mono);
|
|
2495
|
+
font-size: 12px;
|
|
2496
|
+
}
|
|
2497
|
+
.ws-url-input:focus { outline: none; border-color: var(--accent); }
|
|
2498
|
+
.ws-viewport-switcher {
|
|
2499
|
+
display: flex;
|
|
2500
|
+
gap: 2px;
|
|
2501
|
+
flex-shrink: 0;
|
|
2502
|
+
}
|
|
2503
|
+
.ws-vp-btn {
|
|
2504
|
+
background: none;
|
|
2505
|
+
border: 1px solid transparent;
|
|
2506
|
+
border-radius: 4px;
|
|
2507
|
+
width: 28px;
|
|
2508
|
+
height: 28px;
|
|
2509
|
+
cursor: pointer;
|
|
2510
|
+
font-size: 14px;
|
|
2511
|
+
display: flex;
|
|
2512
|
+
align-items: center;
|
|
2513
|
+
justify-content: center;
|
|
2514
|
+
}
|
|
2515
|
+
.ws-vp-btn:hover { background: var(--surface); }
|
|
2516
|
+
.ws-vp-btn.ws-vp-active { background: var(--surface); border-color: var(--accent); }
|
|
2517
|
+
|
|
2518
|
+
/* Split-view compare */
|
|
2519
|
+
.ws-preview-container { position: relative; flex: 1; display: flex; }
|
|
2520
|
+
.ws-preview-container .ws-preview-full { flex: 1; }
|
|
2521
|
+
.ws-preview-main { display: none; flex: 1; flex-direction: column; border-left: 2px solid var(--accent); position: relative; }
|
|
2522
|
+
.ws-preview-main.hidden { display: none !important; }
|
|
2523
|
+
.ws-split-view .ws-preview-full,
|
|
2524
|
+
.ws-split-view .ws-preview-main { flex: 1; display: flex; flex-direction: column; min-width: 0; position: relative !important; top: auto !important; left: auto !important; right: auto !important; bottom: auto !important; }
|
|
2525
|
+
.ws-split-view .ws-preview-iframe { width: 100%; height: 100%; border: none; }
|
|
2526
|
+
.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; }
|
|
2527
|
+
.ws-preview-loading { display: flex; align-items: center; justify-content: center; flex: 1; color: var(--text-muted); font-size: 14px; }
|
|
2528
|
+
.btn-active { background: var(--accent) !important; color: white !important; }
|
|
2529
|
+
.ws-split-view .ws-preview-full::before {
|
|
2530
|
+
content: '⛺ 내 캠프';
|
|
2531
|
+
position: absolute; top: 8px; left: 8px; z-index: 10;
|
|
2532
|
+
background: var(--surface); padding: 2px 8px; border-radius: 4px;
|
|
2533
|
+
font-size: 11px; color: var(--text-muted); pointer-events: none;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
/* Ship report preview */
|
|
2537
|
+
.ship-report-preview { margin-bottom: 12px; padding: 10px; background: var(--surface); border-radius: 6px; font-size: 13px; line-height: 1.5; }
|
|
2538
|
+
.ship-report-desc { margin-bottom: 8px; }
|
package/dist/bin/sanjang.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// Node 22+ required for --experimental-transform-types
|
|
3
|
+
const nodeVersion = parseInt(process.versions.node.split(".")[0], 10);
|
|
4
|
+
if (nodeVersion < 22) {
|
|
5
|
+
console.error(`⛰ 산장: Node 22 이상이 필요합니다. (현재: v${process.versions.node})`);
|
|
6
|
+
console.error(" 해결: nvm install 22 && nvm use 22");
|
|
7
|
+
console.error(" 또는: https://nodejs.org 에서 최신 LTS를 설치하세요.");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
2
10
|
import { execSync } from "node:child_process";
|
|
3
11
|
import { existsSync } from "node:fs";
|
|
4
12
|
import { resolve } from "node:path";
|
|
@@ -14,7 +22,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
14
22
|
i++;
|
|
15
23
|
}
|
|
16
24
|
if (args[i] === "--port" && args[i + 1]) {
|
|
17
|
-
port = parseInt(args[i + 1]);
|
|
25
|
+
port = parseInt(args[i + 1], 10);
|
|
18
26
|
i++;
|
|
19
27
|
}
|
|
20
28
|
if (args[i] === "--force") {
|
|
@@ -53,8 +61,8 @@ if (command === "init") {
|
|
|
53
61
|
rl.question(" 어떤 앱을 띄울까요? [번호]: ", resolve);
|
|
54
62
|
});
|
|
55
63
|
rl.close();
|
|
56
|
-
const idx = parseInt(answer) - 1;
|
|
57
|
-
if (idx < 0 || idx >= apps.length || isNaN(idx)) {
|
|
64
|
+
const idx = parseInt(answer, 10) - 1;
|
|
65
|
+
if (idx < 0 || idx >= apps.length || Number.isNaN(idx)) {
|
|
58
66
|
console.error("⛰ 잘못된 선택입니다.");
|
|
59
67
|
process.exit(1);
|
|
60
68
|
}
|
|
@@ -116,21 +124,156 @@ if (command === "init") {
|
|
|
116
124
|
}
|
|
117
125
|
else if (command === "help" || command === "--help" || command === "-h") {
|
|
118
126
|
console.log(`
|
|
119
|
-
⛰ 산장 (Sanjang) —
|
|
127
|
+
⛰ 산장 (Sanjang) — 비개발자가 AI로 코드를 고치고 PR을 보낼 수 있는 로컬 개발 환경
|
|
120
128
|
|
|
121
129
|
사용법:
|
|
122
130
|
sanjang 서버 시작 (대시보드: http://localhost:4000)
|
|
123
131
|
sanjang init 프로젝트 분석 → sanjang.config.js 생성
|
|
132
|
+
sanjang list 캠프 목록 보기
|
|
133
|
+
sanjang status 서버 + 캠프 상태 확인
|
|
134
|
+
sanjang start <name> 캠프 시작
|
|
135
|
+
sanjang stop <name> 캠프 중지
|
|
136
|
+
sanjang open <name> 캠프를 브라우저에서 열기
|
|
124
137
|
sanjang help 이 도움말
|
|
125
138
|
|
|
126
139
|
옵션:
|
|
127
140
|
--port <N> 대시보드 포트 (기본: 4000)
|
|
128
141
|
--project <path> 프로젝트 경로 (기본: 현재 디렉토리)
|
|
142
|
+
--json JSON으로 출력 (Claude Code 등 자동화용)
|
|
129
143
|
--force 기존 설정을 덮어쓰고 다시 생성
|
|
130
144
|
|
|
131
145
|
자세히: https://github.com/paul-sherpas/sanjang
|
|
132
146
|
`);
|
|
133
147
|
}
|
|
148
|
+
else if (command === "list" || command === "status" || command === "start" || command === "stop" || command === "open") {
|
|
149
|
+
// CLI commands that talk to the running sanjang server
|
|
150
|
+
const jsonMode = args.includes("--json");
|
|
151
|
+
const campName = args[1] && !args[1].startsWith("-") ? args[1] : undefined;
|
|
152
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
153
|
+
async function apiFetch(path, method = "GET", body) {
|
|
154
|
+
const opts = { method, headers: { "content-type": "application/json" } };
|
|
155
|
+
if (body)
|
|
156
|
+
opts.body = JSON.stringify(body);
|
|
157
|
+
const res = await fetch(`${baseUrl}${path}`, opts);
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
160
|
+
throw new Error(err.error || `HTTP ${res.status}`);
|
|
161
|
+
}
|
|
162
|
+
return res.json();
|
|
163
|
+
}
|
|
164
|
+
async function tryApi(fn) {
|
|
165
|
+
try {
|
|
166
|
+
return await fn();
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
if (err.cause && String(err.cause).includes("ECONNREFUSED")) {
|
|
170
|
+
console.error("⛰ 산장 서버가 실행되지 않고 있습니다.");
|
|
171
|
+
console.error(` sanjang 또는 npx sanjang 으로 먼저 시작하세요.`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
if (command === "list") {
|
|
179
|
+
const camps = await tryApi(() => apiFetch("/api/playgrounds"));
|
|
180
|
+
if (jsonMode) {
|
|
181
|
+
process.stdout.write(JSON.stringify(camps, null, 2) + "\n");
|
|
182
|
+
}
|
|
183
|
+
else if (camps.length === 0) {
|
|
184
|
+
console.log("⛰ 캠프가 없습니다. 대시보드에서 만들어보세요.");
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
console.log("⛰ 캠프 목록:\n");
|
|
188
|
+
for (const c of camps) {
|
|
189
|
+
const status = c.status === "running" ? "🟢" : c.status === "error" ? "🔴" : "⚪";
|
|
190
|
+
const url = c.status === "running" && c.fePort ? `http://localhost:${c.fePort}` : "";
|
|
191
|
+
console.log(` ${status} ${c.name}\t${c.branch}\t${url}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (command === "status") {
|
|
196
|
+
const camps = await tryApi(() => apiFetch("/api/playgrounds"));
|
|
197
|
+
const running = camps.filter(c => c.status === "running").length;
|
|
198
|
+
const total = camps.length;
|
|
199
|
+
if (jsonMode) {
|
|
200
|
+
process.stdout.write(JSON.stringify({ server: { url: baseUrl, status: "running" }, camps }, null, 2) + "\n");
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
console.log(`⛰ 산장 서버: ${baseUrl}`);
|
|
204
|
+
console.log(` 캠프: ${total}개 (실행 중 ${running}개)`);
|
|
205
|
+
for (const c of camps) {
|
|
206
|
+
const status = c.status === "running" ? "🟢" : c.status === "error" ? "🔴" : "⚪";
|
|
207
|
+
console.log(` ${status} ${c.name} (${c.status})`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (command === "start") {
|
|
212
|
+
if (!campName) {
|
|
213
|
+
console.error("⛰ 사용법: sanjang start <캠프이름>");
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
const result = await tryApi(() => apiFetch(`/api/playgrounds/${campName}/start`, "POST"));
|
|
217
|
+
if (jsonMode) {
|
|
218
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
console.log(`⛰ ${campName} 캠프를 시작합니다.`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (command === "stop") {
|
|
225
|
+
if (!campName) {
|
|
226
|
+
console.error("⛰ 사용법: sanjang stop <캠프이름>");
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
const result = await tryApi(() => apiFetch(`/api/playgrounds/${campName}/stop`, "POST"));
|
|
230
|
+
if (jsonMode) {
|
|
231
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
console.log(`⛰ ${campName} 캠프를 중지합니다.`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else if (command === "open") {
|
|
238
|
+
if (!campName) {
|
|
239
|
+
console.error("⛰ 사용법: sanjang open <캠프이름>");
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
const camps = await tryApi(() => apiFetch("/api/playgrounds"));
|
|
243
|
+
const camp = camps.find(c => c.name === campName);
|
|
244
|
+
if (!camp) {
|
|
245
|
+
console.error(`⛰ "${campName}" 캠프를 찾을 수 없습니다.`);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
if (camp.status !== "running" || !camp.fePort) {
|
|
249
|
+
console.error(`⛰ "${campName}" 캠프가 실행 중이 아닙니다. sanjang start ${campName} 을 먼저 실행하세요.`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
const campUrl = `http://localhost:${camp.fePort}`;
|
|
253
|
+
if (jsonMode) {
|
|
254
|
+
process.stdout.write(JSON.stringify({ name: campName, url: campUrl, status: camp.status }, null, 2) + "\n");
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
console.log(`⛰ ${campName} 캠프를 브라우저에서 엽니다. → ${campUrl}`);
|
|
258
|
+
}
|
|
259
|
+
const { spawn: spawnOpen } = await import("node:child_process");
|
|
260
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
261
|
+
try {
|
|
262
|
+
spawnOpen(openCmd, [campUrl], { stdio: "ignore", detached: true }).unref();
|
|
263
|
+
}
|
|
264
|
+
catch { /* */ }
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
if (jsonMode) {
|
|
269
|
+
process.stdout.write(JSON.stringify({ error: err.message }, null, 2) + "\n");
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
console.error(`⛰ 오류: ${err.message}`);
|
|
273
|
+
}
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
134
277
|
else {
|
|
135
278
|
// Default: start server — auto-init if no config exists
|
|
136
279
|
const configPath = resolve(projectRoot, "sanjang.config.js");
|
|
@@ -147,10 +290,12 @@ else {
|
|
|
147
290
|
console.log("");
|
|
148
291
|
const { createInterface } = await import("node:readline");
|
|
149
292
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
150
|
-
const answer = await new Promise((r) => {
|
|
293
|
+
const answer = await new Promise((r) => {
|
|
294
|
+
rl.question(" 어떤 앱을 띄울까요? [번호]: ", r);
|
|
295
|
+
});
|
|
151
296
|
rl.close();
|
|
152
|
-
const idx = parseInt(answer) - 1;
|
|
153
|
-
if (idx < 0 || idx >= apps.length || isNaN(idx)) {
|
|
297
|
+
const idx = parseInt(answer, 10) - 1;
|
|
298
|
+
if (idx < 0 || idx >= apps.length || Number.isNaN(idx)) {
|
|
154
299
|
console.error("⛰ 잘못된 선택입니다.");
|
|
155
300
|
process.exit(1);
|
|
156
301
|
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ import type { DetectedApp, DetectedProject, GenerateConfigResult, SanjangConfig
|
|
|
4
4
|
* Returns merged config with defaults.
|
|
5
5
|
*/
|
|
6
6
|
export declare function loadConfig(projectRoot: string): Promise<SanjangConfig>;
|
|
7
|
+
/**
|
|
8
|
+
* Auto-detect a test command from package.json.
|
|
9
|
+
* Returns null if no test script found or it's the npm default placeholder.
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectTestCommand(projectRoot: string, cwd?: string): string | null;
|
|
7
12
|
/**
|
|
8
13
|
* Auto-detect project type and generate config.
|
|
9
14
|
*/
|