@gbdx/devis 1.0.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 +661 -0
- package/README.md +201 -0
- package/dist/cli/index.js +2217 -0
- package/dist/ui/assets/index-B3EEPtRc.js +33294 -0
- package/dist/ui/assets/index-BDxL9RBN.css +8892 -0
- package/dist/ui/index.html +31 -0
- package/package.json +78 -0
- package/schema/devis.schema.json +49 -0
- package/scripts/install.mjs +137 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="ko">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Devis</title>
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-B3EEPtRc.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BDxL9RBN.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
<script>
|
|
13
|
+
// 진단용 — webview 는 콘솔 에러를 로그파일에 남기지 않으므로,
|
|
14
|
+
// 초기화/렌더 중 발생한 에러를 화면에 직접 그린다.
|
|
15
|
+
(() => {
|
|
16
|
+
function show(label, detail) {
|
|
17
|
+
const el = document.getElementById('root')
|
|
18
|
+
if (!el) return
|
|
19
|
+
el.innerHTML = `<pre style="white-space:pre-wrap;padding:16px;font:13px monospace;color:#c00;">${label}\n\n${detail}</pre>`
|
|
20
|
+
}
|
|
21
|
+
window.addEventListener('error', (e) => {
|
|
22
|
+
show(`[error] ${e.message || ''}`, e.error?.stack || `${e.filename}:${e.lineno}`)
|
|
23
|
+
})
|
|
24
|
+
window.addEventListener('unhandledrejection', (e) => {
|
|
25
|
+
const r = e.reason
|
|
26
|
+
show('[unhandledrejection]', r?.stack || String(r))
|
|
27
|
+
})
|
|
28
|
+
})()
|
|
29
|
+
</script>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gbdx/devis",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Local development dashboard for pnpm monorepos — workspace scanning, process management, HTTPS proxy, Docker, and more.",
|
|
5
|
+
"license": "AGPL-3.0-or-later",
|
|
6
|
+
"author": "gibigspub@gmail.com",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"devis": "./dist/cli/index.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"schema",
|
|
17
|
+
"scripts/install.mjs",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=24.0.0"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"dev",
|
|
29
|
+
"monorepo",
|
|
30
|
+
"pnpm",
|
|
31
|
+
"workspace",
|
|
32
|
+
"dashboard",
|
|
33
|
+
"pm2",
|
|
34
|
+
"caddy",
|
|
35
|
+
"https",
|
|
36
|
+
"local-development"
|
|
37
|
+
],
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^24.12.4",
|
|
40
|
+
"@types/react": "^19.2.14",
|
|
41
|
+
"@types/react-dom": "^19.2.3",
|
|
42
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
43
|
+
"tsup": "^8.5.0",
|
|
44
|
+
"tsx": "^4.22.0",
|
|
45
|
+
"typescript": "^6.0.0",
|
|
46
|
+
"vite": "^6.4.2"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@mantine/core": "^9.2.1",
|
|
50
|
+
"@mantine/hooks": "^9.2.1",
|
|
51
|
+
"@mantine/modals": "^9.2.1",
|
|
52
|
+
"@mantine/notifications": "^9.2.1",
|
|
53
|
+
"@neutralinojs/lib": "^6.7.0",
|
|
54
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
55
|
+
"@tabler/icons-react": "^3.44.0",
|
|
56
|
+
"@tanstack/react-virtual": "^3.13.24",
|
|
57
|
+
"anser": "^2.3.5",
|
|
58
|
+
"devcert": "^1.2.2",
|
|
59
|
+
"jsonc-parser": "^3.3.1",
|
|
60
|
+
"pm2": "^5.4.3",
|
|
61
|
+
"react": "^19.2.6",
|
|
62
|
+
"react-dom": "^19.2.6",
|
|
63
|
+
"zod": "^3.23.8",
|
|
64
|
+
"@inquirer/prompts": "^8.4.3",
|
|
65
|
+
"get-port": "^7.2.0"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"dev": "vite",
|
|
69
|
+
"dev:app": "node dist/cli/index.js dev",
|
|
70
|
+
"build:ui": "vite build",
|
|
71
|
+
"build:cli": "tsup",
|
|
72
|
+
"build": "pnpm run build:ui && pnpm run build:cli",
|
|
73
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
74
|
+
"lint": "biome check .",
|
|
75
|
+
"postinstall": "node scripts/install.mjs",
|
|
76
|
+
"release": "pnpm build && pnpm pack --pack-destination .release"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "devis.schema.json",
|
|
4
|
+
"title": "Devis workspace config",
|
|
5
|
+
"description": "devis.config.jsonc / devis.config.local.jsonc 에서 작성하는 설정. 모든 필드는 선택 사항이며, 생략 시 워크스페이스 정보 기반 기본값이 적용된다.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "JSON Schema 참조 — VS Code 의 자동완성·검증을 위해 devis 가 생성한 캐시 파일을 가리킨다."
|
|
12
|
+
},
|
|
13
|
+
"workspaceSlug": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"minLength": 1,
|
|
16
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$",
|
|
17
|
+
"description": "Caddy 설정 트리의 @id 접두어이자 pm2 namespace 로 쓰이는 워크스페이스 식별자. devis 가 관리하는 모든 엔트리가 `<workspaceSlug>:route:<app>` 처럼 이 prefix 로 식별·범위 한정된다. 기본값: 루트 package.json name → 디렉터리명 순으로 추론 (@scope/foo → scope-foo)."
|
|
18
|
+
},
|
|
19
|
+
"caddyAdmin": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"format": "uri",
|
|
22
|
+
"description": "Caddy Admin API 주소. 기본값: http://localhost:2019"
|
|
23
|
+
},
|
|
24
|
+
"internalDomain": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "내부(루프백) HTTPS 베이스 도메인. 예: myapp.lvh.me. 이 도메인은 127.0.0.1 로 해석되도록 구성돼 있어야 한다. 기본값: `<workspaceSlug>.lvh.me` — lvh.me 는 공개 DNS 가 *.lvh.me → 127.0.0.1 을 응답하므로 /etc/hosts 수정 없이 동작한다."
|
|
27
|
+
},
|
|
28
|
+
"publicDomain": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "외부 접근용 퍼블릭 도메인. 설정 시 `<app>.<publicDomain>` 사이트가 추가로 등록되고 ACME(Let's Encrypt) 인증서가 발급된다."
|
|
31
|
+
},
|
|
32
|
+
"tls": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"description": "기존 로컬 인증서. 설정 시 devcert CA 대신 이 인증서를 사용한다.",
|
|
35
|
+
"additionalProperties": false,
|
|
36
|
+
"required": ["cert", "key"],
|
|
37
|
+
"properties": {
|
|
38
|
+
"cert": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "PEM 인증서 절대경로."
|
|
41
|
+
},
|
|
42
|
+
"key": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "PEM 개인키 절대경로."
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, spawnSync } from 'node:child_process'
|
|
3
|
+
/**
|
|
4
|
+
* postinstall — caddy 바이너리를 GitHub Releases 에서 다운로드한다.
|
|
5
|
+
*
|
|
6
|
+
* pnpm i -D devis 실행 시 자동으로 호출된다.
|
|
7
|
+
* 이미 .bin/caddy 가 있거나 시스템 caddy 가 발견되면 스킵한다.
|
|
8
|
+
* DEVIS_SKIP_CADDY=1 환경변수로 다운로드를 건너뛸 수 있다.
|
|
9
|
+
*/
|
|
10
|
+
import { chmodSync, createWriteStream, existsSync, mkdirSync, unlinkSync } from 'node:fs'
|
|
11
|
+
import { get } from 'node:https'
|
|
12
|
+
import { dirname, resolve } from 'node:path'
|
|
13
|
+
import { pipeline } from 'node:stream/promises'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
17
|
+
const PKG_DIR = resolve(__dirname, '..')
|
|
18
|
+
const BIN_DIR = resolve(PKG_DIR, '.bin')
|
|
19
|
+
const CADDY_VERSION = '2.11.3'
|
|
20
|
+
|
|
21
|
+
const platform = process.platform
|
|
22
|
+
const arch = process.arch
|
|
23
|
+
const platformMap = { darwin: 'mac', linux: 'linux', win32: 'windows' }
|
|
24
|
+
const archMap = { x64: 'amd64', arm64: 'arm64' }
|
|
25
|
+
|
|
26
|
+
const osPart = platformMap[platform]
|
|
27
|
+
const archPart = archMap[arch]
|
|
28
|
+
|
|
29
|
+
if (process.env.DEVIS_SKIP_CADDY === '1') {
|
|
30
|
+
process.exit(0)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!osPart || !archPart) {
|
|
34
|
+
console.warn(
|
|
35
|
+
`[devis] caddy 자동 설치: 지원하지 않는 플랫폼 (${platform}/${arch}). mise 또는 brew로 caddy를 설치하세요.`
|
|
36
|
+
)
|
|
37
|
+
process.exit(0)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ext = platform === 'win32' ? '.exe' : ''
|
|
41
|
+
const caddyBin = resolve(BIN_DIR, `caddy${ext}`)
|
|
42
|
+
|
|
43
|
+
// 이미 설치돼 있으면 스킵
|
|
44
|
+
if (existsSync(caddyBin)) {
|
|
45
|
+
process.exit(0)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 시스템 caddy 가 있으면 번들 다운로드 스킵
|
|
49
|
+
try {
|
|
50
|
+
execSync('caddy version', { stdio: 'ignore' })
|
|
51
|
+
console.log('[devis] 시스템 caddy 발견 — 번들 다운로드를 건너뜁니다.')
|
|
52
|
+
process.exit(0)
|
|
53
|
+
} catch {
|
|
54
|
+
// 없으면 다운로드 진행
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
mkdirSync(BIN_DIR, { recursive: true })
|
|
58
|
+
|
|
59
|
+
// caddy 공식 릴리즈 URL
|
|
60
|
+
// 예: caddy_2.9.1_darwin_arm64.tar.gz
|
|
61
|
+
const archiveName = `caddy_${CADDY_VERSION}_${osPart}_${archPart}${platform === 'win32' ? '.zip' : '.tar.gz'}`
|
|
62
|
+
const downloadUrl = `https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/${archiveName}`
|
|
63
|
+
|
|
64
|
+
console.log(`[devis] caddy v${CADDY_VERSION} (${osPart}/${archPart}) 다운로드 중...`)
|
|
65
|
+
console.log(`[devis] ${downloadUrl}`)
|
|
66
|
+
|
|
67
|
+
/** HTTPS GET — 리다이렉트를 따라간다. */
|
|
68
|
+
function httpsGet(url) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
get(url, (res) => {
|
|
71
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
72
|
+
httpsGet(res.headers.location).then(resolve, reject)
|
|
73
|
+
res.resume()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (res.statusCode !== 200) {
|
|
77
|
+
reject(new Error(`HTTP ${res.statusCode}`))
|
|
78
|
+
res.resume()
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
resolve(res)
|
|
82
|
+
}).on('error', reject)
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** tar.gz 스트림을 임시 파일로 저장한 뒤 시스템 tar 로 caddy 를 추출한다. */
|
|
87
|
+
async function extractCaddyFromTarGz(stream) {
|
|
88
|
+
const tmpArchive = caddyBin + '.tar.gz.tmp'
|
|
89
|
+
const fileOut = createWriteStream(tmpArchive)
|
|
90
|
+
await pipeline(stream, fileOut)
|
|
91
|
+
|
|
92
|
+
const result = spawnSync('tar', ['-xzf', tmpArchive, '-C', BIN_DIR, 'caddy'], { stdio: 'inherit' })
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
unlinkSync(tmpArchive)
|
|
96
|
+
} catch {}
|
|
97
|
+
|
|
98
|
+
if (result.status !== 0) {
|
|
99
|
+
throw new Error('tar 추출 실패')
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function main() {
|
|
104
|
+
try {
|
|
105
|
+
const res = await httpsGet(downloadUrl)
|
|
106
|
+
|
|
107
|
+
if (platform === 'win32') {
|
|
108
|
+
const tmpZip = caddyBin + '.zip.tmp'
|
|
109
|
+
const fileOut = createWriteStream(tmpZip)
|
|
110
|
+
await pipeline(res, fileOut)
|
|
111
|
+
|
|
112
|
+
spawnSync('powershell', ['-Command', `Expand-Archive -Path '${tmpZip}' -DestinationPath '${BIN_DIR}' -Force`], {
|
|
113
|
+
stdio: 'inherit',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
unlinkSync(tmpZip)
|
|
118
|
+
} catch {}
|
|
119
|
+
} else {
|
|
120
|
+
await extractCaddyFromTarGz(res)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (existsSync(caddyBin)) {
|
|
124
|
+
chmodSync(caddyBin, 0o755)
|
|
125
|
+
console.log(`[devis] caddy 설치 완료: ${caddyBin}`)
|
|
126
|
+
} else {
|
|
127
|
+
throw new Error('추출 후 바이너리를 찾을 수 없습니다.')
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.warn(`[devis] caddy 자동 설치 실패: ${err.message}`)
|
|
131
|
+
console.warn('[devis] 수동 설치: https://caddyserver.com/docs/install')
|
|
132
|
+
console.warn('[devis] 또는: brew install caddy / mise use -g caddy')
|
|
133
|
+
process.exit(0)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
main()
|