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 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
+ }