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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paul Jeong
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# 산장 (Sanjang)
|
|
2
|
+
|
|
3
|
+
> 바이브코더를 위한 로컬 개발 환경 매니저
|
|
4
|
+
|
|
5
|
+
브랜치 하나로 격리된 dev 환경을 띄우고, 프리뷰하고, 관리하는 도구입니다.
|
|
6
|
+
Git worktree 기반으로 동시에 여러 작업 환경(캠프)을 운영할 수 있습니다.
|
|
7
|
+
|
|
8
|
+
## 주요 기능
|
|
9
|
+
|
|
10
|
+
- **포털 홈**: 이어하기(열린 PR + 캠프)와 새로 시작(자연어 퀵스타트)로 대시보드 첫 화면 구성
|
|
11
|
+
- **캠프 생성**: 브랜치 선택 → 자동 worktree + 의존성 설치 + dev 서버 시작
|
|
12
|
+
- **의존성 캐시**: `init` 시 node_modules 프리빌드, 캠프 생성 시 캐시 클론으로 수초 만에 완료
|
|
13
|
+
- **멀티앱 감지**: 모노레포에서 여러 앱을 자동 감지하고 init 시 인터랙티브 선택
|
|
14
|
+
- **포트 자동 관리**: 캠프마다 다른 포트 자동 할당, 충돌 없음
|
|
15
|
+
- **대시보드**: 브라우저에서 모든 캠프 상태 확인 + 시작/중지/삭제
|
|
16
|
+
- **스냅샷**: 현재 상태 저장/복원 (git stash 기반)
|
|
17
|
+
- **일 시키기**: 대시보드에서 Claude에게 작업 지시 (claude -p 연동)
|
|
18
|
+
- **팀에 보내기**: 변경사항 commit + push → PR 생성 플로우
|
|
19
|
+
- **프로젝트 자동 감지**: Next.js, Vite, SvelteKit, Angular, shadow-cljs, Turborepo 등 자동 인식
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 설치 (for AI Assistants)
|
|
24
|
+
|
|
25
|
+
> 이 섹션은 Claude Code 같은 AI 어시스턴트가 읽고 실행하는 용도입니다.
|
|
26
|
+
> 사용자가 이 GitHub URL을 던지면 아래 절차를 따라 설치하세요.
|
|
27
|
+
|
|
28
|
+
### 자동 설치 절차
|
|
29
|
+
|
|
30
|
+
1. **대상 프로젝트의 git root로 이동합니다.**
|
|
31
|
+
|
|
32
|
+
2. **산장을 설치하고 시작합니다:**
|
|
33
|
+
```bash
|
|
34
|
+
npx sanjang init
|
|
35
|
+
```
|
|
36
|
+
이 명령이 자동으로:
|
|
37
|
+
- 프로젝트를 분석하여 `sanjang.config.js` 생성
|
|
38
|
+
- `.gitignore`에 `.sanjang/` 추가
|
|
39
|
+
- 대시보드 서버 시작 (http://localhost:4000)
|
|
40
|
+
|
|
41
|
+
3. **사용자에게 안내합니다:**
|
|
42
|
+
```
|
|
43
|
+
⛰ 산장 설치 완료!
|
|
44
|
+
대시보드: http://localhost:4000
|
|
45
|
+
"+ 새 캠프" 버튼으로 브랜치별 작업 환경을 만들 수 있습니다.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 설치 후 사용법 안내
|
|
49
|
+
|
|
50
|
+
- **새로 시작**: 포털 홈에서 "뭘 하고 싶어?" 입력 → 자동으로 브랜치+캠프 생성
|
|
51
|
+
- **캠프 생성**: 대시보드에서 "+ 새 캠프" → 브랜치 선택 → 자동 생성+시작
|
|
52
|
+
- **이어하기**: 포털 홈에서 열린 PR이나 기존 캠프 클릭 → 바로 작업 재개
|
|
53
|
+
- **프리뷰**: 각 캠프 카드의 URL 클릭
|
|
54
|
+
- **일 시키기**: 캠프 카드의 입력창에 프롬프트 입력 → Claude가 해당 worktree에서 작업
|
|
55
|
+
- **팀에 보내기**: 변경 완료 후 "팀에 보내기" 버튼 → commit + push
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 설치 (사람용)
|
|
60
|
+
|
|
61
|
+
### 방법 1: npx (추천)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
cd your-project
|
|
65
|
+
npx sanjang init
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
config만 생성하고 서버는 나중에 시작하려면:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx sanjang init --no-start
|
|
72
|
+
npx sanjang
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 방법 2: 글로벌 설치
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install -g sanjang
|
|
79
|
+
cd your-project
|
|
80
|
+
sanjang init
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 설정 (`sanjang.config.js`)
|
|
86
|
+
|
|
87
|
+
`sanjang init`이 자동으로 생성하지만, 직접 작성/수정할 수도 있습니다.
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
export default {
|
|
91
|
+
// 프론트엔드 개발 서버
|
|
92
|
+
dev: {
|
|
93
|
+
command: 'npm run dev', // 실행 명령
|
|
94
|
+
port: 3000, // 기본 포트 (캠프별 +1씩 증가)
|
|
95
|
+
portFlag: '--port', // 포트 오버라이드 플래그
|
|
96
|
+
cwd: '.', // worktree 기준 상대 경로
|
|
97
|
+
env: {}, // 추가 환경변수
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// (선택) 설치 명령 — 캠프 생성 시 자동 실행
|
|
101
|
+
setup: 'npm install',
|
|
102
|
+
|
|
103
|
+
// (선택) 복사할 파일 — gitignored 파일을 메인에서 복사
|
|
104
|
+
copyFiles: ['.env', '.env.local'],
|
|
105
|
+
|
|
106
|
+
// (선택) 백엔드 — 전체 캠프가 공유하는 서버
|
|
107
|
+
backend: {
|
|
108
|
+
command: 'npm run start:api',
|
|
109
|
+
port: 8000,
|
|
110
|
+
healthCheck: '/health',
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// (선택) 포트 범위
|
|
114
|
+
ports: {
|
|
115
|
+
fe: { base: 3000, slots: 8 },
|
|
116
|
+
be: { base: 8000, slots: 8 },
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 프레임워크별 예시
|
|
122
|
+
|
|
123
|
+
**Next.js:**
|
|
124
|
+
```js
|
|
125
|
+
export default {
|
|
126
|
+
dev: { command: 'npx next dev', port: 3000, portFlag: '-p' },
|
|
127
|
+
setup: 'npm install',
|
|
128
|
+
copyFiles: ['.env.local'],
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Vite (React/Vue/Svelte):**
|
|
133
|
+
```js
|
|
134
|
+
export default {
|
|
135
|
+
dev: { command: 'npx vite dev', port: 5173, portFlag: '--port' },
|
|
136
|
+
setup: 'npm install',
|
|
137
|
+
};
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Turborepo (monorepo):**
|
|
141
|
+
```js
|
|
142
|
+
export default {
|
|
143
|
+
dev: {
|
|
144
|
+
command: 'npx turbo run dev --filter=my-app',
|
|
145
|
+
port: 3000,
|
|
146
|
+
portFlag: '--port',
|
|
147
|
+
cwd: '.',
|
|
148
|
+
},
|
|
149
|
+
setup: 'npm install',
|
|
150
|
+
copyFiles: ['.env', 'apps/my-app/.env.local'],
|
|
151
|
+
};
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 자연어 퀵스타트
|
|
157
|
+
|
|
158
|
+
포털 홈에서 자연어로 작업을 설명하면 자동으로 캠프가 만들어집니다:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
"로그인 버튼 색상 변경" → camp/login-button-color-change 브랜치 생성
|
|
162
|
+
"대시보드 필터 추가" → camp/dashboard-filter-add 브랜치 생성
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
한국어와 영어 모두 지원합니다. 내부적으로 한국어 개발 용어를 영문 kebab-case로 변환합니다.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 의존성 캐시
|
|
170
|
+
|
|
171
|
+
`sanjang init` 시 node_modules를 프리빌드하여 `.sanjang/cache/`에 저장합니다. 이후 캠프 생성 시 `npm install` 대신 캐시를 복사하여 수초 만에 완료됩니다.
|
|
172
|
+
|
|
173
|
+
- 캐시는 lockfile 해시로 유효성을 검증합니다
|
|
174
|
+
- lockfile이 변경되면 자동으로 캐시를 무효화합니다
|
|
175
|
+
- `POST /api/cache/rebuild`로 수동 리빌드 가능합니다
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 개념
|
|
180
|
+
|
|
181
|
+
| 용어 | 설명 |
|
|
182
|
+
|------|------|
|
|
183
|
+
| **산장** | 이 도구 전체. 대시보드 서버 + 캠프 매니저 |
|
|
184
|
+
| **캠프** | 개별 작업 환경. git worktree + dev 서버 |
|
|
185
|
+
| **포털** | 대시보드 첫 화면. 이어하기 + 새로 시작 |
|
|
186
|
+
| **스냅샷** | 캠프의 현재 상태를 저장한 것 (git stash) |
|
|
187
|
+
| **캐시** | 의존성 프리빌드. 캠프 생성 속도 향상 |
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 개발
|
|
192
|
+
|
|
193
|
+
TypeScript로 작성되었습니다. 빌드 스텝 없이 Node.js의 `--experimental-transform-types`로 직접 실행합니다.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# 테스트
|
|
197
|
+
npm test
|
|
198
|
+
|
|
199
|
+
# 타입 체크
|
|
200
|
+
npm run typecheck
|
|
201
|
+
|
|
202
|
+
# 린트
|
|
203
|
+
npm run lint
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 요구사항
|
|
209
|
+
|
|
210
|
+
- Node.js 22+ (`--experimental-transform-types` 지원)
|
|
211
|
+
- Git 2.20+ (worktree 지원)
|
|
212
|
+
- (선택) Claude Code — "일 시키기" 기능용
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 라이선스
|
|
217
|
+
|
|
218
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Tests for bin/sanjang.js CLI flag parsing logic
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
|
|
6
|
+
describe("sanjang CLI flag parsing", () => {
|
|
7
|
+
it("detects --no-start flag in args array", () => {
|
|
8
|
+
const args = ["init", "--no-start"];
|
|
9
|
+
const noStart = args.includes("--no-start");
|
|
10
|
+
assert.equal(noStart, true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("does not detect --no-start when absent", () => {
|
|
14
|
+
const args = ["init"];
|
|
15
|
+
const noStart = args.includes("--no-start");
|
|
16
|
+
assert.equal(noStart, false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("parses --port value correctly", () => {
|
|
20
|
+
const args = ["init", "--port", "5000"];
|
|
21
|
+
let port = 4000;
|
|
22
|
+
for (let i = 0; i < args.length; i++) {
|
|
23
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
24
|
+
port = parseInt(args[i + 1]!);
|
|
25
|
+
i++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
assert.equal(port, 5000);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("uses default port 4000 when --port not specified", () => {
|
|
32
|
+
const args = ["init"];
|
|
33
|
+
let port = 4000;
|
|
34
|
+
for (let i = 0; i < args.length; i++) {
|
|
35
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
36
|
+
port = parseInt(args[i + 1]!);
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
assert.equal(port, 4000);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/bin/sanjang.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Shell wrapper — delegates to the TypeScript entry point
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const tsEntry = join(__dirname, 'sanjang.ts');
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
execFileSync(process.execPath, ['--experimental-transform-types', tsEntry, ...process.argv.slice(2)], {
|
|
12
|
+
stdio: 'inherit',
|
|
13
|
+
env: { ...process.env, NODE_NO_WARNINGS: '1' },
|
|
14
|
+
});
|
|
15
|
+
} catch (err) {
|
|
16
|
+
process.exit(err?.status ?? 1);
|
|
17
|
+
}
|
package/bin/sanjang.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const args: string[] = process.argv.slice(2);
|
|
6
|
+
const command: string | undefined = args[0];
|
|
7
|
+
|
|
8
|
+
// Parse options
|
|
9
|
+
let projectRoot: string = process.cwd();
|
|
10
|
+
let port: number = 4000;
|
|
11
|
+
let force: boolean = false;
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
if (args[i] === "--project" && args[i + 1]) {
|
|
15
|
+
projectRoot = resolve(args[i + 1]!);
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
19
|
+
port = parseInt(args[i + 1]!);
|
|
20
|
+
i++;
|
|
21
|
+
}
|
|
22
|
+
if (args[i] === "--force") {
|
|
23
|
+
force = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Find git root
|
|
28
|
+
try {
|
|
29
|
+
projectRoot = execSync("git rev-parse --show-toplevel", {
|
|
30
|
+
cwd: projectRoot,
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
33
|
+
}).trim();
|
|
34
|
+
} catch {
|
|
35
|
+
console.error("⛰ 산장: git 저장소를 찾을 수 없습니다.");
|
|
36
|
+
console.error(" git 저장소 안에서 실행해주세요.");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (command === "init") {
|
|
41
|
+
const { generateConfig, detectApps } = await import("../lib/config.ts");
|
|
42
|
+
|
|
43
|
+
// Detect apps in subdirectories
|
|
44
|
+
const apps = detectApps(projectRoot);
|
|
45
|
+
let appDir: string | undefined;
|
|
46
|
+
|
|
47
|
+
if (apps.length >= 2) {
|
|
48
|
+
// Multi-app interview
|
|
49
|
+
console.log("");
|
|
50
|
+
console.log("⛰ 여러 앱이 감지되었습니다:");
|
|
51
|
+
for (let i = 0; i < apps.length; i++) {
|
|
52
|
+
console.log(` ${i + 1}) ${apps[i]!.dir}/\t(${apps[i]!.framework})`);
|
|
53
|
+
}
|
|
54
|
+
console.log("");
|
|
55
|
+
|
|
56
|
+
const { createInterface } = await import("node:readline");
|
|
57
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
58
|
+
const answer = await new Promise<string>((resolve) => {
|
|
59
|
+
rl.question(" 어떤 앱을 띄울까요? [번호]: ", resolve);
|
|
60
|
+
});
|
|
61
|
+
rl.close();
|
|
62
|
+
|
|
63
|
+
const idx = parseInt(answer) - 1;
|
|
64
|
+
if (idx < 0 || idx >= apps.length || isNaN(idx)) {
|
|
65
|
+
console.error("⛰ 잘못된 선택입니다.");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
appDir = apps[idx]!.dir;
|
|
69
|
+
console.log(` → ${appDir}/ (${apps[idx]!.framework}) 선택됨`);
|
|
70
|
+
} else if (apps.length === 1) {
|
|
71
|
+
appDir = apps[0]!.dir;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = generateConfig(projectRoot, { appDir, force });
|
|
75
|
+
|
|
76
|
+
if (result.created) {
|
|
77
|
+
console.log(`⛰ ${result.message}`);
|
|
78
|
+
console.log(` 프레임워크: ${result.framework}`);
|
|
79
|
+
console.log(` 설정 파일: ${result.configPath}`);
|
|
80
|
+
} else {
|
|
81
|
+
console.log(`⛰ ${result.message}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Add .sanjang to .gitignore if not present
|
|
85
|
+
const gitignorePath = resolve(projectRoot, ".gitignore");
|
|
86
|
+
if (existsSync(gitignorePath)) {
|
|
87
|
+
const { readFileSync, appendFileSync } = await import("node:fs");
|
|
88
|
+
const content = readFileSync(gitignorePath, "utf8");
|
|
89
|
+
if (!content.includes(".sanjang")) {
|
|
90
|
+
appendFileSync(gitignorePath, "\n# Sanjang local dev camps\n.sanjang/\n");
|
|
91
|
+
console.log(" .gitignore에 .sanjang/ 추가됨");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Prebuild dependency cache
|
|
96
|
+
const { loadConfig } = await import("../lib/config.ts");
|
|
97
|
+
const initConfig = await loadConfig(projectRoot);
|
|
98
|
+
if (initConfig.setup) {
|
|
99
|
+
console.log("");
|
|
100
|
+
console.log(" 의존성 캐시를 빌드합니다...");
|
|
101
|
+
const { buildCache } = await import("../lib/engine/cache.ts");
|
|
102
|
+
const cacheResult = await buildCache(projectRoot, initConfig, (msg: string) => {
|
|
103
|
+
console.log(` ${msg}`);
|
|
104
|
+
});
|
|
105
|
+
if (cacheResult.success) {
|
|
106
|
+
console.log(` 캐시 빌드 완료 ✓ (${(cacheResult.duration / 1000).toFixed(1)}초)`);
|
|
107
|
+
} else {
|
|
108
|
+
console.log(` ⚠️ 캐시 빌드 실패: ${cacheResult.error}`);
|
|
109
|
+
console.log(" 캠프 생성 시 일반 설치를 사용합니다.");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Auto-start server unless --no-start
|
|
114
|
+
const noStart = args.includes("--no-start");
|
|
115
|
+
if (!noStart) {
|
|
116
|
+
console.log("");
|
|
117
|
+
console.log(" 서버를 시작합니다...");
|
|
118
|
+
const { startServer } = await import("../lib/server.ts");
|
|
119
|
+
await startServer(projectRoot, { port });
|
|
120
|
+
} else {
|
|
121
|
+
console.log("");
|
|
122
|
+
console.log(" 다음 단계: sanjang 또는 npx sanjang 으로 서버를 시작하세요.");
|
|
123
|
+
}
|
|
124
|
+
} else if (command === "help" || command === "--help" || command === "-h") {
|
|
125
|
+
console.log(`
|
|
126
|
+
⛰ 산장 (Sanjang) — 바이브코더를 위한 로컬 개발 환경 매니저
|
|
127
|
+
|
|
128
|
+
사용법:
|
|
129
|
+
sanjang 서버 시작 (대시보드: http://localhost:4000)
|
|
130
|
+
sanjang init 프로젝트 분석 → sanjang.config.js 생성
|
|
131
|
+
sanjang help 이 도움말
|
|
132
|
+
|
|
133
|
+
옵션:
|
|
134
|
+
--port <N> 대시보드 포트 (기본: 4000)
|
|
135
|
+
--project <path> 프로젝트 경로 (기본: 현재 디렉토리)
|
|
136
|
+
--force 기존 설정을 덮어쓰고 다시 생성
|
|
137
|
+
|
|
138
|
+
자세히: https://github.com/paul-sherpas/sanjang
|
|
139
|
+
`);
|
|
140
|
+
} else {
|
|
141
|
+
// Default: start server
|
|
142
|
+
const { startServer } = await import("../lib/server.ts");
|
|
143
|
+
await startServer(projectRoot, { port });
|
|
144
|
+
}
|