sanjang 0.3.0 → 0.3.2
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/dist/bin/sanjang.d.ts +1 -0
- package/dist/bin/sanjang.js +138 -0
- package/dist/lib/config.d.ts +19 -0
- package/dist/lib/config.js +318 -0
- package/dist/lib/engine/cache.d.ts +7 -0
- package/dist/lib/engine/cache.js +183 -0
- package/dist/lib/engine/config-hotfix.d.ts +7 -0
- package/dist/lib/engine/config-hotfix.js +129 -0
- package/dist/lib/engine/conflict.d.ts +12 -0
- package/dist/lib/engine/conflict.js +32 -0
- package/dist/lib/engine/diagnostics.d.ts +15 -0
- package/dist/lib/engine/diagnostics.js +58 -0
- package/dist/lib/engine/naming.d.ts +10 -0
- package/dist/lib/engine/naming.js +83 -0
- package/dist/lib/engine/ports.d.ts +9 -0
- package/dist/lib/engine/ports.js +55 -0
- package/dist/lib/engine/pr.d.ts +27 -0
- package/dist/lib/engine/pr.js +54 -0
- package/dist/lib/engine/process.d.ts +15 -0
- package/dist/lib/engine/process.js +250 -0
- package/dist/lib/engine/self-heal.d.ts +12 -0
- package/dist/lib/engine/self-heal.js +98 -0
- package/dist/lib/engine/smart-init.d.ts +7 -0
- package/dist/lib/engine/smart-init.js +138 -0
- package/dist/lib/engine/smart-pr.d.ts +19 -0
- package/dist/lib/engine/smart-pr.js +105 -0
- package/dist/lib/engine/snapshot.d.ts +10 -0
- package/dist/lib/engine/snapshot.js +35 -0
- package/dist/lib/engine/state.d.ts +7 -0
- package/dist/lib/engine/state.js +53 -0
- package/dist/lib/engine/suggest.d.ts +21 -0
- package/dist/lib/engine/suggest.js +121 -0
- package/dist/lib/engine/warp.d.ts +23 -0
- package/dist/lib/engine/warp.js +32 -0
- package/dist/lib/engine/watcher.d.ts +11 -0
- package/dist/lib/engine/watcher.js +43 -0
- package/dist/lib/engine/worktree.d.ts +13 -0
- package/dist/lib/engine/worktree.js +91 -0
- package/dist/lib/server.d.ts +20 -0
- package/dist/lib/server.js +1399 -0
- package/dist/lib/types.d.ts +109 -0
- package/dist/lib/types.js +2 -0
- package/package.json +5 -5
- package/bin/__tests__/sanjang.test.ts +0 -42
- package/bin/sanjang.js +0 -17
- package/bin/sanjang.ts +0 -144
- package/lib/config.ts +0 -337
- package/lib/engine/cache.ts +0 -218
- package/lib/engine/config-hotfix.ts +0 -161
- package/lib/engine/conflict.ts +0 -33
- package/lib/engine/diagnostics.ts +0 -81
- package/lib/engine/naming.ts +0 -93
- package/lib/engine/ports.ts +0 -61
- package/lib/engine/pr.ts +0 -71
- package/lib/engine/process.ts +0 -283
- package/lib/engine/self-heal.ts +0 -130
- package/lib/engine/smart-init.ts +0 -136
- package/lib/engine/smart-pr.ts +0 -130
- package/lib/engine/snapshot.ts +0 -45
- package/lib/engine/state.ts +0 -60
- package/lib/engine/suggest.ts +0 -169
- package/lib/engine/warp.ts +0 -47
- package/lib/engine/watcher.ts +0 -40
- package/lib/engine/worktree.ts +0 -100
- package/lib/server.ts +0 -1560
- package/lib/types.ts +0 -130
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export interface Camp {
|
|
2
|
+
name: string;
|
|
3
|
+
branch: string;
|
|
4
|
+
slot: number;
|
|
5
|
+
fePort: number;
|
|
6
|
+
bePort: number;
|
|
7
|
+
url?: string;
|
|
8
|
+
status: "stopped" | "starting" | "starting-frontend" | "running" | "setting-up" | "error";
|
|
9
|
+
description?: string;
|
|
10
|
+
baseCommit?: string;
|
|
11
|
+
parentBranch?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DevConfig {
|
|
14
|
+
command: string;
|
|
15
|
+
port: number;
|
|
16
|
+
portFlag: string | null;
|
|
17
|
+
cwd: string;
|
|
18
|
+
env: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
export interface BackendConfig {
|
|
21
|
+
command: string;
|
|
22
|
+
port: number;
|
|
23
|
+
healthCheck?: string;
|
|
24
|
+
cwd?: string;
|
|
25
|
+
env?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
export interface PortRange {
|
|
28
|
+
base: number;
|
|
29
|
+
slots: number;
|
|
30
|
+
}
|
|
31
|
+
export interface PortsConfig {
|
|
32
|
+
fe: PortRange;
|
|
33
|
+
be: PortRange;
|
|
34
|
+
}
|
|
35
|
+
export interface SanjangConfig {
|
|
36
|
+
dev: DevConfig;
|
|
37
|
+
setup: string | null;
|
|
38
|
+
copyFiles: string[];
|
|
39
|
+
backend: BackendConfig | null;
|
|
40
|
+
ports: PortsConfig;
|
|
41
|
+
_autoDetected?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface PortAllocation {
|
|
44
|
+
slot: number;
|
|
45
|
+
fePort: number;
|
|
46
|
+
bePort: number;
|
|
47
|
+
}
|
|
48
|
+
export interface PortStatus extends PortAllocation {
|
|
49
|
+
feBusy: boolean;
|
|
50
|
+
beBusy: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface CacheValidation {
|
|
53
|
+
valid: boolean;
|
|
54
|
+
reason?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface CacheBuildResult {
|
|
57
|
+
success: boolean;
|
|
58
|
+
error?: string;
|
|
59
|
+
duration: number;
|
|
60
|
+
}
|
|
61
|
+
export interface CacheApplyResult {
|
|
62
|
+
applied: boolean;
|
|
63
|
+
reason?: string;
|
|
64
|
+
duration?: number;
|
|
65
|
+
count?: number;
|
|
66
|
+
}
|
|
67
|
+
export interface LockfileInfo {
|
|
68
|
+
path: string;
|
|
69
|
+
name: string;
|
|
70
|
+
}
|
|
71
|
+
export interface DetectedProject {
|
|
72
|
+
framework: string;
|
|
73
|
+
dev: DevConfig;
|
|
74
|
+
setup: string | null;
|
|
75
|
+
copyFiles: string[];
|
|
76
|
+
_note?: string;
|
|
77
|
+
}
|
|
78
|
+
export interface DetectedApp {
|
|
79
|
+
dir: string;
|
|
80
|
+
framework: string;
|
|
81
|
+
detected: DetectedProject;
|
|
82
|
+
}
|
|
83
|
+
export interface GenerateConfigResult {
|
|
84
|
+
created: boolean;
|
|
85
|
+
framework?: string;
|
|
86
|
+
configPath?: string;
|
|
87
|
+
message: string;
|
|
88
|
+
}
|
|
89
|
+
export interface SnapshotInfo {
|
|
90
|
+
name: string;
|
|
91
|
+
date: string;
|
|
92
|
+
message: string;
|
|
93
|
+
}
|
|
94
|
+
export interface DiagnosticsResult {
|
|
95
|
+
status: string;
|
|
96
|
+
checks: DiagnosticsCheck[];
|
|
97
|
+
}
|
|
98
|
+
export interface DiagnosticsCheck {
|
|
99
|
+
name: string;
|
|
100
|
+
ok: boolean;
|
|
101
|
+
message: string;
|
|
102
|
+
}
|
|
103
|
+
export type BroadcastMessage = {
|
|
104
|
+
type: string;
|
|
105
|
+
name?: string;
|
|
106
|
+
source?: string;
|
|
107
|
+
data?: unknown;
|
|
108
|
+
};
|
|
109
|
+
export type EventCallback = (event: BroadcastMessage) => void;
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sanjang",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Local dev environment manager for vibe coders",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"sanjang": "./bin/sanjang.js"
|
|
7
|
+
"sanjang": "./dist/bin/sanjang.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/sanjang.js",
|
|
11
|
+
"build": "tsc -p tsconfig.build.json",
|
|
12
|
+
"prepublishOnly": "npm run build && echo '#!/usr/bin/env node' | cat - dist/bin/sanjang.js > dist/bin/_tmp && mv dist/bin/_tmp dist/bin/sanjang.js",
|
|
11
13
|
"test": "node --experimental-transform-types --test test/**/*.test.ts bin/__tests__/*.test.ts",
|
|
12
14
|
"typecheck": "tsc --noEmit",
|
|
13
15
|
"lint": "npx @biomejs/biome check lib/ test/ bin/"
|
|
@@ -15,7 +17,6 @@
|
|
|
15
17
|
"dependencies": {
|
|
16
18
|
"express": "^4.19.2",
|
|
17
19
|
"simple-git": "^3.27.0",
|
|
18
|
-
"user": "^0.0.0",
|
|
19
20
|
"ws": "^8.18.0"
|
|
20
21
|
},
|
|
21
22
|
"engines": {
|
|
@@ -33,8 +34,7 @@
|
|
|
33
34
|
"local-dev"
|
|
34
35
|
],
|
|
35
36
|
"files": [
|
|
36
|
-
"
|
|
37
|
-
"lib/",
|
|
37
|
+
"dist/",
|
|
38
38
|
"dashboard/",
|
|
39
39
|
"templates/"
|
|
40
40
|
],
|
|
@@ -1,42 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
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
|
-
}
|
package/lib/config.ts
DELETED
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import type { DetectedApp, DetectedProject, GenerateConfigResult, SanjangConfig } from "./types.ts";
|
|
5
|
-
import { deepFindEnvFiles, detectSetupIssues } from "./engine/smart-init.ts";
|
|
6
|
-
|
|
7
|
-
const CONFIG_FILE: string = "sanjang.config.js";
|
|
8
|
-
|
|
9
|
-
const DEFAULTS: SanjangConfig = {
|
|
10
|
-
dev: {
|
|
11
|
-
command: "npm run dev",
|
|
12
|
-
port: 3000,
|
|
13
|
-
portFlag: "--port",
|
|
14
|
-
cwd: ".",
|
|
15
|
-
env: {},
|
|
16
|
-
},
|
|
17
|
-
setup: null,
|
|
18
|
-
copyFiles: [],
|
|
19
|
-
backend: null,
|
|
20
|
-
ports: {
|
|
21
|
-
fe: { base: 3000, slots: 8 },
|
|
22
|
-
be: { base: 8000, slots: 8 },
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Load sanjang.config.js from project root.
|
|
28
|
-
* Returns merged config with defaults.
|
|
29
|
-
*/
|
|
30
|
-
export async function loadConfig(projectRoot: string): Promise<SanjangConfig> {
|
|
31
|
-
const configPath = join(projectRoot, CONFIG_FILE);
|
|
32
|
-
|
|
33
|
-
if (!existsSync(configPath)) {
|
|
34
|
-
console.warn("⚠️ sanjang.config.js를 찾을 수 없습니다. 기본 설정을 사용합니다.");
|
|
35
|
-
console.warn(" → sanjang init 으로 프로젝트에 맞는 설정을 생성하세요.");
|
|
36
|
-
return { ...DEFAULTS, _autoDetected: false };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const mod = await import(pathToFileURL(configPath).href);
|
|
41
|
-
const userConfig = mod.default || mod;
|
|
42
|
-
return mergeConfig(userConfig);
|
|
43
|
-
} catch (err) {
|
|
44
|
-
console.error(`sanjang.config.js 로드 실패: ${(err as Error).message}`);
|
|
45
|
-
return { ...DEFAULTS, _autoDetected: false };
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function mergeConfig(user: Record<string, unknown>): SanjangConfig {
|
|
50
|
-
const config: SanjangConfig = { ...DEFAULTS };
|
|
51
|
-
|
|
52
|
-
if (typeof user.dev === "string") {
|
|
53
|
-
config.dev = { ...DEFAULTS.dev, command: user.dev };
|
|
54
|
-
} else if (user.dev) {
|
|
55
|
-
config.dev = { ...DEFAULTS.dev, ...(user.dev as Partial<SanjangConfig["dev"]>) };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (user.setup) config.setup = user.setup as string;
|
|
59
|
-
if (user.copyFiles) config.copyFiles = user.copyFiles as string[];
|
|
60
|
-
if (user.backend) config.backend = user.backend as SanjangConfig["backend"];
|
|
61
|
-
if (user.ports) {
|
|
62
|
-
const userPorts = user.ports as Partial<SanjangConfig["ports"]>;
|
|
63
|
-
config.ports = {
|
|
64
|
-
fe: { ...DEFAULTS.ports.fe, ...userPorts.fe },
|
|
65
|
-
be: { ...DEFAULTS.ports.be, ...userPorts.be },
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return config;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Auto-detect project type and generate config.
|
|
74
|
-
*/
|
|
75
|
-
export function detectProject(projectRoot: string): DetectedProject {
|
|
76
|
-
const has = (f: string): boolean => existsSync(join(projectRoot, f));
|
|
77
|
-
const readJson = (f: string): Record<string, unknown> | null => {
|
|
78
|
-
try {
|
|
79
|
-
return JSON.parse(readFileSync(join(projectRoot, f), "utf8")) as Record<string, unknown>;
|
|
80
|
-
} catch {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// Framework detection
|
|
86
|
-
if (has("next.config.js") || has("next.config.mjs") || has("next.config.ts")) {
|
|
87
|
-
return {
|
|
88
|
-
framework: "Next.js",
|
|
89
|
-
dev: { command: "npx next dev", port: 3000, portFlag: "-p", cwd: ".", env: {} },
|
|
90
|
-
setup: has("bun.lockb") ? "bun install" : has("pnpm-lock.yaml") ? "pnpm install" : "npm install",
|
|
91
|
-
copyFiles: findEnvFiles(projectRoot),
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (has("nuxt.config.js") || has("nuxt.config.ts")) {
|
|
96
|
-
return {
|
|
97
|
-
framework: "Nuxt",
|
|
98
|
-
dev: { command: "npx nuxt dev", port: 3000, portFlag: "--port", cwd: ".", env: {} },
|
|
99
|
-
setup: detectPackageManager(projectRoot),
|
|
100
|
-
copyFiles: findEnvFiles(projectRoot),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (has("svelte.config.js") || has("svelte.config.ts")) {
|
|
105
|
-
return {
|
|
106
|
-
framework: "SvelteKit",
|
|
107
|
-
dev: { command: "npx vite dev", port: 5173, portFlag: "--port", cwd: ".", env: {} },
|
|
108
|
-
setup: detectPackageManager(projectRoot),
|
|
109
|
-
copyFiles: findEnvFiles(projectRoot),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (has("angular.json")) {
|
|
114
|
-
return {
|
|
115
|
-
framework: "Angular",
|
|
116
|
-
dev: { command: "npx ng serve", port: 4200, portFlag: "--port", cwd: ".", env: {} },
|
|
117
|
-
setup: "npm install",
|
|
118
|
-
copyFiles: findEnvFiles(projectRoot),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (has("vite.config.js") || has("vite.config.ts") || has("vite.config.mjs")) {
|
|
123
|
-
return {
|
|
124
|
-
framework: "Vite",
|
|
125
|
-
dev: { command: "npx vite dev", port: 5173, portFlag: "--port", cwd: ".", env: {} },
|
|
126
|
-
setup: detectPackageManager(projectRoot),
|
|
127
|
-
copyFiles: findEnvFiles(projectRoot),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ClojureScript / shadow-cljs (root or common subdirectories)
|
|
132
|
-
const shadowDirs = [".", "frontend", "client", "web", "app"];
|
|
133
|
-
for (const dir of shadowDirs) {
|
|
134
|
-
const prefix = dir === "." ? "" : `${dir}/`;
|
|
135
|
-
if (has(`${prefix}shadow-cljs.edn`)) {
|
|
136
|
-
const hasBb = has(`${prefix}bb.edn`);
|
|
137
|
-
return {
|
|
138
|
-
framework: "shadow-cljs",
|
|
139
|
-
dev: { command: hasBb ? "bb dev" : "npx shadow-cljs watch app", port: 3000, portFlag: null, cwd: dir, env: {} },
|
|
140
|
-
setup: "npm install",
|
|
141
|
-
copyFiles: findEnvFiles(projectRoot),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Monorepo detection
|
|
147
|
-
if (has("turbo.json")) {
|
|
148
|
-
const mainApp = detectTurboMainApp(projectRoot);
|
|
149
|
-
const filter = mainApp ? ` --filter=${mainApp.name}` : "";
|
|
150
|
-
const port = mainApp?.port ?? 3000;
|
|
151
|
-
return {
|
|
152
|
-
framework: "Turborepo",
|
|
153
|
-
dev: { command: `npx turbo run dev${filter}`, port, portFlag: null, cwd: ".", env: {} },
|
|
154
|
-
setup: detectPackageManager(projectRoot),
|
|
155
|
-
copyFiles: findEnvFiles(projectRoot),
|
|
156
|
-
_note: mainApp
|
|
157
|
-
? `Turborepo: filtered to ${mainApp.name} (port ${port}).`
|
|
158
|
-
: "Turborepo detected. You may need to adjust the dev command to filter a specific app.",
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Fallback: package.json scripts
|
|
163
|
-
const pkg = readJson("package.json");
|
|
164
|
-
if ((pkg?.scripts as Record<string, unknown> | undefined)?.dev) {
|
|
165
|
-
return {
|
|
166
|
-
framework: "Node.js",
|
|
167
|
-
dev: { command: "npm run dev", port: 3000, portFlag: "--port", cwd: ".", env: {} },
|
|
168
|
-
setup: detectPackageManager(projectRoot),
|
|
169
|
-
copyFiles: findEnvFiles(projectRoot),
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
framework: "unknown",
|
|
175
|
-
dev: { command: "npm run dev", port: 3000, portFlag: "--port", cwd: ".", env: {} },
|
|
176
|
-
setup: "npm install",
|
|
177
|
-
copyFiles: [],
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Scan first-level subdirectories for app candidates.
|
|
183
|
-
* Returns array of { dir, framework, detected } sorted by dir name.
|
|
184
|
-
*/
|
|
185
|
-
export function detectApps(projectRoot: string): DetectedApp[] {
|
|
186
|
-
const entries = readdirSync(projectRoot, { withFileTypes: true });
|
|
187
|
-
const ignore = new Set(["node_modules", ".git", ".sanjang", "dist", "build", ".next", ".nuxt"]);
|
|
188
|
-
|
|
189
|
-
const apps: DetectedApp[] = [];
|
|
190
|
-
for (const entry of entries) {
|
|
191
|
-
if (!entry.isDirectory()) continue;
|
|
192
|
-
if (entry.name.startsWith(".") || ignore.has(entry.name)) continue;
|
|
193
|
-
|
|
194
|
-
const subPath = join(projectRoot, entry.name);
|
|
195
|
-
const detected = detectProject(subPath);
|
|
196
|
-
if (detected.framework === "unknown") continue;
|
|
197
|
-
|
|
198
|
-
apps.push({
|
|
199
|
-
dir: entry.name,
|
|
200
|
-
framework: detected.framework,
|
|
201
|
-
detected,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return apps.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
interface TurboAppInfo {
|
|
209
|
-
name: string;
|
|
210
|
-
port: number;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function detectTurboMainApp(root: string): TurboAppInfo | null {
|
|
214
|
-
// Scan apps/*/package.json for the main app (has dev script with --port or vite/next)
|
|
215
|
-
const appDirs = ["apps", "packages"];
|
|
216
|
-
const candidates: TurboAppInfo[] = [];
|
|
217
|
-
|
|
218
|
-
for (const dir of appDirs) {
|
|
219
|
-
const base = join(root, dir);
|
|
220
|
-
if (!existsSync(base)) continue;
|
|
221
|
-
let entries;
|
|
222
|
-
try { entries = readdirSync(base, { withFileTypes: true }); } catch { continue; }
|
|
223
|
-
for (const entry of entries) {
|
|
224
|
-
if (!entry.isDirectory()) continue;
|
|
225
|
-
// Skip storybook, demo, docs apps
|
|
226
|
-
if (/storybook|demo|docs|e2e|test/i.test(entry.name)) continue;
|
|
227
|
-
const pkgPath = join(base, entry.name, "package.json");
|
|
228
|
-
if (!existsSync(pkgPath)) continue;
|
|
229
|
-
try {
|
|
230
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
|
|
231
|
-
const scripts = pkg.scripts as Record<string, string> | undefined;
|
|
232
|
-
const devScript = scripts?.dev;
|
|
233
|
-
if (!devScript) continue;
|
|
234
|
-
const portMatch = devScript.match(/--port\s+(\d+)/);
|
|
235
|
-
const port = portMatch?.[1] ? parseInt(portMatch[1], 10) : 3000;
|
|
236
|
-
candidates.push({ name: entry.name, port });
|
|
237
|
-
} catch { continue; }
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Prefer app with explicit port, then first candidate
|
|
242
|
-
return candidates.find(c => c.port !== 3000) ?? candidates[0] ?? null;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function detectPackageManager(root: string): string {
|
|
246
|
-
if (existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock"))) return "bun install";
|
|
247
|
-
if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm install";
|
|
248
|
-
if (existsSync(join(root, "yarn.lock"))) return "yarn install";
|
|
249
|
-
return "npm install";
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function findEnvFiles(root: string): string[] {
|
|
253
|
-
const envFiles = [".env", ".env.local", ".env.development", ".env.development.local"];
|
|
254
|
-
return envFiles.filter((f) => existsSync(join(root, f)));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export function generateConfig(
|
|
258
|
-
projectRoot: string,
|
|
259
|
-
options: { appDir?: string; force?: boolean } = {},
|
|
260
|
-
): GenerateConfigResult {
|
|
261
|
-
const { appDir, force } = options;
|
|
262
|
-
const configPath = join(projectRoot, CONFIG_FILE);
|
|
263
|
-
|
|
264
|
-
if (existsSync(configPath) && !force) {
|
|
265
|
-
return { created: false, message: "sanjang.config.js already exists." };
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Detect from selected app subdirectory or root
|
|
269
|
-
const detectRoot = appDir ? join(projectRoot, appDir) : projectRoot;
|
|
270
|
-
const detected = detectProject(detectRoot);
|
|
271
|
-
|
|
272
|
-
// Override cwd and setup for subdirectory apps
|
|
273
|
-
if (appDir && appDir !== ".") {
|
|
274
|
-
detected.dev.cwd = appDir;
|
|
275
|
-
if (detected.setup) {
|
|
276
|
-
detected.setup = `cd '${appDir.replace(/'/g, "'\\''")}' && ${detected.setup}`;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Smart env detection — scan entire project tree, not just root
|
|
281
|
-
detected.copyFiles = deepFindEnvFiles(projectRoot).filter(
|
|
282
|
-
// Exclude .env.example, .env.test, .env.template
|
|
283
|
-
(f) => !f.includes("example") && !f.includes("template") && !f.includes(".test"),
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
// Detect potential issues
|
|
287
|
-
const issues = detectSetupIssues(detectRoot);
|
|
288
|
-
|
|
289
|
-
const lines = [
|
|
290
|
-
"export default {",
|
|
291
|
-
` // ${detected.framework} detected`,
|
|
292
|
-
"",
|
|
293
|
-
" // Dev server command",
|
|
294
|
-
" dev: {",
|
|
295
|
-
` command: '${detected.dev.command}',`,
|
|
296
|
-
` port: ${detected.dev.port},`,
|
|
297
|
-
` portFlag: ${detected.dev.portFlag ? `'${detected.dev.portFlag}'` : "null"},`,
|
|
298
|
-
` cwd: '${detected.dev.cwd}',`,
|
|
299
|
-
" },",
|
|
300
|
-
"",
|
|
301
|
-
];
|
|
302
|
-
|
|
303
|
-
if (detected.setup) {
|
|
304
|
-
lines.push(` // Install dependencies after creating a camp`);
|
|
305
|
-
lines.push(` setup: ${JSON.stringify(detected.setup)},`);
|
|
306
|
-
lines.push("");
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (detected.copyFiles.length) {
|
|
310
|
-
lines.push(" // Copy gitignored files from main repo");
|
|
311
|
-
lines.push(` copyFiles: ${JSON.stringify(detected.copyFiles)},`);
|
|
312
|
-
lines.push("");
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (detected._note) {
|
|
316
|
-
lines.push(` // NOTE: ${detected._note}`);
|
|
317
|
-
lines.push("");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
lines.push(" // (optional) Backend server");
|
|
321
|
-
lines.push(" // backend: {");
|
|
322
|
-
lines.push(" // command: 'npm run start:api',");
|
|
323
|
-
lines.push(" // port: 8000,");
|
|
324
|
-
lines.push(" // healthCheck: '/health',");
|
|
325
|
-
lines.push(" // },");
|
|
326
|
-
lines.push("};");
|
|
327
|
-
lines.push("");
|
|
328
|
-
|
|
329
|
-
writeFileSync(configPath, lines.join("\n"), "utf8");
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
created: true,
|
|
333
|
-
framework: detected.framework,
|
|
334
|
-
configPath,
|
|
335
|
-
message: `sanjang.config.js created (${detected.framework} detected).`,
|
|
336
|
-
};
|
|
337
|
-
}
|