@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,2217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// cli/preload-env.ts
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
if (!process.env.PM2_HOME) {
|
|
7
|
+
const devisHome2 = process.env.DEVIS_HOME || join(homedir(), ".devis");
|
|
8
|
+
process.env.PM2_HOME = join(devisHome2, "pm2");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// cli/commands.ts
|
|
12
|
+
import { execFileSync as execFileSync5, spawn as spawn3 } from "child_process";
|
|
13
|
+
import { existsSync as existsSync10 } from "fs";
|
|
14
|
+
import { join as join12 } from "path";
|
|
15
|
+
|
|
16
|
+
// cli/app/build.ts
|
|
17
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
18
|
+
import {
|
|
19
|
+
chmodSync as chmodSync2,
|
|
20
|
+
copyFileSync,
|
|
21
|
+
cpSync,
|
|
22
|
+
existsSync as existsSync2,
|
|
23
|
+
mkdirSync as mkdirSync3,
|
|
24
|
+
readFileSync,
|
|
25
|
+
rmSync as rmSync2,
|
|
26
|
+
utimesSync,
|
|
27
|
+
writeFileSync as writeFileSync2
|
|
28
|
+
} from "fs";
|
|
29
|
+
import { join as join5 } from "path";
|
|
30
|
+
|
|
31
|
+
// cli/global/paths.ts
|
|
32
|
+
import { homedir as homedir2 } from "os";
|
|
33
|
+
import { join as join2 } from "path";
|
|
34
|
+
function devisHome() {
|
|
35
|
+
return process.env.DEVIS_HOME || join2(homedir2(), ".devis");
|
|
36
|
+
}
|
|
37
|
+
function workspacesDir() {
|
|
38
|
+
return join2(devisHome(), "workspaces");
|
|
39
|
+
}
|
|
40
|
+
function workspaceDir(slug) {
|
|
41
|
+
return join2(workspacesDir(), slug);
|
|
42
|
+
}
|
|
43
|
+
function workspaceMetaFile(slug) {
|
|
44
|
+
return join2(workspaceDir(slug), "workspace.json");
|
|
45
|
+
}
|
|
46
|
+
function activeFile() {
|
|
47
|
+
return join2(devisHome(), "active");
|
|
48
|
+
}
|
|
49
|
+
function lockFile() {
|
|
50
|
+
return join2(devisHome(), "lock");
|
|
51
|
+
}
|
|
52
|
+
function appCacheDir() {
|
|
53
|
+
return join2(devisHome(), "app");
|
|
54
|
+
}
|
|
55
|
+
function pm2Home() {
|
|
56
|
+
return join2(devisHome(), "pm2");
|
|
57
|
+
}
|
|
58
|
+
function logsDir() {
|
|
59
|
+
return join2(devisHome(), "logs");
|
|
60
|
+
}
|
|
61
|
+
function caddyHome() {
|
|
62
|
+
return join2(devisHome(), "caddy");
|
|
63
|
+
}
|
|
64
|
+
function caddyDataDir() {
|
|
65
|
+
return join2(caddyHome(), "data");
|
|
66
|
+
}
|
|
67
|
+
function caddyBootstrapFile() {
|
|
68
|
+
return join2(caddyHome(), "bootstrap.json");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// cli/app/icon.ts
|
|
72
|
+
import { execFileSync } from "child_process";
|
|
73
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
74
|
+
import { tmpdir } from "os";
|
|
75
|
+
import { join as join3 } from "path";
|
|
76
|
+
import { Resvg } from "@resvg/resvg-js";
|
|
77
|
+
var TERMINAL_GREEN = "#5af78e";
|
|
78
|
+
var FRAME_STROKE = "rgba(220,220,220,0.55)";
|
|
79
|
+
var BEZEL = "#000000";
|
|
80
|
+
var INNER_BG = "#2e2e2e";
|
|
81
|
+
function iconSvg(size) {
|
|
82
|
+
const pad = Math.round(size * 0.05);
|
|
83
|
+
const inner = size - pad * 2;
|
|
84
|
+
const radius = Math.round(inner * 0.22);
|
|
85
|
+
const stroke = Math.max(2, Math.round(inner * 0.042));
|
|
86
|
+
const bezel = Math.max(2, Math.round(inner * 0.06));
|
|
87
|
+
const outerInset = stroke / 2;
|
|
88
|
+
const innerInset = stroke + bezel;
|
|
89
|
+
const innerRadius = Math.max(0, radius - innerInset);
|
|
90
|
+
const labelSize = Math.round(inner * 0.18);
|
|
91
|
+
const labelX = pad + innerInset + Math.round(inner * 0.06);
|
|
92
|
+
const labelY = pad + innerInset + Math.round(inner * 0.08);
|
|
93
|
+
const nameSize = Math.round(inner * 0.19);
|
|
94
|
+
const nameX = size / 2;
|
|
95
|
+
const nameY = pad + innerInset + Math.round((inner - innerInset * 2) * 0.73);
|
|
96
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">
|
|
97
|
+
<rect x="${pad + outerInset}" y="${pad + outerInset}" width="${inner - stroke}" height="${inner - stroke}"
|
|
98
|
+
rx="${radius - outerInset}" ry="${radius - outerInset}"
|
|
99
|
+
fill="${BEZEL}" stroke="${FRAME_STROKE}" stroke-width="${stroke}"/>
|
|
100
|
+
<rect x="${pad + innerInset}" y="${pad + innerInset}" width="${inner - innerInset * 2}" height="${inner - innerInset * 2}"
|
|
101
|
+
rx="${innerRadius}" ry="${innerRadius}" fill="${INNER_BG}"/>
|
|
102
|
+
<text x="${labelX}" y="${labelY}" text-anchor="start" dominant-baseline="hanging"
|
|
103
|
+
font-family="Menlo, Consolas, 'SF Mono', monospace" font-weight="600"
|
|
104
|
+
font-size="${labelSize}" fill="${TERMINAL_GREEN}">>_</text>
|
|
105
|
+
<text x="${nameX}" y="${nameY}" text-anchor="middle" dominant-baseline="auto"
|
|
106
|
+
font-family="Menlo, Consolas, 'SF Mono', monospace" font-weight="600"
|
|
107
|
+
font-size="${nameSize}" fill="#ffffff">Devis</text>
|
|
108
|
+
</svg>`;
|
|
109
|
+
}
|
|
110
|
+
function renderPng(size) {
|
|
111
|
+
return new Resvg(iconSvg(size), { fitTo: { mode: "width", value: size } }).render().asPng();
|
|
112
|
+
}
|
|
113
|
+
var MAC_ICONSET = [
|
|
114
|
+
[16, "icon_16x16.png"],
|
|
115
|
+
[32, "icon_16x16@2x.png"],
|
|
116
|
+
[32, "icon_32x32.png"],
|
|
117
|
+
[64, "icon_32x32@2x.png"],
|
|
118
|
+
[128, "icon_128x128.png"],
|
|
119
|
+
[256, "icon_128x128@2x.png"],
|
|
120
|
+
[256, "icon_256x256.png"],
|
|
121
|
+
[512, "icon_256x256@2x.png"],
|
|
122
|
+
[512, "icon_512x512.png"],
|
|
123
|
+
[1024, "icon_512x512@2x.png"]
|
|
124
|
+
];
|
|
125
|
+
function generateIcons(input) {
|
|
126
|
+
writeFileSync(join3(input.resourcesDir, "icon.png"), renderPng(256));
|
|
127
|
+
const out = { windowIconUrl: "/resources/icon.png" };
|
|
128
|
+
if (process.platform === "darwin" && input.appBundle) {
|
|
129
|
+
const tmpRoot = mkdtempSync(join3(tmpdir(), "dt-icons-"));
|
|
130
|
+
try {
|
|
131
|
+
const iconset = join3(tmpRoot, "AppIcon.iconset");
|
|
132
|
+
mkdirSync(iconset, { recursive: true });
|
|
133
|
+
for (const [px, name] of MAC_ICONSET) {
|
|
134
|
+
writeFileSync(join3(iconset, name), renderPng(px));
|
|
135
|
+
}
|
|
136
|
+
const resDir = join3(input.appBundle, "Contents", "Resources");
|
|
137
|
+
mkdirSync(resDir, { recursive: true });
|
|
138
|
+
execFileSync("iconutil", ["-c", "icns", iconset, "-o", join3(resDir, "AppIcon.icns")], {
|
|
139
|
+
stdio: "ignore"
|
|
140
|
+
});
|
|
141
|
+
out.macIconName = "AppIcon";
|
|
142
|
+
} finally {
|
|
143
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
} else if (process.platform === "linux") {
|
|
146
|
+
const iconPath = join3(input.appCache, "icon.png");
|
|
147
|
+
writeFileSync(iconPath, renderPng(512));
|
|
148
|
+
out.linuxIconPath = iconPath;
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// cli/app/runtime.ts
|
|
154
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
155
|
+
import { chmodSync, createWriteStream, existsSync, mkdirSync as mkdirSync2, unlinkSync } from "fs";
|
|
156
|
+
import { get } from "https";
|
|
157
|
+
import { join as join4 } from "path";
|
|
158
|
+
import { pipeline } from "stream/promises";
|
|
159
|
+
var NEUTRALINO_VERSION = "6.7.0";
|
|
160
|
+
function neutralinoBinaryName() {
|
|
161
|
+
const { platform, arch } = process;
|
|
162
|
+
if (platform === "darwin") return arch === "arm64" ? "neutralino-mac_arm64" : "neutralino-mac_x64";
|
|
163
|
+
if (platform === "win32") return "neutralino-win_x64.exe";
|
|
164
|
+
if (platform === "linux") {
|
|
165
|
+
if (arch === "arm64") return "neutralino-linux_arm64";
|
|
166
|
+
if (arch === "arm") return "neutralino-linux_armhf";
|
|
167
|
+
return "neutralino-linux_x64";
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
function httpsGet(url) {
|
|
172
|
+
return new Promise((resolve3, reject) => {
|
|
173
|
+
get(url, (res) => {
|
|
174
|
+
const status2 = res.statusCode ?? 0;
|
|
175
|
+
if (status2 >= 300 && status2 < 400 && res.headers.location) {
|
|
176
|
+
res.resume();
|
|
177
|
+
httpsGet(res.headers.location).then(resolve3, reject);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (status2 !== 200) {
|
|
181
|
+
res.resume();
|
|
182
|
+
reject(new Error(`HTTP ${status2}`));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
resolve3(res);
|
|
186
|
+
}).on("error", reject);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async function downloadAndExtract(binName, cacheDir, binPath) {
|
|
190
|
+
const zipUrl = `https://github.com/neutralinojs/neutralinojs/releases/download/v${NEUTRALINO_VERSION}/neutralinojs-v${NEUTRALINO_VERSION}.zip`;
|
|
191
|
+
const tmpZip = join4(cacheDir, `neutralinojs-v${NEUTRALINO_VERSION}.zip`);
|
|
192
|
+
const res = await httpsGet(zipUrl);
|
|
193
|
+
await pipeline(res, createWriteStream(tmpZip));
|
|
194
|
+
if (process.platform === "win32") {
|
|
195
|
+
execFileSync2("tar", ["-xf", tmpZip, binName, "-C", cacheDir], { stdio: "ignore" });
|
|
196
|
+
} else {
|
|
197
|
+
execFileSync2("unzip", ["-o", "-j", tmpZip, binName, "-d", cacheDir], { stdio: "ignore" });
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
unlinkSync(tmpZip);
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
if (process.platform !== "win32") chmodSync(binPath, 493);
|
|
204
|
+
}
|
|
205
|
+
async function resolveNeutralinoBin(cacheBase) {
|
|
206
|
+
const binName = neutralinoBinaryName();
|
|
207
|
+
if (!binName) return null;
|
|
208
|
+
const cacheDir = join4(cacheBase, "neutralino", NEUTRALINO_VERSION);
|
|
209
|
+
const binPath = join4(cacheDir, binName);
|
|
210
|
+
if (existsSync(binPath)) return binPath;
|
|
211
|
+
mkdirSync2(cacheDir, { recursive: true });
|
|
212
|
+
console.log(" devis: \uB370\uC2A4\uD06C\uD1B1 \uB7F0\uD0C0\uC784(Neutralino) \uCD5C\uCD08 1\uD68C \uB2E4\uC6B4\uB85C\uB4DC \uC911...");
|
|
213
|
+
await downloadAndExtract(binName, cacheDir, binPath);
|
|
214
|
+
return existsSync(binPath) ? binPath : null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// cli/app/build.ts
|
|
218
|
+
var LSREGISTER = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
|
|
219
|
+
function refreshMacIconCache(appBundle) {
|
|
220
|
+
try {
|
|
221
|
+
execFileSync3(LSREGISTER, ["-f", appBundle], { stdio: "ignore" });
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const now = /* @__PURE__ */ new Date();
|
|
226
|
+
utimesSync(appBundle, now, now);
|
|
227
|
+
const icns = join5(appBundle, "Contents", "Resources", "AppIcon.icns");
|
|
228
|
+
if (existsSync2(icns)) utimesSync(icns, now, now);
|
|
229
|
+
} catch {
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
var APP_NAME = "Devis";
|
|
233
|
+
var BUNDLE_ID = "com.gbdx.devis";
|
|
234
|
+
function appBundlePath(devisRoot) {
|
|
235
|
+
const version = devisVersion(devisRoot);
|
|
236
|
+
const isMac = process.platform === "darwin";
|
|
237
|
+
return join5(appCacheDir(), version, isMac ? `${APP_NAME}.app` : APP_NAME);
|
|
238
|
+
}
|
|
239
|
+
function devisVersion(devisRoot) {
|
|
240
|
+
try {
|
|
241
|
+
const pkg = JSON.parse(readFileSync(join5(devisRoot, "package.json"), "utf8"));
|
|
242
|
+
return pkg.version ?? "0.0.0";
|
|
243
|
+
} catch {
|
|
244
|
+
return "0.0.0";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function neutralinoConfig(input, icons) {
|
|
248
|
+
const sourceConfig = input.devUrl ? { url: input.devUrl } : { documentRoot: "/resources/", url: "/index.html" };
|
|
249
|
+
return {
|
|
250
|
+
applicationId: BUNDLE_ID,
|
|
251
|
+
version: "1.0.0",
|
|
252
|
+
defaultMode: "window",
|
|
253
|
+
...sourceConfig,
|
|
254
|
+
enableServer: true,
|
|
255
|
+
enableNativeAPI: true,
|
|
256
|
+
logging: { enabled: true, writeToLogFile: true },
|
|
257
|
+
// 빈 포트 자동 선택. webview localStorage 는 origin(host:port) 에 묶이지만
|
|
258
|
+
// 사용자 설정(테마·로그 패널 사이즈 등) 은 Neutralino `storage` API 로
|
|
259
|
+
// 파일 영속화 (ui/storage.ts) — origin 변경과 무관하게 유지된다.
|
|
260
|
+
port: 0,
|
|
261
|
+
nativeAllowList: ["app.*", "os.*", "filesystem.*", "events.*", "window.*", "computer.*", "storage.*"],
|
|
262
|
+
/**
|
|
263
|
+
* SPA(ui/platform/neutralino.ts) 가 `window.NL_devis` 로 읽는 컨텍스트.
|
|
264
|
+
* 워크스페이스 무관 — SPA 가 `homeDir` 의 active/workspaces 를 직접 읽는다.
|
|
265
|
+
*/
|
|
266
|
+
globalVariables: {
|
|
267
|
+
devis: {
|
|
268
|
+
homeDir: devisHome(),
|
|
269
|
+
tools: input.tools,
|
|
270
|
+
pathEnv: input.pathEnv
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
modes: {
|
|
274
|
+
window: {
|
|
275
|
+
title: input.devUrl ? `${APP_NAME} [dev]` : APP_NAME,
|
|
276
|
+
icon: icons.windowIconUrl,
|
|
277
|
+
width: 1600,
|
|
278
|
+
height: 900,
|
|
279
|
+
minWidth: 900,
|
|
280
|
+
minHeight: 600,
|
|
281
|
+
center: true,
|
|
282
|
+
exitProcessOnClose: true,
|
|
283
|
+
injectGlobals: true,
|
|
284
|
+
enableInspector: true
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function infoPlist(iconName) {
|
|
290
|
+
const iconKey = iconName ? `
|
|
291
|
+
<key>CFBundleIconFile</key><string>${iconName}</string>` : "";
|
|
292
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
293
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
294
|
+
<plist version="1.0">
|
|
295
|
+
<dict>
|
|
296
|
+
<key>CFBundleExecutable</key><string>devis</string>
|
|
297
|
+
<key>CFBundleIdentifier</key><string>${BUNDLE_ID}</string>
|
|
298
|
+
<key>CFBundleName</key><string>${APP_NAME}</string>
|
|
299
|
+
<key>CFBundleDisplayName</key><string>${APP_NAME}</string>${iconKey}
|
|
300
|
+
<key>CFBundlePackageType</key><string>APPL</string>
|
|
301
|
+
<key>CFBundleVersion</key><string>1.0.0</string>
|
|
302
|
+
<key>CFBundleShortVersionString</key><string>1.0.0</string>
|
|
303
|
+
<key>NSHighResolutionCapable</key><true/>
|
|
304
|
+
<!-- exitProcessOnClose \uC885\uB8CC \uD50C\uB85C\uC6B0\uAC00 NSApp \uC815\uC2DD \uC808\uCC28\uB97C \uAC70\uCE58\uC9C0 \uC54A\uC544 macOS \uAC00
|
|
305
|
+
"force quit" \uC73C\uB85C \uC778\uC2DD\uD558\uB294 \uAC83\uC744 \uB9C9\uB294\uB2E4. -->
|
|
306
|
+
<key>NSQuitAlwaysKeepsWindows</key><false/>
|
|
307
|
+
</dict>
|
|
308
|
+
</plist>
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
function desktopEntry(execPath, iconPath) {
|
|
312
|
+
const iconLine = iconPath ? `
|
|
313
|
+
Icon=${iconPath}` : "";
|
|
314
|
+
return `[Desktop Entry]
|
|
315
|
+
Type=Application
|
|
316
|
+
Name=${APP_NAME}
|
|
317
|
+
Exec=${execPath}${iconLine}
|
|
318
|
+
Terminal=false
|
|
319
|
+
Categories=Development;
|
|
320
|
+
`;
|
|
321
|
+
}
|
|
322
|
+
async function buildApp(input) {
|
|
323
|
+
const version = devisVersion(input.devisRoot);
|
|
324
|
+
const appCache = join5(appCacheDir(), version);
|
|
325
|
+
const uiSrc = join5(input.devisRoot, "dist", "ui");
|
|
326
|
+
if (!input.devUrl && !existsSync2(join5(uiSrc, "index.html"))) {
|
|
327
|
+
throw new Error(`\uBE4C\uB4DC\uB41C UI \uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${uiSrc}
|
|
328
|
+
\uBA3C\uC800 devis \uB97C \uBE4C\uB4DC\uD558\uC138\uC694 (pnpm build).`);
|
|
329
|
+
}
|
|
330
|
+
const frameworkBin = await resolveNeutralinoBin(devisHome());
|
|
331
|
+
if (!frameworkBin) {
|
|
332
|
+
throw new Error(`\uC774 \uD50C\uB7AB\uD3FC(${process.platform}/${process.arch})\uC740 Neutralino \uB7F0\uD0C0\uC784\uC744 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`);
|
|
333
|
+
}
|
|
334
|
+
const isMac = process.platform === "darwin";
|
|
335
|
+
const isWin = process.platform === "win32";
|
|
336
|
+
const bundleDirName = isMac ? `${APP_NAME}.app` : APP_NAME;
|
|
337
|
+
const appBundle = join5(appCache, bundleDirName);
|
|
338
|
+
const binDir = isMac ? join5(appBundle, "Contents", "MacOS") : appBundle;
|
|
339
|
+
const binName = isWin ? "devis.exe" : "devis";
|
|
340
|
+
const execPath = join5(binDir, binName);
|
|
341
|
+
const resourcesDir = join5(binDir, "resources");
|
|
342
|
+
if (!existsSync2(execPath)) {
|
|
343
|
+
mkdirSync3(binDir, { recursive: true });
|
|
344
|
+
copyFileSync(frameworkBin, execPath);
|
|
345
|
+
if (!isWin) chmodSync2(execPath, 493);
|
|
346
|
+
}
|
|
347
|
+
rmSync2(resourcesDir, { recursive: true, force: true });
|
|
348
|
+
if (input.devUrl) {
|
|
349
|
+
mkdirSync3(resourcesDir, { recursive: true });
|
|
350
|
+
} else {
|
|
351
|
+
cpSync(uiSrc, resourcesDir, { recursive: true });
|
|
352
|
+
}
|
|
353
|
+
const icons = generateIcons({
|
|
354
|
+
appCache,
|
|
355
|
+
appBundle: isMac ? appBundle : void 0,
|
|
356
|
+
resourcesDir
|
|
357
|
+
});
|
|
358
|
+
writeFileSync2(join5(binDir, "neutralino.config.json"), `${JSON.stringify(neutralinoConfig(input, icons), null, 2)}
|
|
359
|
+
`);
|
|
360
|
+
if (isMac) {
|
|
361
|
+
writeFileSync2(join5(appBundle, "Contents", "Info.plist"), infoPlist(icons.macIconName));
|
|
362
|
+
refreshMacIconCache(appBundle);
|
|
363
|
+
} else if (process.platform === "linux") {
|
|
364
|
+
writeFileSync2(join5(appCache, "devis.desktop"), desktopEntry(execPath, icons.linuxIconPath));
|
|
365
|
+
}
|
|
366
|
+
return { execPath, appBundle: isMac ? appBundle : void 0 };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// cli/app/launch.ts
|
|
370
|
+
import { spawn } from "child_process";
|
|
371
|
+
function launchApp(app) {
|
|
372
|
+
const child = spawn(app.execPath, [], { detached: true, stdio: "ignore" });
|
|
373
|
+
child.unref();
|
|
374
|
+
return child.pid;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// cli/apps.ts
|
|
378
|
+
import { basename } from "path";
|
|
379
|
+
import { Separator, select } from "@inquirer/prompts";
|
|
380
|
+
|
|
381
|
+
// cli/projects.ts
|
|
382
|
+
import { execSync } from "child_process";
|
|
383
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
384
|
+
import { join as join7, sep } from "path";
|
|
385
|
+
|
|
386
|
+
// cli/workspace.ts
|
|
387
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
388
|
+
import { dirname, join as join6, resolve } from "path";
|
|
389
|
+
function findWorkspaceRoot(from = process.cwd()) {
|
|
390
|
+
let dir = resolve(from);
|
|
391
|
+
while (true) {
|
|
392
|
+
if (existsSync3(join6(dir, "pnpm-workspace.yaml"))) return dir;
|
|
393
|
+
const parent = dirname(dir);
|
|
394
|
+
if (parent === dir) return process.cwd();
|
|
395
|
+
dir = parent;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function findWorkspaceRootStrict(from = process.cwd()) {
|
|
399
|
+
let dir = resolve(from);
|
|
400
|
+
while (true) {
|
|
401
|
+
if (existsSync3(join6(dir, "pnpm-workspace.yaml"))) return dir;
|
|
402
|
+
const parent = dirname(dir);
|
|
403
|
+
if (parent === dir) return null;
|
|
404
|
+
dir = parent;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function findDevisRoot() {
|
|
408
|
+
let dir = import.meta.dirname;
|
|
409
|
+
while (true) {
|
|
410
|
+
if (existsSync3(join6(dir, "package.json"))) return dir;
|
|
411
|
+
const parent = dirname(dir);
|
|
412
|
+
if (parent === dir) return import.meta.dirname;
|
|
413
|
+
dir = parent;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function parseWorkspacePatterns(root) {
|
|
417
|
+
const yamlPath = join6(root, "pnpm-workspace.yaml");
|
|
418
|
+
let rawPatterns = [];
|
|
419
|
+
try {
|
|
420
|
+
const content = readFileSync2(yamlPath, "utf8");
|
|
421
|
+
const inPackages = /packages:\s*\n((?:\s*-\s*.+\n?)*)/;
|
|
422
|
+
const m = content.match(inPackages);
|
|
423
|
+
if (m) {
|
|
424
|
+
rawPatterns = m[1].split("\n").map((l) => l.replace(/^\s*-\s*/, "").trim()).filter(Boolean);
|
|
425
|
+
}
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
const categoryMap = /* @__PURE__ */ new Map();
|
|
429
|
+
for (const pattern of rawPatterns) {
|
|
430
|
+
const firstSegment = pattern.split("/")[0];
|
|
431
|
+
if (!firstSegment) continue;
|
|
432
|
+
const cat = inferCategory(firstSegment);
|
|
433
|
+
categoryMap.set(firstSegment, cat);
|
|
434
|
+
}
|
|
435
|
+
return { categoryMap, rawPatterns };
|
|
436
|
+
}
|
|
437
|
+
function inferCategory(segment) {
|
|
438
|
+
const lower = segment.toLowerCase();
|
|
439
|
+
if (["app", "apps", "service", "services", "site", "sites"].includes(lower)) return "app";
|
|
440
|
+
if (["module", "modules", "package", "packages", "lib", "libs", "shared"].includes(lower)) return "module";
|
|
441
|
+
return "infra";
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// cli/projects.ts
|
|
445
|
+
function makeCategorizer(root) {
|
|
446
|
+
const { categoryMap } = parseWorkspacePatterns(root);
|
|
447
|
+
return (path) => {
|
|
448
|
+
if (path === root) return "workspace";
|
|
449
|
+
const rel = path.startsWith(root + sep) ? path.slice(root.length + 1) : path;
|
|
450
|
+
const firstSegment = rel.split(sep)[0];
|
|
451
|
+
return categoryMap.get(firstSegment) ?? "infra";
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function readPackageInfo(dir) {
|
|
455
|
+
try {
|
|
456
|
+
const pkg = JSON.parse(readFileSync3(join7(dir, "package.json"), "utf8"));
|
|
457
|
+
return {
|
|
458
|
+
type: pkg.projectInfo?.type,
|
|
459
|
+
hasDev: typeof pkg.scripts?.dev === "string" && pkg.scripts.dev.length > 0
|
|
460
|
+
};
|
|
461
|
+
} catch {
|
|
462
|
+
return {};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function discoverProjects() {
|
|
466
|
+
const root = findWorkspaceRoot();
|
|
467
|
+
const categorize = makeCategorizer(root);
|
|
468
|
+
const json = execSync("pnpm -r list --depth -1 --json", {
|
|
469
|
+
cwd: root,
|
|
470
|
+
encoding: "utf8",
|
|
471
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
472
|
+
});
|
|
473
|
+
const pkgs = JSON.parse(json);
|
|
474
|
+
return pkgs.map((p) => {
|
|
475
|
+
const info = readPackageInfo(p.path);
|
|
476
|
+
return {
|
|
477
|
+
name: p.name,
|
|
478
|
+
dir: p.path,
|
|
479
|
+
category: categorize(p.path),
|
|
480
|
+
...info.type ? { type: info.type } : {},
|
|
481
|
+
...info.hasDev ? { hasDev: info.hasDev } : {}
|
|
482
|
+
};
|
|
483
|
+
}).sort((a, b) => a.dir.localeCompare(b.dir));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// cli/apps.ts
|
|
487
|
+
function discoverApps() {
|
|
488
|
+
return discoverProjects().filter((p) => p.hasDev && p.category !== "workspace").map((p) => ({
|
|
489
|
+
name: basename(p.dir),
|
|
490
|
+
pkg: p.name,
|
|
491
|
+
dir: p.dir,
|
|
492
|
+
category: p.category,
|
|
493
|
+
...p.type ? { type: p.type } : {}
|
|
494
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
495
|
+
}
|
|
496
|
+
var ALL = "__all__";
|
|
497
|
+
var DEVIS = "__devis__";
|
|
498
|
+
var CADDY = "__caddy__";
|
|
499
|
+
async function selectApps(apps, selector, message = "\uC2E4\uD589\uD560 \uD504\uB85C\uC81D\uD2B8\uB97C \uC120\uD0DD\uD558\uC138\uC694") {
|
|
500
|
+
const pick = (value) => {
|
|
501
|
+
if (value === ALL || value === "all") return apps;
|
|
502
|
+
const app = apps.find((a) => a.name === value || a.pkg === value);
|
|
503
|
+
return app ? [app] : [];
|
|
504
|
+
};
|
|
505
|
+
const arg = selector?.trim();
|
|
506
|
+
if (arg) {
|
|
507
|
+
const sel = pick(arg);
|
|
508
|
+
if (sel.length === 0) {
|
|
509
|
+
console.error(
|
|
510
|
+
`
|
|
511
|
+
\uC54C \uC218 \uC5C6\uB294 \uD504\uB85C\uC81D\uD2B8: "${arg}"
|
|
512
|
+
\uC0AC\uC6A9 \uAC00\uB2A5: all, ${apps.map((a) => a.name).join(", ")}
|
|
513
|
+
`
|
|
514
|
+
);
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
return sel;
|
|
518
|
+
}
|
|
519
|
+
if (!process.stdin.isTTY) return apps;
|
|
520
|
+
try {
|
|
521
|
+
const value = await select({
|
|
522
|
+
message,
|
|
523
|
+
choices: [
|
|
524
|
+
new Separator("Dev \u2304"),
|
|
525
|
+
{ name: "Devis", value: DEVIS },
|
|
526
|
+
new Separator("Apps \u2304"),
|
|
527
|
+
{ name: "ALL", value: ALL },
|
|
528
|
+
...apps.map((a) => ({ name: `${a.name}`, value: a.name })),
|
|
529
|
+
new Separator("Proxy \u2304"),
|
|
530
|
+
{ name: "caddy", value: CADDY }
|
|
531
|
+
]
|
|
532
|
+
});
|
|
533
|
+
if (value === DEVIS) return "devis";
|
|
534
|
+
if (value === CADDY) return "caddy";
|
|
535
|
+
return pick(value);
|
|
536
|
+
} catch (err) {
|
|
537
|
+
if (err?.name === "ExitPromptError") {
|
|
538
|
+
process.exit(0);
|
|
539
|
+
}
|
|
540
|
+
throw err;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// cli/caddy.ts
|
|
545
|
+
import { execSync as execSync3 } from "child_process";
|
|
546
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
547
|
+
import http from "http";
|
|
548
|
+
import https from "https";
|
|
549
|
+
import { homedir as homedir3 } from "os";
|
|
550
|
+
import { join as join10 } from "path";
|
|
551
|
+
|
|
552
|
+
// cli/certs.ts
|
|
553
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
554
|
+
import { resolve as resolve2 } from "path";
|
|
555
|
+
|
|
556
|
+
// cli/sudo-prompt.ts
|
|
557
|
+
import { execFileSync as execFileSync4, execSync as execSync2, spawnSync } from "child_process";
|
|
558
|
+
function hasTty() {
|
|
559
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
560
|
+
}
|
|
561
|
+
function sudoCached() {
|
|
562
|
+
try {
|
|
563
|
+
execSync2("sudo -n -v", { stdio: "ignore" });
|
|
564
|
+
return true;
|
|
565
|
+
} catch {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function promptPasswordMacOS(reason) {
|
|
570
|
+
const script = [
|
|
571
|
+
'tell application "System Events"',
|
|
572
|
+
` set theResult to display dialog ${jsString(reason)} default answer "" with hidden answer with title "Devis \u2014 \uAD00\uB9AC\uC790 \uAD8C\uD55C \uD544\uC694" with icon caution buttons {"\uCDE8\uC18C", "\uD655\uC778"} default button "\uD655\uC778" cancel button "\uCDE8\uC18C"`,
|
|
573
|
+
" return text returned of theResult",
|
|
574
|
+
"end tell"
|
|
575
|
+
].join("\n");
|
|
576
|
+
try {
|
|
577
|
+
const out = execFileSync4("osascript", ["-e", script], { encoding: "utf8" });
|
|
578
|
+
const pw = out.trim();
|
|
579
|
+
return pw.length > 0 ? pw : null;
|
|
580
|
+
} catch {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function jsString(s) {
|
|
585
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
586
|
+
}
|
|
587
|
+
function cacheSudo(password) {
|
|
588
|
+
const res = spawnSync("sudo", ["-S", "-v"], {
|
|
589
|
+
input: `${password}
|
|
590
|
+
`,
|
|
591
|
+
encoding: "utf8",
|
|
592
|
+
// stderr 도 캡처해 실패 메시지를 확인 (성공 시엔 stderr 도 비어있음).
|
|
593
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
594
|
+
});
|
|
595
|
+
return res.status === 0;
|
|
596
|
+
}
|
|
597
|
+
var pendingAuth = null;
|
|
598
|
+
function ensureSudoForCerts() {
|
|
599
|
+
if (pendingAuth) return pendingAuth;
|
|
600
|
+
pendingAuth = doEnsureSudo().finally(() => {
|
|
601
|
+
pendingAuth = null;
|
|
602
|
+
});
|
|
603
|
+
return pendingAuth;
|
|
604
|
+
}
|
|
605
|
+
async function doEnsureSudo() {
|
|
606
|
+
if (hasTty()) return { ready: true };
|
|
607
|
+
if (sudoCached()) return { ready: true };
|
|
608
|
+
if (process.platform !== "darwin") {
|
|
609
|
+
return {
|
|
610
|
+
ready: false,
|
|
611
|
+
reason: "TTY \uC5C6\uC774 \uD638\uCD9C\uB3FC sudo \uC778\uC99D\uC744 \uBC1B\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD130\uBBF8\uB110\uC5D0\uC11C `devis up <name>` \uC744 \uBA3C\uC800 \uD55C \uBC88 \uC2E4\uD589\uD574 \uC778\uC99D\uC11C\uB97C \uBC1C\uAE09\uD574\uB450\uBA74, \uC774\uD6C4 UI \uC5D0\uC11C\uB3C4 \uB3D9\uC791\uD569\uB2C8\uB2E4."
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const message = "devis \uAC00 \uB85C\uCEEC HTTPS \uC778\uC99D\uC11C\uB97C \uC0DD\uC131\uD558\uAE30 \uC704\uD574 \uAD00\uB9AC\uC790 \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.\n\uBE44\uBC00\uBC88\uD638\uB97C \uC785\uB825\uD558\uBA74 \uC774\uD6C4 5\uBD84\uAC04 \uCD94\uAC00 \uC785\uB825 \uC5C6\uC774 \uC9C4\uD589\uB429\uB2C8\uB2E4.";
|
|
615
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
616
|
+
const pw = promptPasswordMacOS(message);
|
|
617
|
+
if (pw === null) {
|
|
618
|
+
return { ready: false, reason: "\uC0AC\uC6A9\uC790\uAC00 \uBE44\uBC00\uBC88\uD638 \uC785\uB825\uC744 \uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4." };
|
|
619
|
+
}
|
|
620
|
+
if (cacheSudo(pw)) return { ready: true };
|
|
621
|
+
}
|
|
622
|
+
return { ready: false, reason: "sudo \uC778\uC99D \uC2E4\uD328 \u2014 \uBE44\uBC00\uBC88\uD638\uAC00 3\uD68C \uBAA8\uB450 \uC798\uBABB \uC785\uB825\uB410\uC2B5\uB2C8\uB2E4." };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// cli/certs.ts
|
|
626
|
+
function isPublicLoopbackDomain(domain) {
|
|
627
|
+
const lower = domain.toLowerCase();
|
|
628
|
+
return lower === "lvh.me" || lower.endsWith(".lvh.me") || lower === "localtest.me" || lower.endsWith(".localtest.me") || lower.endsWith(".sslip.io") || lower.endsWith(".nip.io");
|
|
629
|
+
}
|
|
630
|
+
async function ensureCert(domain) {
|
|
631
|
+
const devcert = await import("devcert");
|
|
632
|
+
const api = devcert.default ?? devcert;
|
|
633
|
+
const isFirstTime = !api.hasCertificateFor(domain);
|
|
634
|
+
if (isFirstTime) {
|
|
635
|
+
if (hasTty()) {
|
|
636
|
+
console.log("");
|
|
637
|
+
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
638
|
+
console.log(" \u2502 \u{1F510} \uB85C\uCEEC HTTPS \uC778\uC99D\uC11C \uCD08\uAE30 \uC124\uC815 \u2502");
|
|
639
|
+
console.log(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
640
|
+
console.log(" \u2502 devis \uAC00 \uB85C\uCEEC CA(\uC778\uC99D\uAE30\uAD00)\uB97C \uC2DC\uC2A4\uD15C \uD0A4\uCCB4\uC778\uC5D0 \u2502");
|
|
641
|
+
console.log(" \u2502 \uB4F1\uB85D\uD569\uB2C8\uB2E4. \uC774 \uC791\uC5C5\uC740 \uCD5C\uCD08 1\uD68C\uB9CC \uD544\uC694\uD569\uB2C8\uB2E4. \u2502");
|
|
642
|
+
console.log(" \u2502 \u2502");
|
|
643
|
+
console.log(" \u2502 \uC7A0\uC2DC \uD6C4 \uD130\uBBF8\uB110\uC5D0\uC11C \uC2DC\uC2A4\uD15C \uC554\uD638(sudo)\uB97C \uC694\uCCAD\uD569\uB2C8\uB2E4. \u2502");
|
|
644
|
+
console.log(" \u2502 \uC554\uD638\uB97C \uC785\uB825\uD558\uBA74 \uC774\uD6C4\uC5D0\uB294 \uC790\uB3D9\uC73C\uB85C \uCC98\uB9AC\uB429\uB2C8\uB2E4. \u2502");
|
|
645
|
+
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
646
|
+
console.log("");
|
|
647
|
+
} else {
|
|
648
|
+
console.log(" HTTPS \uC778\uC99D\uC11C \uC0DD\uC131\uC744 \uC704\uD574 \uAD00\uB9AC\uC790 \uAD8C\uD55C\uC774 \uD544\uC694\uD569\uB2C8\uB2E4 \u2014 GUI \uB2E4\uC774\uC5BC\uB85C\uADF8\uB97C \uB744\uC6C1\uB2C8\uB2E4.");
|
|
649
|
+
const result = await ensureSudoForCerts();
|
|
650
|
+
if (!result.ready) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
`devcert \uC778\uC99D\uC11C \uC0DD\uC131 \uBD88\uAC00 (${domain}).
|
|
653
|
+
\uC6D0\uC778: ${result.reason ?? "sudo \uC778\uC99D \uC2E4\uD328"}`
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
const opts = isPublicLoopbackDomain(domain) ? { skipHostsFile: true } : {};
|
|
660
|
+
const { key, cert } = await api.certificateFor(domain, opts);
|
|
661
|
+
return {
|
|
662
|
+
certificate: cert.toString("utf8"),
|
|
663
|
+
key: key.toString("utf8")
|
|
664
|
+
};
|
|
665
|
+
} catch (err) {
|
|
666
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
667
|
+
throw new Error(
|
|
668
|
+
`devcert \uC778\uC99D\uC11C \uC0DD\uC131 \uC2E4\uD328 (${domain}).
|
|
669
|
+
\uC6D0\uC778: ${msg}
|
|
670
|
+
sudo \uC554\uD638\uAC00 \uC798\uBABB\uB410\uAC70\uB098 \uAD8C\uD55C\uC774 \uC5C6\uB294 \uACBD\uC6B0 \uC7AC\uC2DC\uB3C4\uD558\uC138\uC694.`
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function readCert(certFile, keyFile) {
|
|
675
|
+
return {
|
|
676
|
+
certificate: readFileSync4(resolve2(certFile), "utf8"),
|
|
677
|
+
key: readFileSync4(resolve2(keyFile), "utf8")
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// cli/config.ts
|
|
682
|
+
import { lookup } from "dns/promises";
|
|
683
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
684
|
+
import { join as join8 } from "path";
|
|
685
|
+
import { parse as parseJsonc } from "jsonc-parser";
|
|
686
|
+
function loadOneConfig(filePath) {
|
|
687
|
+
if (!existsSync4(filePath)) return void 0;
|
|
688
|
+
try {
|
|
689
|
+
const errors = [];
|
|
690
|
+
const raw = parseJsonc(readFileSync5(filePath, "utf8"), errors, {
|
|
691
|
+
allowTrailingComma: true,
|
|
692
|
+
disallowComments: false
|
|
693
|
+
});
|
|
694
|
+
if (errors.length > 0) {
|
|
695
|
+
console.error(` ${filePath}: JSONC \uD30C\uC2F1 \uACBD\uACE0 (${errors.length}\uAC74) \u2014 \uAC00\uB2A5\uD55C \uB9CC\uD07C \uBB34\uC2DC\uD558\uACE0 \uC9C4\uD589`);
|
|
696
|
+
}
|
|
697
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
698
|
+
const { $schema, ...rest } = raw;
|
|
699
|
+
return rest;
|
|
700
|
+
} catch {
|
|
701
|
+
return void 0;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function loadUserConfig(root) {
|
|
705
|
+
const exts = [".jsonc", ".json"];
|
|
706
|
+
const find = (stem) => {
|
|
707
|
+
for (const ext of exts) {
|
|
708
|
+
const cfg = loadOneConfig(join8(root, `${stem}${ext}`));
|
|
709
|
+
if (cfg) return cfg;
|
|
710
|
+
}
|
|
711
|
+
return void 0;
|
|
712
|
+
};
|
|
713
|
+
const base = find("devis.config") ?? {};
|
|
714
|
+
const local = find("devis.config.local") ?? {};
|
|
715
|
+
return { ...base, ...local };
|
|
716
|
+
}
|
|
717
|
+
async function detectUpstreamHost() {
|
|
718
|
+
try {
|
|
719
|
+
await Promise.race([
|
|
720
|
+
lookup("host.docker.internal"),
|
|
721
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("dns timeout")), 300))
|
|
722
|
+
]);
|
|
723
|
+
return "host.docker.internal";
|
|
724
|
+
} catch {
|
|
725
|
+
return "127.0.0.1";
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function defaultSlugFrom(root) {
|
|
729
|
+
try {
|
|
730
|
+
const pkg = JSON.parse(readFileSync5(join8(root, "package.json"), "utf8"));
|
|
731
|
+
return pkg.name?.replace(/[@/]/g, "-").replace(/^-/, "") ?? root.split("/").pop() ?? "dev";
|
|
732
|
+
} catch {
|
|
733
|
+
return root.split("/").pop() ?? "dev";
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async function loadConfig(root) {
|
|
737
|
+
const wsRoot = root ?? findWorkspaceRoot();
|
|
738
|
+
const userCfg = loadUserConfig(wsRoot);
|
|
739
|
+
const defaultSlug = defaultSlugFrom(wsRoot);
|
|
740
|
+
const workspaceSlug = userCfg.workspaceSlug ?? defaultSlug;
|
|
741
|
+
return {
|
|
742
|
+
workspaceSlug,
|
|
743
|
+
caddyAdmin: userCfg.caddyAdmin ?? "http://localhost:2019",
|
|
744
|
+
internalDomain: userCfg.internalDomain ?? `${workspaceSlug}.lvh.me`,
|
|
745
|
+
upstreamHost: await detectUpstreamHost(),
|
|
746
|
+
workspaceRoot: wsRoot,
|
|
747
|
+
...userCfg.publicDomain ? { publicDomain: userCfg.publicDomain } : {},
|
|
748
|
+
...userCfg.tls ? { tls: userCfg.tls } : {}
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// cli/global/registry.ts
|
|
753
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync6, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
754
|
+
import { z } from "zod";
|
|
755
|
+
var WorkspaceMetaSchema = z.object({
|
|
756
|
+
/** 모노레포 루트 절대경로 (pnpm-workspace.yaml 있는 위치). */
|
|
757
|
+
path: z.string(),
|
|
758
|
+
/** 표시 이름 — 미지정 시 UI 가 디렉터리 basename 폴백. */
|
|
759
|
+
label: z.string().optional(),
|
|
760
|
+
/** 최초 등록 시각 (ISO 8601). */
|
|
761
|
+
registeredAt: z.string(),
|
|
762
|
+
/** 마지막 활성화 시각 (ISO 8601). */
|
|
763
|
+
lastUsedAt: z.string(),
|
|
764
|
+
/** UI 사이드바에서 사용자가 드래그로 수동 정렬한 위치 (0 부터). */
|
|
765
|
+
order: z.number().optional()
|
|
766
|
+
});
|
|
767
|
+
function listSlugs() {
|
|
768
|
+
const root = workspacesDir();
|
|
769
|
+
if (!existsSync5(root)) return [];
|
|
770
|
+
return readdirSync(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
771
|
+
}
|
|
772
|
+
function readWorkspace(slug) {
|
|
773
|
+
const file = workspaceMetaFile(slug);
|
|
774
|
+
if (!existsSync5(file)) return null;
|
|
775
|
+
try {
|
|
776
|
+
const raw = JSON.parse(readFileSync6(file, "utf8"));
|
|
777
|
+
const meta = WorkspaceMetaSchema.parse(raw);
|
|
778
|
+
return { slug, meta };
|
|
779
|
+
} catch {
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function listWorkspaces() {
|
|
784
|
+
return listSlugs().map(readWorkspace).filter((w) => w !== null);
|
|
785
|
+
}
|
|
786
|
+
function findByPath(path) {
|
|
787
|
+
for (const w of listWorkspaces()) {
|
|
788
|
+
if (w.meta.path === path) return w;
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
function uniqueSlug(desired) {
|
|
793
|
+
const taken = new Set(listSlugs());
|
|
794
|
+
if (!taken.has(desired)) return desired;
|
|
795
|
+
for (let i = 2; i < 1e3; i++) {
|
|
796
|
+
const candidate = `${desired}-${i}`;
|
|
797
|
+
if (!taken.has(candidate)) return candidate;
|
|
798
|
+
}
|
|
799
|
+
throw new Error(`\uC2AC\uB7EC\uADF8 \uCDA9\uB3CC\uC774 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4: ${desired}`);
|
|
800
|
+
}
|
|
801
|
+
function writeMeta(slug, meta) {
|
|
802
|
+
mkdirSync4(workspaceDir(slug), { recursive: true });
|
|
803
|
+
writeFileSync3(workspaceMetaFile(slug), `${JSON.stringify(meta, null, 2)}
|
|
804
|
+
`);
|
|
805
|
+
}
|
|
806
|
+
function registerWorkspace(input) {
|
|
807
|
+
const existing = findByPath(input.path);
|
|
808
|
+
if (existing) {
|
|
809
|
+
const updated = { ...existing.meta, lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
810
|
+
writeMeta(existing.slug, updated);
|
|
811
|
+
return { slug: existing.slug, meta: updated };
|
|
812
|
+
}
|
|
813
|
+
const slug = uniqueSlug(input.desiredSlug);
|
|
814
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
815
|
+
const meta = {
|
|
816
|
+
path: input.path,
|
|
817
|
+
...input.label ? { label: input.label } : {},
|
|
818
|
+
registeredAt: now,
|
|
819
|
+
lastUsedAt: now
|
|
820
|
+
};
|
|
821
|
+
writeMeta(slug, meta);
|
|
822
|
+
return { slug, meta };
|
|
823
|
+
}
|
|
824
|
+
function unregisterWorkspace(slug) {
|
|
825
|
+
const dir = workspaceDir(slug);
|
|
826
|
+
if (!existsSync5(dir)) return false;
|
|
827
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
function touchWorkspace(slug) {
|
|
831
|
+
const ws = readWorkspace(slug);
|
|
832
|
+
if (!ws) return;
|
|
833
|
+
writeMeta(slug, { ...ws.meta, lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// cli/global/routes.ts
|
|
837
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
838
|
+
import { join as join9 } from "path";
|
|
839
|
+
import { z as z2 } from "zod";
|
|
840
|
+
var PersistedAppSchema = z2.object({
|
|
841
|
+
/** pm2 프로세스 이름 = 디렉터리 basename. */
|
|
842
|
+
name: z2.string(),
|
|
843
|
+
/** 응답할 호스트들 — [internalHost, publicHost?]. 비면 daemon 으로 간주(라우트 불필요). */
|
|
844
|
+
hosts: z2.array(z2.string()),
|
|
845
|
+
/** 앱이 listen 하는 포트. */
|
|
846
|
+
port: z2.number().int().positive()
|
|
847
|
+
});
|
|
848
|
+
var PersistedRoutesSchema = z2.object({
|
|
849
|
+
version: z2.literal(1),
|
|
850
|
+
savedAt: z2.string(),
|
|
851
|
+
apps: z2.array(PersistedAppSchema)
|
|
852
|
+
});
|
|
853
|
+
var FILE_NAME = "routes.json";
|
|
854
|
+
function routesFile(slug) {
|
|
855
|
+
return join9(workspaceDir(slug), FILE_NAME);
|
|
856
|
+
}
|
|
857
|
+
function readRoutes(slug) {
|
|
858
|
+
const file = routesFile(slug);
|
|
859
|
+
if (!existsSync6(file)) return [];
|
|
860
|
+
try {
|
|
861
|
+
const raw = JSON.parse(readFileSync7(file, "utf8"));
|
|
862
|
+
const parsed = PersistedRoutesSchema.parse(raw);
|
|
863
|
+
return parsed.apps;
|
|
864
|
+
} catch {
|
|
865
|
+
return [];
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function writeRoutes(slug, apps) {
|
|
869
|
+
const file = routesFile(slug);
|
|
870
|
+
if (apps.length === 0) {
|
|
871
|
+
if (existsSync6(file)) rmSync4(file, { force: true });
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const payload = {
|
|
875
|
+
version: 1,
|
|
876
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
877
|
+
apps
|
|
878
|
+
};
|
|
879
|
+
writeFileSync4(file, `${JSON.stringify(payload, null, 2)}
|
|
880
|
+
`);
|
|
881
|
+
}
|
|
882
|
+
function removeRoutes(slug, appNames) {
|
|
883
|
+
const remaining = readRoutes(slug).filter((a) => !appNames.has(a.name));
|
|
884
|
+
writeRoutes(slug, remaining);
|
|
885
|
+
}
|
|
886
|
+
function listAllRoutes(slugs) {
|
|
887
|
+
const out = [];
|
|
888
|
+
for (const slug of slugs) {
|
|
889
|
+
const apps = readRoutes(slug);
|
|
890
|
+
if (apps.length > 0) out.push({ slug, apps });
|
|
891
|
+
}
|
|
892
|
+
return out;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// cli/caddy.ts
|
|
896
|
+
function caddyRequest(admin, path, init = {}) {
|
|
897
|
+
const url = new URL(`${admin}${path}`);
|
|
898
|
+
const client = url.protocol === "https:" ? https : http;
|
|
899
|
+
const headers = {};
|
|
900
|
+
if (init.body !== void 0) {
|
|
901
|
+
headers["Content-Type"] = "application/json";
|
|
902
|
+
headers["Content-Length"] = String(Buffer.byteLength(init.body));
|
|
903
|
+
}
|
|
904
|
+
return new Promise((done, fail) => {
|
|
905
|
+
const req = client.request(url, { method: init.method ?? "GET", headers }, (res) => {
|
|
906
|
+
let body = "";
|
|
907
|
+
res.setEncoding("utf8");
|
|
908
|
+
res.on("data", (chunk) => body += chunk);
|
|
909
|
+
res.on("end", () => {
|
|
910
|
+
const status2 = res.statusCode ?? 0;
|
|
911
|
+
done({ status: status2, ok: status2 >= 200 && status2 < 300, body });
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
req.on("error", fail);
|
|
915
|
+
if (init.body !== void 0) req.write(init.body);
|
|
916
|
+
req.end();
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
async function caddyReachable(admin) {
|
|
920
|
+
try {
|
|
921
|
+
return (await caddyRequest(admin, "/config/")).ok;
|
|
922
|
+
} catch {
|
|
923
|
+
return false;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
async function caddyGetConfig(admin) {
|
|
927
|
+
const res = await caddyRequest(admin, "/config/");
|
|
928
|
+
if (!res.ok) throw new Error(`Caddy GET /config/ \uC2E4\uD328: ${res.status}`);
|
|
929
|
+
return res.body ? JSON.parse(res.body) : null;
|
|
930
|
+
}
|
|
931
|
+
async function caddyLoad(admin, config) {
|
|
932
|
+
const res = await caddyRequest(admin, "/load", {
|
|
933
|
+
method: "POST",
|
|
934
|
+
body: JSON.stringify(config)
|
|
935
|
+
});
|
|
936
|
+
if (!res.ok) {
|
|
937
|
+
throw new Error(`Caddy POST /load \uC2E4\uD328: ${res.status} ${res.body}`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function caddyWorks(bin) {
|
|
941
|
+
try {
|
|
942
|
+
execSync3(`"${bin}" version`, { stdio: "ignore", timeout: 5e3 });
|
|
943
|
+
return true;
|
|
944
|
+
} catch {
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
function resolveCaddyBin() {
|
|
949
|
+
const { platform, arch } = process;
|
|
950
|
+
const archSuffix = arch === "arm64" ? "arm64" : "amd64";
|
|
951
|
+
const platformSuffix = platform === "darwin" ? "mac" : platform === "win32" ? "windows" : "linux";
|
|
952
|
+
const ext = platform === "win32" ? ".exe" : "";
|
|
953
|
+
const candidates = [];
|
|
954
|
+
const binDir = join10(findDevisRoot(), ".bin");
|
|
955
|
+
candidates.push(join10(binDir, `caddy${ext}`), join10(binDir, `caddy-${platformSuffix}-${archSuffix}${ext}`));
|
|
956
|
+
const miseData = process.env.MISE_DATA_DIR || join10(homedir3(), ".local", "share", "mise");
|
|
957
|
+
const miseCaddyDir = join10(miseData, "installs", "caddy");
|
|
958
|
+
if (existsSync7(miseCaddyDir)) {
|
|
959
|
+
candidates.push(join10(miseCaddyDir, "latest", `caddy${ext}`));
|
|
960
|
+
try {
|
|
961
|
+
for (const v of readdirSync2(miseCaddyDir)) {
|
|
962
|
+
candidates.push(join10(miseCaddyDir, v, `caddy${ext}`));
|
|
963
|
+
}
|
|
964
|
+
} catch {
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
const p = execSync3("mise which caddy", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
969
|
+
if (p) candidates.push(p);
|
|
970
|
+
} catch {
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
const cmd = platform === "win32" ? "where caddy" : "command -v caddy";
|
|
974
|
+
const p = execSync3(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim().split(/\r?\n/)[0];
|
|
975
|
+
if (p) candidates.push(p);
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
for (const c of candidates) {
|
|
979
|
+
if (existsSync7(c) && caddyWorks(c)) return c;
|
|
980
|
+
}
|
|
981
|
+
throw new Error(
|
|
982
|
+
"caddy \uC2E4\uD589 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uB610\uB294 \uBC1C\uACAC\uB41C caddy \uAC00 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4).\n \uB2E4\uC74C \uC911 \uD558\uB098\uB85C \uD574\uACB0\uD558\uC138\uC694:\n - mise use -g caddy (mise \uB85C \uC124\uCE58\uB9CC \uD558\uACE0 \uBC84\uC804 pin \uC744 \uC548 \uD55C \uACBD\uC6B0)\n - brew install caddy\n - `pnpm approve-builds` \uB85C @gbdx/devis \uC758 postinstall(\uBC88\uB4E4 caddy \uB2E4\uC6B4\uB85C\uB4DC)\uC744 \uD5C8\uC6A9"
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
function writeCaddyBootstrap(adminListen) {
|
|
986
|
+
mkdirSync5(caddyHome(), { recursive: true });
|
|
987
|
+
mkdirSync5(caddyDataDir(), { recursive: true });
|
|
988
|
+
const config = {
|
|
989
|
+
admin: { listen: adminListen },
|
|
990
|
+
storage: {
|
|
991
|
+
module: "file_system",
|
|
992
|
+
root: caddyDataDir()
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
const filePath = caddyBootstrapFile();
|
|
996
|
+
writeFileSync5(filePath, JSON.stringify(config, null, 2));
|
|
997
|
+
return filePath;
|
|
998
|
+
}
|
|
999
|
+
function caddyAdminListen(adminUrl) {
|
|
1000
|
+
try {
|
|
1001
|
+
const u = new URL(adminUrl);
|
|
1002
|
+
const host = u.hostname || "localhost";
|
|
1003
|
+
const port = u.port || "2019";
|
|
1004
|
+
return `${host}:${port}`;
|
|
1005
|
+
} catch {
|
|
1006
|
+
return "localhost:2019";
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
async function waitForCaddy(admin, attempts = 50) {
|
|
1010
|
+
for (let i = 0; i < attempts; i++) {
|
|
1011
|
+
if (await caddyReachable(admin)) return;
|
|
1012
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1013
|
+
}
|
|
1014
|
+
throw new Error("Caddy Admin API \uAC00 \uC2DC\uAC04 \uB0B4\uC5D0 \uC751\uB2F5\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
|
|
1015
|
+
}
|
|
1016
|
+
var HTTPS_SERVER = "devis-https";
|
|
1017
|
+
function findHttpsServer(config) {
|
|
1018
|
+
const servers = config?.apps?.http?.servers ?? {};
|
|
1019
|
+
for (const [name, srv] of Object.entries(servers)) {
|
|
1020
|
+
const listen = srv?.listen ?? [];
|
|
1021
|
+
if (listen.some((l) => l === ":443" || l.endsWith(":443"))) return name;
|
|
1022
|
+
}
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
function ownedBy(id, appNames, slug) {
|
|
1026
|
+
const s = String(id ?? "");
|
|
1027
|
+
if (!s.startsWith(`${slug}:`)) return false;
|
|
1028
|
+
return appNames.has(s.slice(s.lastIndexOf(":") + 1));
|
|
1029
|
+
}
|
|
1030
|
+
function stripOurEntries(config, appNames, slug) {
|
|
1031
|
+
const cfg = config ?? {};
|
|
1032
|
+
const servers = cfg?.apps?.http?.servers ?? {};
|
|
1033
|
+
for (const srv of Object.values(servers)) {
|
|
1034
|
+
if (Array.isArray(srv.routes)) {
|
|
1035
|
+
srv.routes = srv.routes.filter((r) => !ownedBy(r["@id"], appNames, slug));
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
const tls = cfg?.apps?.tls;
|
|
1039
|
+
if (tls?.automation?.policies) {
|
|
1040
|
+
tls.automation.policies = tls.automation.policies.filter((p) => !ownedBy(p["@id"], appNames, slug));
|
|
1041
|
+
}
|
|
1042
|
+
for (const loader of ["load_pem", "load_files"]) {
|
|
1043
|
+
if (Array.isArray(tls?.certificates?.[loader])) {
|
|
1044
|
+
tls.certificates[loader] = tls.certificates[loader].filter((c) => !ownedBy(c["@id"], appNames, slug));
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return cfg;
|
|
1048
|
+
}
|
|
1049
|
+
function applyOurEntries(config, apps, cfg) {
|
|
1050
|
+
const slug = cfg.workspaceSlug;
|
|
1051
|
+
const appNames = new Set(apps.map((a) => a.name));
|
|
1052
|
+
const base = stripOurEntries(config, appNames, slug) ?? {};
|
|
1053
|
+
base.apps ??= {};
|
|
1054
|
+
base.apps.http ??= {};
|
|
1055
|
+
base.apps.http.servers ??= {};
|
|
1056
|
+
const serverName = findHttpsServer(base) ?? HTTPS_SERVER;
|
|
1057
|
+
base.apps.http.servers[serverName] ??= { listen: [":443"], routes: [] };
|
|
1058
|
+
const server = base.apps.http.servers[serverName];
|
|
1059
|
+
server.routes ??= [];
|
|
1060
|
+
for (const app of apps) {
|
|
1061
|
+
server.routes.unshift({
|
|
1062
|
+
"@id": `${slug}:route:${app.name}`,
|
|
1063
|
+
match: [{ host: app.hosts }],
|
|
1064
|
+
handle: [
|
|
1065
|
+
{
|
|
1066
|
+
handler: "reverse_proxy",
|
|
1067
|
+
upstreams: [{ dial: `${cfg.upstreamHost}:${app.port}` }]
|
|
1068
|
+
}
|
|
1069
|
+
],
|
|
1070
|
+
terminal: true
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
base.apps.tls ??= {};
|
|
1074
|
+
const tls = base.apps.tls;
|
|
1075
|
+
tls.certificates ??= {};
|
|
1076
|
+
tls.certificates.load_pem ??= [];
|
|
1077
|
+
for (const app of apps) {
|
|
1078
|
+
tls.certificates.load_pem.push({
|
|
1079
|
+
"@id": `${slug}:cert:${app.name}`,
|
|
1080
|
+
certificate: app.cert.certificate,
|
|
1081
|
+
key: app.cert.key
|
|
1082
|
+
});
|
|
1083
|
+
if (cfg.publicDomain) {
|
|
1084
|
+
tls.automation ??= {};
|
|
1085
|
+
tls.automation.policies ??= [];
|
|
1086
|
+
tls.automation.policies.push({
|
|
1087
|
+
"@id": `${slug}:tlspolicy:public:${app.name}`,
|
|
1088
|
+
subjects: [`${app.name}.${cfg.publicDomain}`],
|
|
1089
|
+
issuers: [{ module: "acme" }]
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return base;
|
|
1094
|
+
}
|
|
1095
|
+
async function buildCaddyApp(app, cfg) {
|
|
1096
|
+
const cert = cfg.tls ? readCert(cfg.tls.cert, cfg.tls.key) : await ensureCert(app.hosts[0]);
|
|
1097
|
+
return { name: app.name, port: app.port, hosts: app.hosts, cert };
|
|
1098
|
+
}
|
|
1099
|
+
async function restoreAllRoutes(caddyAdmin) {
|
|
1100
|
+
const result = { workspaces: 0, routes: 0, errors: [] };
|
|
1101
|
+
const bundles = listAllRoutes(listSlugs());
|
|
1102
|
+
if (bundles.length === 0) return result;
|
|
1103
|
+
let config = await caddyGetConfig(caddyAdmin);
|
|
1104
|
+
for (const { slug, apps } of bundles) {
|
|
1105
|
+
const ws = readWorkspace(slug);
|
|
1106
|
+
if (!ws) {
|
|
1107
|
+
result.errors.push(`${slug}: \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uBA54\uD0C0\uAC00 \uC190\uC0C1\uB3FC \uC2A4\uD0B5`);
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
const webApps = apps.filter((a) => a.hosts.length > 0);
|
|
1111
|
+
if (webApps.length === 0) continue;
|
|
1112
|
+
try {
|
|
1113
|
+
const cfg = await loadConfig(ws.meta.path);
|
|
1114
|
+
const caddyApps = [];
|
|
1115
|
+
for (const app of webApps) {
|
|
1116
|
+
caddyApps.push(await buildCaddyApp(app, cfg));
|
|
1117
|
+
}
|
|
1118
|
+
config = applyOurEntries(config, caddyApps, cfg);
|
|
1119
|
+
result.workspaces += 1;
|
|
1120
|
+
result.routes += caddyApps.length;
|
|
1121
|
+
} catch (err) {
|
|
1122
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1123
|
+
result.errors.push(`${slug}: ${msg}`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (result.workspaces > 0) {
|
|
1127
|
+
await caddyLoad(caddyAdmin, config);
|
|
1128
|
+
}
|
|
1129
|
+
return result;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// cli/global/active.ts
|
|
1133
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
1134
|
+
import { dirname as dirname2 } from "path";
|
|
1135
|
+
function readActiveSlug() {
|
|
1136
|
+
const f = activeFile();
|
|
1137
|
+
if (!existsSync8(f)) return null;
|
|
1138
|
+
const s = readFileSync8(f, "utf8").trim();
|
|
1139
|
+
return s || null;
|
|
1140
|
+
}
|
|
1141
|
+
function writeActiveSlug(slug) {
|
|
1142
|
+
const f = activeFile();
|
|
1143
|
+
if (existsSync8(f)) {
|
|
1144
|
+
try {
|
|
1145
|
+
const current = readFileSync8(f, "utf8").trim();
|
|
1146
|
+
if (current === slug) {
|
|
1147
|
+
touchWorkspace(slug);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
} catch {
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
mkdirSync6(dirname2(f), { recursive: true });
|
|
1154
|
+
writeFileSync6(f, `${slug}
|
|
1155
|
+
`);
|
|
1156
|
+
touchWorkspace(slug);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// cli/global/context.ts
|
|
1160
|
+
function setPm2HomeEnv() {
|
|
1161
|
+
process.env.PM2_HOME = pm2Home();
|
|
1162
|
+
}
|
|
1163
|
+
async function resolveCommandContext() {
|
|
1164
|
+
const wsRoot = findWorkspaceRootStrict();
|
|
1165
|
+
if (!wsRoot) {
|
|
1166
|
+
console.error(
|
|
1167
|
+
`
|
|
1168
|
+
\uC774 \uBA85\uB839\uC740 \uBAA8\uB178\uB808\uD3EC \uC548\uC5D0\uC11C \uC2E4\uD589\uD574\uC57C \uD569\uB2C8\uB2E4 (pnpm-workspace.yaml \uC744 \uCC3E\uC744 \uC218 \uC5C6\uC74C).
|
|
1169
|
+
\uD604\uC7AC \uC704\uCE58: ${process.cwd()}
|
|
1170
|
+
`
|
|
1171
|
+
);
|
|
1172
|
+
process.exit(1);
|
|
1173
|
+
}
|
|
1174
|
+
const cfg = await loadConfig(wsRoot);
|
|
1175
|
+
const workspace = registerWorkspace({ path: wsRoot, desiredSlug: cfg.workspaceSlug });
|
|
1176
|
+
writeActiveSlug(workspace.slug);
|
|
1177
|
+
setPm2HomeEnv();
|
|
1178
|
+
return { workspace, cfg };
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// cli/global/resolve.ts
|
|
1182
|
+
async function autoRegisterFromCwd(cwd = process.cwd()) {
|
|
1183
|
+
const wsRoot = findWorkspaceRootStrict(cwd);
|
|
1184
|
+
if (!wsRoot) return null;
|
|
1185
|
+
const cfg = await loadConfig(wsRoot);
|
|
1186
|
+
const workspace = registerWorkspace({ path: wsRoot, desiredSlug: cfg.workspaceSlug });
|
|
1187
|
+
writeActiveSlug(workspace.slug);
|
|
1188
|
+
return workspace;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// cli/global/singleton.ts
|
|
1192
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
1193
|
+
import { dirname as dirname3 } from "path";
|
|
1194
|
+
function isPidAlive(pid) {
|
|
1195
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1196
|
+
try {
|
|
1197
|
+
process.kill(pid, 0);
|
|
1198
|
+
return true;
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
return err.code === "EPERM";
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
function readLockPid() {
|
|
1204
|
+
const f = lockFile();
|
|
1205
|
+
if (!existsSync9(f)) return null;
|
|
1206
|
+
const s = readFileSync9(f, "utf8").trim();
|
|
1207
|
+
const pid = Number.parseInt(s, 10);
|
|
1208
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
1209
|
+
}
|
|
1210
|
+
function runningInstancePid() {
|
|
1211
|
+
const pid = readLockPid();
|
|
1212
|
+
if (pid === null) return null;
|
|
1213
|
+
if (pid === process.pid) return null;
|
|
1214
|
+
return isPidAlive(pid) ? pid : null;
|
|
1215
|
+
}
|
|
1216
|
+
function writeLockPid(pid) {
|
|
1217
|
+
const f = lockFile();
|
|
1218
|
+
mkdirSync7(dirname3(f), { recursive: true });
|
|
1219
|
+
writeFileSync7(f, `${pid}
|
|
1220
|
+
`);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// cli/pm2.ts
|
|
1224
|
+
import { execFile, execSync as execSync4, spawn as spawn2 } from "child_process";
|
|
1225
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
1226
|
+
import { createRequire } from "module";
|
|
1227
|
+
import { homedir as homedir4 } from "os";
|
|
1228
|
+
import { dirname as dirname4, join as join11 } from "path";
|
|
1229
|
+
|
|
1230
|
+
// core/pm2.ts
|
|
1231
|
+
var CADDY_PROC = "caddy";
|
|
1232
|
+
var SYSTEM_NAMESPACE = "__devis_system__";
|
|
1233
|
+
function stripNamespacePrefix(name, slug) {
|
|
1234
|
+
const prefix = `${slug}__`;
|
|
1235
|
+
return name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// cli/pm2.ts
|
|
1239
|
+
var pm2Cached = null;
|
|
1240
|
+
function pm2() {
|
|
1241
|
+
if (!pm2Cached) {
|
|
1242
|
+
const require2 = createRequire(import.meta.url);
|
|
1243
|
+
pm2Cached = require2("pm2");
|
|
1244
|
+
}
|
|
1245
|
+
return pm2Cached;
|
|
1246
|
+
}
|
|
1247
|
+
var PM2_BIN = (() => {
|
|
1248
|
+
try {
|
|
1249
|
+
const require2 = createRequire(import.meta.url);
|
|
1250
|
+
const pkgPath = require2.resolve("pm2/package.json");
|
|
1251
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
1252
|
+
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pm2;
|
|
1253
|
+
if (binRel) return join11(dirname4(pkgPath), binRel);
|
|
1254
|
+
} catch {
|
|
1255
|
+
}
|
|
1256
|
+
return "pm2";
|
|
1257
|
+
})();
|
|
1258
|
+
function pConnect() {
|
|
1259
|
+
return new Promise((done, fail) => pm2().connect((err) => err ? fail(err) : done()));
|
|
1260
|
+
}
|
|
1261
|
+
function pStart(opts) {
|
|
1262
|
+
return new Promise((done, fail) => pm2().start(opts, (err) => err ? fail(err) : done()));
|
|
1263
|
+
}
|
|
1264
|
+
function pList() {
|
|
1265
|
+
return new Promise((done, fail) => pm2().list((err, list) => err ? fail(err) : done(list ?? [])));
|
|
1266
|
+
}
|
|
1267
|
+
function pRestart(name) {
|
|
1268
|
+
return new Promise((done, fail) => pm2().restart(name, (err) => err ? fail(err) : done()));
|
|
1269
|
+
}
|
|
1270
|
+
function pStop(name) {
|
|
1271
|
+
return new Promise((done, fail) => pm2().stop(name, (err) => err ? fail(err) : done()));
|
|
1272
|
+
}
|
|
1273
|
+
function pDelete(name) {
|
|
1274
|
+
return new Promise((done, fail) => pm2().delete(name, (err) => err ? fail(err) : done()));
|
|
1275
|
+
}
|
|
1276
|
+
async function withPm2(fn) {
|
|
1277
|
+
await pConnect();
|
|
1278
|
+
try {
|
|
1279
|
+
return await fn();
|
|
1280
|
+
} finally {
|
|
1281
|
+
pm2().disconnect();
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
function resolvePnpm() {
|
|
1285
|
+
try {
|
|
1286
|
+
const p = execSync4("mise which pnpm", {
|
|
1287
|
+
encoding: "utf8",
|
|
1288
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1289
|
+
}).trim();
|
|
1290
|
+
if (p) return p;
|
|
1291
|
+
} catch {
|
|
1292
|
+
}
|
|
1293
|
+
return "pnpm";
|
|
1294
|
+
}
|
|
1295
|
+
var PM2_RESERVED = /* @__PURE__ */ new Set([
|
|
1296
|
+
"name",
|
|
1297
|
+
"namespace",
|
|
1298
|
+
"status",
|
|
1299
|
+
"version",
|
|
1300
|
+
"versioning",
|
|
1301
|
+
"node_version",
|
|
1302
|
+
"unique_id",
|
|
1303
|
+
"pm_id",
|
|
1304
|
+
"pm_uptime",
|
|
1305
|
+
"pm_cwd",
|
|
1306
|
+
"pm_exec_path",
|
|
1307
|
+
"pm_out_log_path",
|
|
1308
|
+
"pm_err_log_path",
|
|
1309
|
+
"pm_pid_path",
|
|
1310
|
+
"exec_mode",
|
|
1311
|
+
"exec_interpreter",
|
|
1312
|
+
"instance_var",
|
|
1313
|
+
"NODE_APP_INSTANCE",
|
|
1314
|
+
"restart_time",
|
|
1315
|
+
"unstable_restarts",
|
|
1316
|
+
"created_at",
|
|
1317
|
+
"km_link",
|
|
1318
|
+
"vizion_running",
|
|
1319
|
+
"axm_actions",
|
|
1320
|
+
"axm_monitor",
|
|
1321
|
+
"axm_options",
|
|
1322
|
+
"axm_dynamic",
|
|
1323
|
+
"PM2_JSON_PROCESSING",
|
|
1324
|
+
"PM2_PROGRAMMATIC",
|
|
1325
|
+
"PM2_USAGE"
|
|
1326
|
+
]);
|
|
1327
|
+
function cleanEnv(extra = {}) {
|
|
1328
|
+
const out = {};
|
|
1329
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
1330
|
+
if (v === void 0) continue;
|
|
1331
|
+
if (PM2_RESERVED.has(k)) continue;
|
|
1332
|
+
if (k === "PORT" || k === "NODE_ENV" || k === "APP_SLUG" || k.startsWith("DEV_")) continue;
|
|
1333
|
+
out[k] = v;
|
|
1334
|
+
}
|
|
1335
|
+
if (!out.CI) out.CI = "true";
|
|
1336
|
+
return { ...out, ...extra };
|
|
1337
|
+
}
|
|
1338
|
+
function pm2LogPath(name) {
|
|
1339
|
+
const home = process.env.PM2_HOME ?? join11(homedir4(), ".pm2");
|
|
1340
|
+
return join11(home, "logs", `${name}.log`);
|
|
1341
|
+
}
|
|
1342
|
+
async function freshStart(opts, existing) {
|
|
1343
|
+
if (opts.name && existing.has(opts.name)) await pDelete(opts.name);
|
|
1344
|
+
await pStart(opts);
|
|
1345
|
+
}
|
|
1346
|
+
async function startApps(apps, bindHost, slug) {
|
|
1347
|
+
const existing = new Set((await pList()).map((p) => p.name ?? ""));
|
|
1348
|
+
const pnpm = resolvePnpm();
|
|
1349
|
+
for (const app of apps) {
|
|
1350
|
+
const pm2Name = `${slug}__${app.name}`;
|
|
1351
|
+
const logFile = pm2LogPath(pm2Name);
|
|
1352
|
+
const hostEnv = {};
|
|
1353
|
+
if (app.hosts[0]) {
|
|
1354
|
+
hostEnv.DEV_HOST = app.hosts[0];
|
|
1355
|
+
hostEnv.DEV_URL = `https://${app.hosts[0]}`;
|
|
1356
|
+
if (app.hosts[1]) hostEnv.DEV_PUBLIC_URL = `https://${app.hosts[1]}`;
|
|
1357
|
+
}
|
|
1358
|
+
await freshStart(
|
|
1359
|
+
{
|
|
1360
|
+
name: pm2Name,
|
|
1361
|
+
output: logFile,
|
|
1362
|
+
error: logFile,
|
|
1363
|
+
namespace: slug,
|
|
1364
|
+
script: pnpm,
|
|
1365
|
+
args: ["--filter", app.pkg, "dev"],
|
|
1366
|
+
interpreter: "none",
|
|
1367
|
+
exec_mode: "fork",
|
|
1368
|
+
instances: 1,
|
|
1369
|
+
force: true,
|
|
1370
|
+
// pm2 재시작 사이 지연(ms) — ts-node-dev 의 포트 충돌 완화를 노린 값이지만
|
|
1371
|
+
// 확실한 해결책은 아니다 (앱 재시작은 devis 가 재오케스트레이션으로 처리).
|
|
1372
|
+
restart_delay: 1,
|
|
1373
|
+
// dev 앱이 죽으면(컴파일 에러 등) 그대로 둔다 — 로그 확인 후 수동 재시작.
|
|
1374
|
+
autorestart: false,
|
|
1375
|
+
// 시간표시 끔
|
|
1376
|
+
time: false,
|
|
1377
|
+
cwd: process.cwd(),
|
|
1378
|
+
env: cleanEnv({
|
|
1379
|
+
NODE_ENV: "development",
|
|
1380
|
+
PORT: String(app.port),
|
|
1381
|
+
DEV_BIND_HOST: bindHost,
|
|
1382
|
+
DEV_PROJECT_DIR: app.dir,
|
|
1383
|
+
...hostEnv
|
|
1384
|
+
})
|
|
1385
|
+
},
|
|
1386
|
+
existing
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
async function startCaddyProcess(adminUrl = "http://localhost:2019") {
|
|
1391
|
+
const bootstrapPath = writeCaddyBootstrap(caddyAdminListen(adminUrl));
|
|
1392
|
+
const existing = new Set((await pList()).map((p) => p.name ?? ""));
|
|
1393
|
+
const logFile = pm2LogPath(CADDY_PROC);
|
|
1394
|
+
await freshStart(
|
|
1395
|
+
{
|
|
1396
|
+
name: CADDY_PROC,
|
|
1397
|
+
output: logFile,
|
|
1398
|
+
error: logFile,
|
|
1399
|
+
namespace: SYSTEM_NAMESPACE,
|
|
1400
|
+
script: resolveCaddyBin(),
|
|
1401
|
+
args: ["run", "--config", bootstrapPath],
|
|
1402
|
+
interpreter: "none",
|
|
1403
|
+
exec_mode: "fork",
|
|
1404
|
+
instances: 1,
|
|
1405
|
+
autorestart: false,
|
|
1406
|
+
time: false,
|
|
1407
|
+
cwd: process.cwd(),
|
|
1408
|
+
env: cleanEnv()
|
|
1409
|
+
},
|
|
1410
|
+
existing
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
function toFleetProc(p, slug) {
|
|
1414
|
+
const env = p.pm2_env ?? {};
|
|
1415
|
+
const name = p.name ?? "?";
|
|
1416
|
+
return {
|
|
1417
|
+
name,
|
|
1418
|
+
displayName: stripNamespacePrefix(name, slug),
|
|
1419
|
+
isCaddy: p.name === CADDY_PROC,
|
|
1420
|
+
status: env.status ?? "unknown",
|
|
1421
|
+
pid: p.pid,
|
|
1422
|
+
since: env.pm_uptime,
|
|
1423
|
+
restarts: env.restart_time,
|
|
1424
|
+
cpu: p.monit?.cpu,
|
|
1425
|
+
memory: p.monit?.memory,
|
|
1426
|
+
port: env.PORT,
|
|
1427
|
+
url: env.DEV_URL,
|
|
1428
|
+
publicUrl: env.DEV_PUBLIC_URL,
|
|
1429
|
+
dir: env.DEV_PROJECT_DIR ?? env.pm_cwd
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
async function getFleet(slug) {
|
|
1433
|
+
const list = await pList();
|
|
1434
|
+
return list.filter((p) => (p.pm2_env ?? {}).namespace === slug).map((p) => toFleetProc(p, slug)).sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
1435
|
+
}
|
|
1436
|
+
async function getSystemCaddy() {
|
|
1437
|
+
const list = await pList();
|
|
1438
|
+
const found = list.find(
|
|
1439
|
+
(p) => p.name === CADDY_PROC && (p.pm2_env ?? {}).namespace === SYSTEM_NAMESPACE
|
|
1440
|
+
);
|
|
1441
|
+
return found ? toFleetProc(found, SYSTEM_NAMESPACE) : null;
|
|
1442
|
+
}
|
|
1443
|
+
async function restartProcs(names) {
|
|
1444
|
+
for (const n of names) await pRestart(n);
|
|
1445
|
+
}
|
|
1446
|
+
async function stopProcs(names) {
|
|
1447
|
+
for (const n of names) await pStop(n);
|
|
1448
|
+
}
|
|
1449
|
+
async function deleteProcs(names) {
|
|
1450
|
+
for (const n of names) await pDelete(n);
|
|
1451
|
+
}
|
|
1452
|
+
function streamLogs(target, lines) {
|
|
1453
|
+
const args = ["logs", "--lines", String(lines)];
|
|
1454
|
+
if (target) args.push(target);
|
|
1455
|
+
return new Promise((done) => {
|
|
1456
|
+
const child = spawn2(PM2_BIN, args, { stdio: "inherit" });
|
|
1457
|
+
child.on("exit", () => done());
|
|
1458
|
+
child.on("error", (err) => {
|
|
1459
|
+
console.error(`pm2 logs \uC2E4\uD589 \uC2E4\uD328: ${err}`);
|
|
1460
|
+
done();
|
|
1461
|
+
});
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// cli/ports.ts
|
|
1466
|
+
import getPort from "get-port";
|
|
1467
|
+
async function allocatePorts(apps, cfg) {
|
|
1468
|
+
const allocated = [];
|
|
1469
|
+
const used = [];
|
|
1470
|
+
for (const app of apps) {
|
|
1471
|
+
const port = await getPort({ exclude: used });
|
|
1472
|
+
used.push(port);
|
|
1473
|
+
const hosts = [];
|
|
1474
|
+
if (app.category === "app" && app.type !== "daemon") {
|
|
1475
|
+
hosts.push(`${app.name}.${cfg.internalDomain}`);
|
|
1476
|
+
if (cfg.publicDomain) hosts.push(`${app.name}.${cfg.publicDomain}`);
|
|
1477
|
+
}
|
|
1478
|
+
allocated.push({ name: app.name, pkg: app.pkg, port, hosts, dir: app.dir });
|
|
1479
|
+
}
|
|
1480
|
+
return allocated;
|
|
1481
|
+
}
|
|
1482
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
1483
|
+
function bindHostFor(upstreamHost) {
|
|
1484
|
+
return LOOPBACK.has(upstreamHost) ? "127.0.0.1" : "0.0.0.0";
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// cli/commands.ts
|
|
1488
|
+
function isAll(selector) {
|
|
1489
|
+
const s = selector?.trim();
|
|
1490
|
+
return !s || s === "all";
|
|
1491
|
+
}
|
|
1492
|
+
function appList(procs) {
|
|
1493
|
+
return procs.map((p) => p.displayName).join(", ") || "(\uC5C6\uC74C)";
|
|
1494
|
+
}
|
|
1495
|
+
async function up(selector) {
|
|
1496
|
+
const { cfg } = await resolveCommandContext();
|
|
1497
|
+
const apps = discoverApps();
|
|
1498
|
+
if (apps.length === 0) {
|
|
1499
|
+
console.error("\n apps/ \uD558\uC704\uC5D0 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uD328\uD0A4\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.\n");
|
|
1500
|
+
process.exit(1);
|
|
1501
|
+
}
|
|
1502
|
+
const selected = await selectApps(apps, selector);
|
|
1503
|
+
if (selected === "devis") {
|
|
1504
|
+
await devis();
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
if (selected === "caddy") {
|
|
1508
|
+
await caddy();
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
const allocated = await allocatePorts(selected, cfg);
|
|
1512
|
+
console.log(`
|
|
1513
|
+
\uC2E4\uD589 \uB300\uC0C1: ${allocated.map((a) => a.name).join(", ")}`);
|
|
1514
|
+
console.log(" \uD3EC\uD2B8 \uD560\uB2F9:");
|
|
1515
|
+
for (const a of allocated) {
|
|
1516
|
+
const target = a.hosts[0] ? `https://${a.hosts[0]}` : "(daemon \u2014 URL \uC5C6\uC74C)";
|
|
1517
|
+
console.log(` ${a.pkg.padEnd(14)} :${a.port} -> ${target}`);
|
|
1518
|
+
}
|
|
1519
|
+
console.log("");
|
|
1520
|
+
const caddyApps = await (async () => {
|
|
1521
|
+
const webApps = allocated.filter((a) => a.hosts.length > 0);
|
|
1522
|
+
if (webApps.length === 0) return null;
|
|
1523
|
+
try {
|
|
1524
|
+
const explicitCert = cfg.tls ? readCert(cfg.tls.cert, cfg.tls.key) : null;
|
|
1525
|
+
return await Promise.all(
|
|
1526
|
+
webApps.map(async (a) => ({
|
|
1527
|
+
name: a.name,
|
|
1528
|
+
port: a.port,
|
|
1529
|
+
hosts: a.hosts,
|
|
1530
|
+
cert: explicitCert ?? await ensureCert(a.hosts[0])
|
|
1531
|
+
}))
|
|
1532
|
+
);
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
console.error(` \uC778\uC99D\uC11C \uC900\uBE44 \uC2E4\uD328 \u2014 HTTPS \uC5C6\uC774 \uACC4\uC18D\uD569\uB2C8\uB2E4: ${err}`);
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
})();
|
|
1538
|
+
await withPm2(async () => {
|
|
1539
|
+
let caddyReady = caddyApps !== null;
|
|
1540
|
+
if (caddyApps) {
|
|
1541
|
+
if (await caddyReachable(cfg.caddyAdmin)) {
|
|
1542
|
+
console.log(" Caddy: \uAE30\uC874 \uC778\uC2A4\uD134\uC2A4 \uC0AC\uC6A9 (\uB77C\uC6B0\uD2B8\uB9CC \uC8FC\uC785)");
|
|
1543
|
+
} else {
|
|
1544
|
+
console.log(" Caddy: \uBBF8\uAC10\uC9C0 \u2014 pm2 \uB85C \uAE30\uB3D9 (\uC2DC\uC2A4\uD15C \uACF5\uC720 \uC778\uC2A4\uD134\uC2A4)");
|
|
1545
|
+
try {
|
|
1546
|
+
await startCaddyProcess(cfg.caddyAdmin);
|
|
1547
|
+
await waitForCaddy(cfg.caddyAdmin);
|
|
1548
|
+
} catch (err) {
|
|
1549
|
+
console.error(` Caddy \uAE30\uB3D9 \uC2E4\uD328 \u2014 HTTPS \uC5C6\uC774 \uACC4\uC18D\uD569\uB2C8\uB2E4: ${err}`);
|
|
1550
|
+
caddyReady = false;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
if (caddyApps && caddyReady) {
|
|
1555
|
+
try {
|
|
1556
|
+
const current = await caddyGetConfig(cfg.caddyAdmin);
|
|
1557
|
+
await caddyLoad(cfg.caddyAdmin, applyOurEntries(current, caddyApps, cfg));
|
|
1558
|
+
console.log(" Caddy: \uB77C\uC6B0\uD2B8 \uC8FC\uC785 \uC644\uB8CC");
|
|
1559
|
+
const updated = new Set(caddyApps.map((a) => a.name));
|
|
1560
|
+
const keep = readRoutes(cfg.workspaceSlug).filter((a) => !updated.has(a.name));
|
|
1561
|
+
const persistedApps = [
|
|
1562
|
+
...keep,
|
|
1563
|
+
...caddyApps.map((a) => ({ name: a.name, hosts: a.hosts, port: a.port }))
|
|
1564
|
+
];
|
|
1565
|
+
writeRoutes(cfg.workspaceSlug, persistedApps);
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
console.error(` Caddy \uB77C\uC6B0\uD2B8 \uC8FC\uC785 \uC2E4\uD328: ${err}`);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
await startApps(allocated, bindHostFor(cfg.upstreamHost), cfg.workspaceSlug);
|
|
1571
|
+
console.log(" \uC571: pm2 \uBC31\uADF8\uB77C\uC6B4\uB4DC \uC2E4\uD589 \uC2DC\uC791");
|
|
1572
|
+
});
|
|
1573
|
+
console.log("\n `pnpm dev` \uAC00 \uB05D\uB098\uB3C4 \uD504\uB85C\uC138\uC2A4\uB294 \uACC4\uC18D \uB3D5\uB2C8\uB2E4. \uC6B4\uC6A9:");
|
|
1574
|
+
console.log(" pnpm dev status \uD504\uB85C\uC138\uC2A4 \uC0C1\uD0DC");
|
|
1575
|
+
console.log(" pnpm dev logs [name] \uB85C\uADF8 \uBCF4\uAE30");
|
|
1576
|
+
console.log(" pnpm dev restart [name] \uC7AC\uC2DC\uC791");
|
|
1577
|
+
console.log(" pnpm dev stop [name] \uC911\uC9C0");
|
|
1578
|
+
console.log(" pnpm dev down [name] \uC815\uB9AC (\uB77C\uC6B0\uD2B8 \uC81C\uAC70 + \uD504\uB85C\uC138\uC2A4 \uC0AD\uC81C)\n");
|
|
1579
|
+
}
|
|
1580
|
+
async function down(selector) {
|
|
1581
|
+
const { cfg } = await resolveCommandContext();
|
|
1582
|
+
await withPm2(async () => {
|
|
1583
|
+
const appProcs = await getFleet(cfg.workspaceSlug);
|
|
1584
|
+
if (appProcs.length === 0) {
|
|
1585
|
+
console.log("\n \uC2E4\uD589 \uC911\uC778 \uD504\uB85C\uC138\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.\n");
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
let targets;
|
|
1589
|
+
if (isAll(selector)) {
|
|
1590
|
+
targets = appProcs;
|
|
1591
|
+
} else {
|
|
1592
|
+
const name = selector?.trim();
|
|
1593
|
+
targets = appProcs.filter((p) => p.displayName === name);
|
|
1594
|
+
if (targets.length === 0) {
|
|
1595
|
+
console.error(`
|
|
1596
|
+
\uC54C \uC218 \uC5C6\uB294 \uD504\uB85C\uC138\uC2A4: "${name}"
|
|
1597
|
+
\uC2E4\uD589 \uC911: ${appList(appProcs)}
|
|
1598
|
+
`);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
const fullNames = new Set(targets.map((p) => p.name));
|
|
1603
|
+
const shortNames = new Set(targets.map((p) => p.displayName));
|
|
1604
|
+
if (shortNames.size > 0 && await caddyReachable(cfg.caddyAdmin)) {
|
|
1605
|
+
try {
|
|
1606
|
+
const current = await caddyGetConfig(cfg.caddyAdmin);
|
|
1607
|
+
await caddyLoad(cfg.caddyAdmin, stripOurEntries(current, shortNames, cfg.workspaceSlug));
|
|
1608
|
+
console.log(" Caddy: \uB77C\uC6B0\uD2B8 \uC81C\uAC70 \uC644\uB8CC");
|
|
1609
|
+
} catch (err) {
|
|
1610
|
+
console.error(` Caddy \uB77C\uC6B0\uD2B8 \uC81C\uAC70 \uC2E4\uD328: ${err}`);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
removeRoutes(cfg.workspaceSlug, shortNames);
|
|
1614
|
+
await deleteProcs([...fullNames]);
|
|
1615
|
+
console.log(`
|
|
1616
|
+
\uC815\uB9AC \uC644\uB8CC: ${[...shortNames].join(", ")}
|
|
1617
|
+
`);
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
async function logs(selector) {
|
|
1621
|
+
setPm2HomeEnv();
|
|
1622
|
+
const arg = selector?.trim();
|
|
1623
|
+
if (arg) {
|
|
1624
|
+
const wsRoot = findWorkspaceRootStrict();
|
|
1625
|
+
const target2 = wsRoot ? `${(await loadConfig(wsRoot)).workspaceSlug}__${arg}` : arg;
|
|
1626
|
+
await streamLogs(target2, 500);
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
const selection = await selectApps(discoverApps(), void 0, "\uB85C\uADF8\uB97C \uBCFC \uB300\uC0C1\uC744 \uC120\uD0DD\uD558\uC138\uC694");
|
|
1630
|
+
let target;
|
|
1631
|
+
if (selection === "devis" || selection === "caddy") {
|
|
1632
|
+
target = selection;
|
|
1633
|
+
} else if (selection.length === 1) {
|
|
1634
|
+
const wsRoot = findWorkspaceRootStrict();
|
|
1635
|
+
const slug = wsRoot ? (await loadConfig(wsRoot)).workspaceSlug : "";
|
|
1636
|
+
target = slug ? `${slug}__${selection[0].name}` : selection[0].name;
|
|
1637
|
+
} else {
|
|
1638
|
+
target = null;
|
|
1639
|
+
}
|
|
1640
|
+
await streamLogs(target, 500);
|
|
1641
|
+
}
|
|
1642
|
+
async function restart(selector) {
|
|
1643
|
+
await applyToApps(selector, "\uC7AC\uC2DC\uC791", restartProcs);
|
|
1644
|
+
}
|
|
1645
|
+
async function stop(selector) {
|
|
1646
|
+
await applyToApps(selector, "\uC911\uC9C0", stopProcs);
|
|
1647
|
+
}
|
|
1648
|
+
async function applyToApps(selector, label, action) {
|
|
1649
|
+
const { cfg } = await resolveCommandContext();
|
|
1650
|
+
await withPm2(async () => {
|
|
1651
|
+
const appProcs = await getFleet(cfg.workspaceSlug);
|
|
1652
|
+
let targets;
|
|
1653
|
+
if (isAll(selector)) {
|
|
1654
|
+
targets = appProcs;
|
|
1655
|
+
} else {
|
|
1656
|
+
const name = selector?.trim();
|
|
1657
|
+
const found = appProcs.find((p) => p.displayName === name);
|
|
1658
|
+
if (!found) {
|
|
1659
|
+
console.error(`
|
|
1660
|
+
\uC54C \uC218 \uC5C6\uB294 \uD504\uB85C\uC138\uC2A4: "${name}"
|
|
1661
|
+
\uC2E4\uD589 \uC911: ${appList(appProcs)}
|
|
1662
|
+
`);
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
targets = [found];
|
|
1666
|
+
}
|
|
1667
|
+
if (targets.length === 0) {
|
|
1668
|
+
console.log("\n \uB300\uC0C1 \uD504\uB85C\uC138\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.\n");
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
await action(targets.map((p) => p.name));
|
|
1672
|
+
console.log(`
|
|
1673
|
+
${label}: ${targets.map((p) => p.displayName).join(", ")}
|
|
1674
|
+
`);
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
async function status() {
|
|
1678
|
+
const { cfg } = await resolveCommandContext();
|
|
1679
|
+
await withPm2(async () => {
|
|
1680
|
+
const fleet = await getFleet(cfg.workspaceSlug);
|
|
1681
|
+
if (fleet.length === 0) {
|
|
1682
|
+
console.log("\n \uC2E4\uD589 \uC911\uC778 \uD504\uB85C\uC138\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. `pnpm dev` \uB85C \uB744\uC6B0\uC138\uC694.\n");
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
const headers = ["NAME", "STATUS", "PID", "UPTIME", "\u21BA", "MEM", "URL"];
|
|
1686
|
+
const rows = fleet.map((p) => {
|
|
1687
|
+
const online = p.status === "online";
|
|
1688
|
+
return [
|
|
1689
|
+
// 사용자 노출 — pm2 의 full name 대신 shortname.
|
|
1690
|
+
p.displayName,
|
|
1691
|
+
p.status,
|
|
1692
|
+
online && p.pid ? String(p.pid) : "-",
|
|
1693
|
+
online ? fmtSince(p.since) : "-",
|
|
1694
|
+
String(p.restarts ?? 0),
|
|
1695
|
+
online ? fmtBytes(p.memory) : "-",
|
|
1696
|
+
p.url ?? "-"
|
|
1697
|
+
];
|
|
1698
|
+
});
|
|
1699
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
1700
|
+
const line = (cells) => ` ${cells.map((c, i) => c.padEnd(widths[i])).join(" ")}`;
|
|
1701
|
+
console.log(`
|
|
1702
|
+
${cfg.workspaceSlug} fleet (pm2 namespace: ${cfg.workspaceSlug})
|
|
1703
|
+
`);
|
|
1704
|
+
console.log(line(headers));
|
|
1705
|
+
for (const r of rows) console.log(line(r));
|
|
1706
|
+
console.log("");
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
function fmtSince(epoch) {
|
|
1710
|
+
if (!epoch) return "-";
|
|
1711
|
+
const sec = Math.floor((Date.now() - epoch) / 1e3);
|
|
1712
|
+
if (sec < 60) return `${sec}s`;
|
|
1713
|
+
const min = Math.floor(sec / 60);
|
|
1714
|
+
if (min < 60) return `${min}m`;
|
|
1715
|
+
const hr = Math.floor(min / 60);
|
|
1716
|
+
return `${hr}h${min % 60}m`;
|
|
1717
|
+
}
|
|
1718
|
+
function fmtBytes(bytes) {
|
|
1719
|
+
if (!bytes) return "-";
|
|
1720
|
+
const mb = bytes / 1024 / 1024;
|
|
1721
|
+
return mb >= 1 ? `${Math.round(mb)}mb` : `${Math.round(bytes / 1024)}kb`;
|
|
1722
|
+
}
|
|
1723
|
+
function whichCmd(cmd) {
|
|
1724
|
+
try {
|
|
1725
|
+
return execFileSync5("which", [cmd], { encoding: "utf8" }).trim() || cmd;
|
|
1726
|
+
} catch {
|
|
1727
|
+
return cmd;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
function resolvePm2(devisRoot) {
|
|
1731
|
+
const bundled = join12(devisRoot, "node_modules", ".bin", "pm2");
|
|
1732
|
+
return existsSync10(bundled) ? bundled : whichCmd("pm2");
|
|
1733
|
+
}
|
|
1734
|
+
function resolveDevisBin(devisRoot) {
|
|
1735
|
+
return join12(devisRoot, "dist", "cli", "index.js");
|
|
1736
|
+
}
|
|
1737
|
+
function activateExistingApp(devisRoot) {
|
|
1738
|
+
if (process.platform === "darwin") {
|
|
1739
|
+
try {
|
|
1740
|
+
execFileSync5("open", ["-a", appBundlePath(devisRoot)], { stdio: "ignore" });
|
|
1741
|
+
} catch {
|
|
1742
|
+
}
|
|
1743
|
+
} else if (process.platform === "linux") {
|
|
1744
|
+
try {
|
|
1745
|
+
execFileSync5("wmctrl", ["-a", "Devis"], { stdio: "ignore" });
|
|
1746
|
+
} catch {
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
async function dev() {
|
|
1751
|
+
const VITE_URL = process.env.DEVIS_DEV_URL || "http://127.0.0.1:5173";
|
|
1752
|
+
console.log(`
|
|
1753
|
+
devis [dev]: vite dev \uC11C\uBC84 \uAC00\uC815 \u2014 ${VITE_URL}`);
|
|
1754
|
+
console.log(" vite \uAC00 \uC548 \uB5A0 \uC788\uC73C\uBA74 \uB2E4\uB978 \uD130\uBBF8\uB110\uC5D0\uC11C \uBA3C\uC800 `pnpm dev` \uC2E4\uD589.\n");
|
|
1755
|
+
const runningPid = runningInstancePid();
|
|
1756
|
+
if (runningPid !== null) {
|
|
1757
|
+
console.error(
|
|
1758
|
+
` \uAE30\uC874 devis \uC778\uC2A4\uD134\uC2A4(PID ${runningPid}) \uAC00 \uB5A0 \uC788\uC2B5\uB2C8\uB2E4 \u2014 \uBA3C\uC800 \uC885\uB8CC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.
|
|
1759
|
+
(\uCC3D\uC744 \uB2EB\uAC70\uB098 \`kill ${runningPid}\`)
|
|
1760
|
+
`
|
|
1761
|
+
);
|
|
1762
|
+
process.exit(1);
|
|
1763
|
+
}
|
|
1764
|
+
const devisRoot = findDevisRoot();
|
|
1765
|
+
const tools = {
|
|
1766
|
+
pnpm: whichCmd("pnpm"),
|
|
1767
|
+
pm2: resolvePm2(devisRoot),
|
|
1768
|
+
docker: whichCmd("docker"),
|
|
1769
|
+
devis: resolveDevisBin(devisRoot)
|
|
1770
|
+
};
|
|
1771
|
+
const pathEnv = process.env.PATH ?? "";
|
|
1772
|
+
console.log(" devis [dev]: \uB370\uC2A4\uD06C\uD1B1 \uC571 \uC900\uBE44 \uC911...");
|
|
1773
|
+
const app = await buildApp({ devisRoot, tools, pathEnv, devUrl: VITE_URL });
|
|
1774
|
+
const pid = launchApp(app);
|
|
1775
|
+
if (pid !== void 0) writeLockPid(pid);
|
|
1776
|
+
console.log(` devis [dev]: \uC2E4\uD589\uB428 \u2014 ${app.appBundle ?? app.execPath}`);
|
|
1777
|
+
console.log(" ui/ \uD30C\uC77C\uC744 \uC218\uC815\uD558\uBA74 vite HMR \uB85C \uC989\uC2DC \uBC18\uC601\uB429\uB2C8\uB2E4.\n");
|
|
1778
|
+
}
|
|
1779
|
+
async function devis() {
|
|
1780
|
+
const workspace = await autoRegisterFromCwd();
|
|
1781
|
+
if (workspace) {
|
|
1782
|
+
console.log(`
|
|
1783
|
+
devis: \uD65C\uC131 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 = ${workspace.slug} (${workspace.meta.path})`);
|
|
1784
|
+
} else {
|
|
1785
|
+
console.log("\n devis: \uBAA8\uB178\uB808\uD3EC \uBC16\uC5D0\uC11C \uD638\uCD9C \u2014 \uD604\uC7AC \uD65C\uC131 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB85C \uC2E4\uD589");
|
|
1786
|
+
}
|
|
1787
|
+
const runningPid = runningInstancePid();
|
|
1788
|
+
if (runningPid !== null) {
|
|
1789
|
+
activateExistingApp(findDevisRoot());
|
|
1790
|
+
console.log(` devis: \uAE30\uC874 \uC778\uC2A4\uD134\uC2A4(PID ${runningPid}) \uAC00 \uC2E4\uD589 \uC911 \u2014 active \uB9CC \uC804\uD658\uD588\uC2B5\uB2C8\uB2E4.
|
|
1791
|
+
`);
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
const devisRoot = findDevisRoot();
|
|
1795
|
+
const tools = {
|
|
1796
|
+
pnpm: whichCmd("pnpm"),
|
|
1797
|
+
pm2: resolvePm2(devisRoot),
|
|
1798
|
+
docker: whichCmd("docker"),
|
|
1799
|
+
devis: resolveDevisBin(devisRoot)
|
|
1800
|
+
};
|
|
1801
|
+
const pathEnv = process.env.PATH ?? "";
|
|
1802
|
+
console.log(" devis: \uB370\uC2A4\uD06C\uD1B1 \uC571 \uC900\uBE44 \uC911...");
|
|
1803
|
+
const app = await buildApp({ devisRoot, tools, pathEnv });
|
|
1804
|
+
const pid = launchApp(app);
|
|
1805
|
+
if (pid !== void 0) writeLockPid(pid);
|
|
1806
|
+
console.log(` devis: \uC2E4\uD589\uB428 \u2014 ${app.appBundle ?? app.execPath}`);
|
|
1807
|
+
console.log(" \uC791\uC5C5\uD45C\uC2DC\uC904/Dock \uC5D0 \uACE0\uC815\uD558\uBA74 \uD074\uB9AD\uB9CC\uC73C\uB85C \uB2E4\uC2DC \uC5F4 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n");
|
|
1808
|
+
}
|
|
1809
|
+
function formatWorkspace(w, activeSlug) {
|
|
1810
|
+
const mark = w.slug === activeSlug ? "*" : " ";
|
|
1811
|
+
const label = w.meta.label ? ` "${w.meta.label}"` : "";
|
|
1812
|
+
return ` ${mark} ${w.slug.padEnd(24)} ${w.meta.path}${label}`;
|
|
1813
|
+
}
|
|
1814
|
+
async function register(arg) {
|
|
1815
|
+
const start = arg ? join12(process.cwd(), arg) : process.cwd();
|
|
1816
|
+
const wsRoot = findWorkspaceRootStrict(start);
|
|
1817
|
+
if (!wsRoot) {
|
|
1818
|
+
console.error(`
|
|
1819
|
+
pnpm-workspace.yaml \uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uBAA8\uB178\uB808\uD3EC\uAC00 \uC544\uB2D9\uB2C8\uB2E4: ${start}
|
|
1820
|
+
`);
|
|
1821
|
+
process.exit(1);
|
|
1822
|
+
}
|
|
1823
|
+
const cfg = await loadConfig(wsRoot);
|
|
1824
|
+
const ws = registerWorkspace({ path: wsRoot, desiredSlug: cfg.workspaceSlug });
|
|
1825
|
+
writeActiveSlug(ws.slug);
|
|
1826
|
+
console.log(`
|
|
1827
|
+
\uB4F1\uB85D \uC644\uB8CC: ${ws.slug} \u2192 ${ws.meta.path}
|
|
1828
|
+
(\uD65C\uC131 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB85C \uC124\uC815)
|
|
1829
|
+
`);
|
|
1830
|
+
}
|
|
1831
|
+
async function unregister(arg) {
|
|
1832
|
+
if (!arg) {
|
|
1833
|
+
console.error("\n \uC0AC\uC6A9\uBC95: devis unregister <slug|path>\n");
|
|
1834
|
+
process.exit(1);
|
|
1835
|
+
}
|
|
1836
|
+
let slug = arg;
|
|
1837
|
+
if (arg.includes("/") || arg.startsWith(".")) {
|
|
1838
|
+
const abs = join12(process.cwd(), arg);
|
|
1839
|
+
const found = findByPath(abs);
|
|
1840
|
+
if (found) slug = found.slug;
|
|
1841
|
+
}
|
|
1842
|
+
const removed = unregisterWorkspace(slug);
|
|
1843
|
+
if (!removed) {
|
|
1844
|
+
console.error(`
|
|
1845
|
+
\uB4F1\uB85D\uB418\uC9C0 \uC54A\uC740 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4: ${arg}
|
|
1846
|
+
`);
|
|
1847
|
+
process.exit(1);
|
|
1848
|
+
}
|
|
1849
|
+
if (readActiveSlug() === slug) writeActiveSlug("");
|
|
1850
|
+
console.log(`
|
|
1851
|
+
\uD574\uC81C \uC644\uB8CC: ${slug}
|
|
1852
|
+
`);
|
|
1853
|
+
}
|
|
1854
|
+
async function workspaces() {
|
|
1855
|
+
const all = listWorkspaces();
|
|
1856
|
+
if (all.length === 0) {
|
|
1857
|
+
console.log("\n \uB4F1\uB85D\uB41C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. `devis register` \uB85C \uCD94\uAC00\uD558\uC138\uC694.\n");
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const active = readActiveSlug();
|
|
1861
|
+
console.log(`
|
|
1862
|
+
\uB4F1\uB85D \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 (* = \uD65C\uC131, ${all.length}\uAC1C):
|
|
1863
|
+
`);
|
|
1864
|
+
for (const w of all.sort((a, b) => (b.meta.lastUsedAt ?? "").localeCompare(a.meta.lastUsedAt ?? ""))) {
|
|
1865
|
+
console.log(formatWorkspace(w, active));
|
|
1866
|
+
}
|
|
1867
|
+
console.log("");
|
|
1868
|
+
}
|
|
1869
|
+
async function isOurCaddyRunning(caddyAdmin) {
|
|
1870
|
+
const ourProc = await getSystemCaddy();
|
|
1871
|
+
const reachable = await caddyReachable(caddyAdmin);
|
|
1872
|
+
const ours = ourProc?.status === "online" && reachable;
|
|
1873
|
+
return { ours, ourProc, reachable };
|
|
1874
|
+
}
|
|
1875
|
+
async function startSystemCaddy(caddyAdmin) {
|
|
1876
|
+
try {
|
|
1877
|
+
await startCaddyProcess(caddyAdmin);
|
|
1878
|
+
await waitForCaddy(caddyAdmin);
|
|
1879
|
+
} catch (err) {
|
|
1880
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1881
|
+
const hint = /EADDRINUSE|address already in use|listen.*in use/i.test(msg) || /admin endpoint.*not available/i.test(msg) ? "\n\u2192 \uB2E4\uB978 \uD504\uB85C\uC138\uC2A4\uAC00 caddy \uAC00 \uC4F0\uB824\uB294 \uD3EC\uD2B8(2019 / 80 / 443)\uB97C \uC810\uC720\uD558\uACE0 \uC788\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4." : "";
|
|
1882
|
+
throw new Error(`Caddy \uAE30\uB3D9 \uC2E4\uD328: ${msg}${hint}`);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
async function syncRoutesFromStorage(caddyAdmin) {
|
|
1886
|
+
const result = await restoreAllRoutes(caddyAdmin);
|
|
1887
|
+
if (result.workspaces === 0) {
|
|
1888
|
+
console.log(" Caddy: \uBCF5\uC6D0\uD560 \uC601\uC18D routes \uC5C6\uC74C");
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
console.log(` Caddy: ${result.workspaces}\uAC1C \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC758 ${result.routes}\uAC1C \uB77C\uC6B0\uD2B8\uB97C \uBCF5\uC6D0\uD588\uC2B5\uB2C8\uB2E4`);
|
|
1892
|
+
for (const err of result.errors) {
|
|
1893
|
+
console.error(` Caddy: \uB77C\uC6B0\uD2B8 \uBCF5\uC6D0 \uC77C\uBD80 \uC2E4\uD328 \u2014 ${err}`);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
async function caddy(action) {
|
|
1897
|
+
const sub = action?.trim() ?? "start";
|
|
1898
|
+
setPm2HomeEnv();
|
|
1899
|
+
const caddyAdmin = await resolveCaddyAdmin();
|
|
1900
|
+
await withPm2(async () => {
|
|
1901
|
+
const state = await isOurCaddyRunning(caddyAdmin);
|
|
1902
|
+
if (sub === "status") {
|
|
1903
|
+
printCaddyStatus(state);
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (sub === "stop") {
|
|
1907
|
+
if (!state.ourProc) {
|
|
1908
|
+
console.log("\n Caddy: \uB744\uC6CC\uB454 \uD504\uB85C\uC138\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.\n");
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
await deleteProcs([state.ourProc.name]);
|
|
1912
|
+
console.log("\n Caddy: \uC885\uB8CC \uC644\uB8CC\n");
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
if (sub === "restart") {
|
|
1916
|
+
if (state.ourProc) await deleteProcs([state.ourProc.name]);
|
|
1917
|
+
console.log("\n Caddy: \uC7AC\uAE30\uB3D9...");
|
|
1918
|
+
await startSystemCaddy(caddyAdmin);
|
|
1919
|
+
console.log(" Caddy: \uC7AC\uAE30\uB3D9 \uC644\uB8CC");
|
|
1920
|
+
await syncRoutesFromStorage(caddyAdmin);
|
|
1921
|
+
console.log("");
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
if (sub === "sync") {
|
|
1925
|
+
if (!state.reachable) {
|
|
1926
|
+
throw new Error("Caddy \uAC00 \uC751\uB2F5\uD558\uC9C0 \uC54A\uC544 \uB77C\uC6B0\uD2B8\uB97C \uC8FC\uC785\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uBA3C\uC800 `caddy start` \uD558\uC138\uC694.");
|
|
1927
|
+
}
|
|
1928
|
+
console.log("\n Caddy: \uC601\uC18D routes \uB3D9\uAE30\uD654 \uC911...");
|
|
1929
|
+
await syncRoutesFromStorage(caddyAdmin);
|
|
1930
|
+
console.log("");
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
if (state.ours) {
|
|
1934
|
+
console.log("\n Caddy: \uC774\uBBF8 \uB5A0 \uC788\uC2B5\uB2C8\uB2E4 \u2014 \uC601\uC18D routes \uB9CC \uB3D9\uAE30\uD654\uD569\uB2C8\uB2E4.");
|
|
1935
|
+
await syncRoutesFromStorage(caddyAdmin);
|
|
1936
|
+
console.log("");
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
if (!state.ourProc && state.reachable) {
|
|
1940
|
+
throw new Error(
|
|
1941
|
+
"Caddy admin \uD3EC\uD2B8(2019)\uB97C \uB2E4\uB978 \uD504\uB85C\uC138\uC2A4\uAC00 \uC810\uC720 \uC911\uC785\uB2C8\uB2E4.\ndevis \uC5D0\uC11C \uAD00\uB9AC\uD558\uB824\uBA74 \uBA3C\uC800 \uADF8 \uD504\uB85C\uC138\uC2A4\uB97C \uC885\uB8CC\uD558\uC138\uC694. \uC678\uBD80 Caddy \uB97C \uADF8\uB300\uB85C \uB450\uACE0 \uB77C\uC6B0\uD2B8\uB9CC \uC8FC\uC785\uD558\uB824\uBA74 `devis caddy sync` \uB97C \uC0AC\uC6A9\uD558\uC138\uC694."
|
|
1942
|
+
);
|
|
1943
|
+
}
|
|
1944
|
+
console.log(state.ourProc ? "\n Caddy: pm2 entry \uAC00 \uC788\uC73C\uB098 \uB3D9\uC791\uD558\uC9C0 \uC54A\uC74C \u2014 \uC7AC\uAE30\uB3D9..." : "\n Caddy: pm2 \uB85C \uAE30\uB3D9...");
|
|
1945
|
+
await startSystemCaddy(caddyAdmin);
|
|
1946
|
+
console.log(" Caddy: \uAE30\uB3D9 \uC644\uB8CC");
|
|
1947
|
+
await syncRoutesFromStorage(caddyAdmin);
|
|
1948
|
+
console.log("");
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
async function resolveCaddyAdmin() {
|
|
1952
|
+
const wsRoot = findWorkspaceRootStrict();
|
|
1953
|
+
if (!wsRoot) return "http://localhost:2019";
|
|
1954
|
+
const cfg = await loadConfig(wsRoot);
|
|
1955
|
+
return cfg.caddyAdmin;
|
|
1956
|
+
}
|
|
1957
|
+
function printCaddyStatus(state) {
|
|
1958
|
+
console.log("");
|
|
1959
|
+
if (state.ours) {
|
|
1960
|
+
console.log(" Caddy: \uC815\uC0C1 \uC791\uB3D9 \uC911 (devis \uAC00 \uB744\uC6B4 \uC778\uC2A4\uD134\uC2A4)");
|
|
1961
|
+
} else if (state.reachable) {
|
|
1962
|
+
console.log(" Caddy: admin \uD3EC\uD2B8(2019)\uC5D0 \uC751\uB2F5 \u2014 \uC678\uBD80 \uC778\uC2A4\uD134\uC2A4\uAC00 \uC810\uC720 \uC911");
|
|
1963
|
+
} else if (state.ourProc) {
|
|
1964
|
+
console.log(` Caddy: pm2 entry \uC788\uC74C (\uC0C1\uD0DC: ${state.ourProc.status}) \u2014 admin \uC751\uB2F5 \uC5C6\uC74C`);
|
|
1965
|
+
} else {
|
|
1966
|
+
console.log(" Caddy: \uB5A0 \uC788\uC9C0 \uC54A\uC74C");
|
|
1967
|
+
}
|
|
1968
|
+
console.log("");
|
|
1969
|
+
}
|
|
1970
|
+
async function pm2Passthrough(args) {
|
|
1971
|
+
setPm2HomeEnv();
|
|
1972
|
+
return new Promise((_done, fail) => {
|
|
1973
|
+
const child = spawn3(PM2_BIN, args, { stdio: "inherit", env: process.env });
|
|
1974
|
+
child.on("exit", (code) => {
|
|
1975
|
+
process.exit(code ?? 0);
|
|
1976
|
+
});
|
|
1977
|
+
child.on("error", (err) => {
|
|
1978
|
+
fail(new Error(`pm2 \uC2E4\uD589 \uC2E4\uD328: ${err instanceof Error ? err.message : String(err)}`));
|
|
1979
|
+
});
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// cli/config/ensure-config.ts
|
|
1984
|
+
import { existsSync as existsSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
1985
|
+
import { join as join14 } from "path";
|
|
1986
|
+
|
|
1987
|
+
// cli/config/sync-schema.ts
|
|
1988
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
|
|
1989
|
+
import { dirname as dirname5, join as join13 } from "path";
|
|
1990
|
+
var SCHEMA_REL_PATH = "./node_modules/.cache/devis/schema.json";
|
|
1991
|
+
function schemaCachePath(workspaceRoot) {
|
|
1992
|
+
return join13(workspaceRoot, "node_modules", ".cache", "devis", "schema.json");
|
|
1993
|
+
}
|
|
1994
|
+
function bundledSchemaPath() {
|
|
1995
|
+
return join13(findDevisRoot(), "schema", "devis.schema.json");
|
|
1996
|
+
}
|
|
1997
|
+
function syncSchema(workspaceRoot) {
|
|
1998
|
+
const source = bundledSchemaPath();
|
|
1999
|
+
if (!existsSync11(source)) return;
|
|
2000
|
+
const target = schemaCachePath(workspaceRoot);
|
|
2001
|
+
const src = readFileSync11(source, "utf8");
|
|
2002
|
+
if (existsSync11(target)) {
|
|
2003
|
+
try {
|
|
2004
|
+
if (readFileSync11(target, "utf8") === src) return;
|
|
2005
|
+
} catch {
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
mkdirSync8(dirname5(target), { recursive: true });
|
|
2009
|
+
writeFileSync8(target, src);
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// cli/config/ensure-config.ts
|
|
2013
|
+
function configExists(workspaceRoot) {
|
|
2014
|
+
for (const ext of [".jsonc", ".json"]) {
|
|
2015
|
+
const p = join14(workspaceRoot, `devis.config${ext}`);
|
|
2016
|
+
if (existsSync12(p)) return p;
|
|
2017
|
+
}
|
|
2018
|
+
return null;
|
|
2019
|
+
}
|
|
2020
|
+
function buildTemplate(workspaceSlug) {
|
|
2021
|
+
return `// devis \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC124\uC815.
|
|
2022
|
+
// \uC774 \uD30C\uC77C\uC740 \uD300 \uACF5\uC6A9\uC73C\uB85C \uCEE4\uBC0B\uD55C\uB2E4. \uAC1C\uC778 \uC624\uBC84\uB77C\uC774\uB4DC\uB294 \uAC19\uC740 \uC704\uCE58\uC5D0
|
|
2023
|
+
// \`devis.config.local.jsonc\` \uB97C \uB9CC\uB4E4\uC5B4 \uB3D9\uC77C\uD55C \uD544\uB4DC\uB97C \uC791\uC131\uD558\uBA74 local \uC774 base \uB97C
|
|
2024
|
+
// \uB36E\uC5B4\uC4F4\uB2E4. local \uD30C\uC77C\uC740 .gitignore \uC5D0 \uCD94\uAC00\uD558\uB294 \uAC83\uC744 \uAD8C\uC7A5.
|
|
2025
|
+
//
|
|
2026
|
+
// \uC790\uB3D9\uC644\uC131\xB7\uAC80\uC99D\uC740 \uC606\uC5D0 \uC704\uCE58\uD55C \`$schema\` \uAC00 \uB2F4\uB2F9\uD55C\uB2E4. \uC774 schema \uD30C\uC77C\uC740
|
|
2027
|
+
// devis CLI \uAC00 \uC2E4\uD589\uB420 \uB54C\uB9C8\uB2E4 \uAC31\uC2E0\uB41C\uB2E4 (\`node_modules/.cache/devis/schema.json\`).
|
|
2028
|
+
{
|
|
2029
|
+
"$schema": "${SCHEMA_REL_PATH}",
|
|
2030
|
+
|
|
2031
|
+
// Caddy @id \uC811\uB450\uC5B4\uC774\uC790 pm2 namespace \uB85C \uC4F0\uC774\uB294 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC2DD\uBCC4\uC790.
|
|
2032
|
+
// \`<workspaceSlug>:route:<app>\` \uD615\uD0DC\uB85C devis \uAC00 \uAD00\uB9AC\uD558\uB294 \uC5D4\uD2B8\uB9AC\uB97C \uC2DD\uBCC4\uD55C\uB2E4.
|
|
2033
|
+
// \uAE30\uBCF8\uAC12\uC740 \uB8E8\uD2B8 package.json \uC758 name \uC5D0\uC11C \uC790\uB3D9 \uCD94\uCD9C\uB428 (@scope/foo \u2192 scope-foo).
|
|
2034
|
+
"workspaceSlug": "${workspaceSlug}"
|
|
2035
|
+
|
|
2036
|
+
// \uB0B4\uBD80(\uB8E8\uD504\uBC31) HTTPS \uBCA0\uC774\uC2A4 \uB3C4\uBA54\uC778. \uAE30\uBCF8 \`<workspaceSlug>.lvh.me\`
|
|
2037
|
+
// (lvh.me \uB294 \uACF5\uAC1C DNS \uAC00 *.lvh.me \u2192 127.0.0.1 \uC744 \uC751\uB2F5\uD574 /etc/hosts \uC218\uC815\uC774 \uD544\uC694 \uC5C6\uB2E4).
|
|
2038
|
+
// "internalDomain": "myapp.lvh.me",
|
|
2039
|
+
|
|
2040
|
+
// \uC678\uBD80 \uC811\uADFC\uC6A9 \uD37C\uBE14\uB9AD \uB3C4\uBA54\uC778 (\uC635\uC158). \uC124\uC815 \uC2DC \`<app>.<publicDomain>\` \uC0AC\uC774\uD2B8\uAC00
|
|
2041
|
+
// ACME(Let's Encrypt) \uC778\uC99D\uC11C\uC640 \uD568\uAED8 \uCD94\uAC00\uB85C \uB4F1\uB85D\uB41C\uB2E4.
|
|
2042
|
+
// "publicDomain": "dev.example.com",
|
|
2043
|
+
|
|
2044
|
+
// Caddy Admin API \uC8FC\uC18C. \uAE30\uBCF8 http://localhost:2019
|
|
2045
|
+
// "caddyAdmin": "http://localhost:2019",
|
|
2046
|
+
|
|
2047
|
+
// \uAE30\uC874 \uB85C\uCEEC \uC778\uC99D\uC11C \uC0AC\uC6A9 (\uC635\uC158). devcert CA \uB300\uC2E0 \uC9C1\uC811 \uBC1C\uAE09\uD55C \uC778\uC99D\uC11C\uB97C \uC4F8 \uB54C.
|
|
2048
|
+
// "tls": { "cert": "/path/to/cert.pem", "key": "/path/to/key.pem" }
|
|
2049
|
+
}
|
|
2050
|
+
`;
|
|
2051
|
+
}
|
|
2052
|
+
function ensureConfig(workspaceRoot) {
|
|
2053
|
+
const existing = configExists(workspaceRoot);
|
|
2054
|
+
if (existing) return null;
|
|
2055
|
+
const target = join14(workspaceRoot, "devis.config.jsonc");
|
|
2056
|
+
const workspaceSlug = defaultSlugFrom(workspaceRoot);
|
|
2057
|
+
writeFileSync9(target, buildTemplate(workspaceSlug));
|
|
2058
|
+
return target;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// cli/logger.ts
|
|
2062
|
+
import { appendFileSync, mkdirSync as mkdirSync9 } from "fs";
|
|
2063
|
+
import { join as join15 } from "path";
|
|
2064
|
+
var currentCommand = "devis";
|
|
2065
|
+
function setLogContext(command) {
|
|
2066
|
+
currentCommand = command;
|
|
2067
|
+
}
|
|
2068
|
+
function logFilePath() {
|
|
2069
|
+
return join15(logsDir(), "devis.log");
|
|
2070
|
+
}
|
|
2071
|
+
var ANSI_RE = /\[[0-9;]*[a-zA-Z]/g;
|
|
2072
|
+
function stripAnsi(s) {
|
|
2073
|
+
return s.replace(ANSI_RE, "");
|
|
2074
|
+
}
|
|
2075
|
+
function joinArgs(args) {
|
|
2076
|
+
return args.map((a) => {
|
|
2077
|
+
if (typeof a === "string") return a;
|
|
2078
|
+
if (a instanceof Error) return a.stack ?? a.message;
|
|
2079
|
+
try {
|
|
2080
|
+
return JSON.stringify(a);
|
|
2081
|
+
} catch {
|
|
2082
|
+
return String(a);
|
|
2083
|
+
}
|
|
2084
|
+
}).join(" ");
|
|
2085
|
+
}
|
|
2086
|
+
function append(level, message) {
|
|
2087
|
+
try {
|
|
2088
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2089
|
+
const tag = level === "INFO" ? "" : ` [${level}]`;
|
|
2090
|
+
appendFileSync(logFilePath(), `[${timestamp}] [${currentCommand}]${tag} ${stripAnsi(message)}
|
|
2091
|
+
`);
|
|
2092
|
+
} catch {
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
var installed = false;
|
|
2096
|
+
function installFileLogger() {
|
|
2097
|
+
if (installed) return;
|
|
2098
|
+
installed = true;
|
|
2099
|
+
try {
|
|
2100
|
+
mkdirSync9(logsDir(), { recursive: true });
|
|
2101
|
+
} catch {
|
|
2102
|
+
}
|
|
2103
|
+
const origLog = console.log;
|
|
2104
|
+
const origErr = console.error;
|
|
2105
|
+
const origWarn = console.warn;
|
|
2106
|
+
console.log = (...args) => {
|
|
2107
|
+
origLog(...args);
|
|
2108
|
+
append("INFO", joinArgs(args));
|
|
2109
|
+
};
|
|
2110
|
+
console.error = (...args) => {
|
|
2111
|
+
origErr(...args);
|
|
2112
|
+
append("ERROR", joinArgs(args));
|
|
2113
|
+
};
|
|
2114
|
+
console.warn = (...args) => {
|
|
2115
|
+
origWarn(...args);
|
|
2116
|
+
append("WARN", joinArgs(args));
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// cli/index.ts
|
|
2121
|
+
var COMMANDS = {
|
|
2122
|
+
up,
|
|
2123
|
+
down,
|
|
2124
|
+
status,
|
|
2125
|
+
ps: status,
|
|
2126
|
+
logs,
|
|
2127
|
+
restart,
|
|
2128
|
+
stop,
|
|
2129
|
+
serve: devis,
|
|
2130
|
+
caddy,
|
|
2131
|
+
register,
|
|
2132
|
+
unregister,
|
|
2133
|
+
workspaces,
|
|
2134
|
+
list: workspaces,
|
|
2135
|
+
dev
|
|
2136
|
+
};
|
|
2137
|
+
var HELP_FLAGS = /* @__PURE__ */ new Set(["-h", "--help", "help"]);
|
|
2138
|
+
var HELP_TEXT = `devis \u2014 pnpm \uBAA8\uB178\uB808\uD3EC \uB85C\uCEEC \uAC1C\uBC1C \uB370\uC2A4\uD06C\uD1B1 \uC571
|
|
2139
|
+
|
|
2140
|
+
USAGE
|
|
2141
|
+
devis [command] [args]
|
|
2142
|
+
|
|
2143
|
+
WORKSPACE
|
|
2144
|
+
devis register [path] \uBAA8\uB178\uB808\uD3EC \uB4F1\uB85D (\uAE30\uBCF8: cwd)
|
|
2145
|
+
devis unregister <slug|path> \uB4F1\uB85D \uD574\uC81C
|
|
2146
|
+
devis workspaces (list) \uB4F1\uB85D \uBAA9\uB85D (* = \uD65C\uC131)
|
|
2147
|
+
|
|
2148
|
+
PROCESS \u2014 cwd \uC758 \uBAA8\uB178\uB808\uD3EC \uB300\uC0C1
|
|
2149
|
+
devis up [name|all] \uC571 \uC2E4\uD589 (\uD3EC\uD2B8\xB7\uC778\uC99D\uC11C\xB7Caddy\xB7pm2 \uC790\uB3D9)
|
|
2150
|
+
devis down [name|all] \uC815\uB9AC (\uB77C\uC6B0\uD2B8 \uC81C\uAC70 + pm2 \uD504\uB85C\uC138\uC2A4 \uC0AD\uC81C)
|
|
2151
|
+
devis status (ps) \uD504\uB85C\uC138\uC2A4 \uD45C
|
|
2152
|
+
devis logs [name] \uB85C\uADF8 \uC2A4\uD2B8\uB9AC\uBC0D (Ctrl+C)
|
|
2153
|
+
devis restart [name|all] \uC7AC\uC2DC\uC791
|
|
2154
|
+
devis stop [name|all] \uC911\uC9C0
|
|
2155
|
+
|
|
2156
|
+
CADDY \u2014 \uC2DC\uC2A4\uD15C \uACF5\uC720 \uC778\uC2A4\uD134\uC2A4
|
|
2157
|
+
devis caddy [action] action: start | stop | restart | status | sync
|
|
2158
|
+
\uAE30\uBCF8 start. start/restart \uD6C4 \uC601\uC18D routes \uC790\uB3D9 \uBCF5\uC6D0
|
|
2159
|
+
|
|
2160
|
+
PM2 \u2014 devis \uC804\uC6A9 PM2_HOME(~/.devis/pm2) \uC73C\uB85C pm2 CLI \uC9C1\uC811 \uD638\uCD9C
|
|
2161
|
+
devis pm2 [args...] \uC608: devis pm2 list / devis pm2 monit
|
|
2162
|
+
devis pm2 logs <name> / devis pm2 flush
|
|
2163
|
+
|
|
2164
|
+
UI / DEVELOPMENT
|
|
2165
|
+
devis \uB370\uC2A4\uD06C\uD1B1 \uC571 \uC2E4\uD589 (cwd \uBAA8\uB178\uB808\uD3EC\uBA74 \uC790\uB3D9 \uB4F1\uB85D)
|
|
2166
|
+
devis dev vite dev \uC11C\uBC84 \uB85C\uB4DC (HMR; \uBCC4\uB3C4 \uD130\uBBF8\uB110\uC5D0\uC11C \`pnpm dev\`)
|
|
2167
|
+
devis serve \`devis\` \uC640 \uB3D9\uC77C
|
|
2168
|
+
|
|
2169
|
+
ENV
|
|
2170
|
+
DEVIS_HOME \uD648 \uB514\uB809\uD130\uB9AC \uC704\uCE58 (\uAE30\uBCF8 ~/.devis)
|
|
2171
|
+
DEVIS_DEV_URL dev \uBAA8\uB4DC vite URL (\uAE30\uBCF8 http://127.0.0.1:5173)
|
|
2172
|
+
DEVIS_SKIP_CADDY postinstall \uC758 caddy \uB2E4\uC6B4\uB85C\uB4DC \uC2A4\uD0B5
|
|
2173
|
+
|
|
2174
|
+
\uC790\uC138\uD55C \uC124\uACC4 \uACB0\uC815\uC740 docs/adr/ \uB97C \uCC38\uACE0\uD558\uC138\uC694.`;
|
|
2175
|
+
function bootstrapWorkspace() {
|
|
2176
|
+
const wsRoot = findWorkspaceRootStrict();
|
|
2177
|
+
if (!wsRoot) return;
|
|
2178
|
+
syncSchema(wsRoot);
|
|
2179
|
+
const created = ensureConfig(wsRoot);
|
|
2180
|
+
if (created) {
|
|
2181
|
+
console.log(`
|
|
2182
|
+
devis.config.jsonc \uB97C \uC0DD\uC131\uD588\uC2B5\uB2C8\uB2E4: ${created}`);
|
|
2183
|
+
console.log(" \uD544\uC694\uD55C \uD56D\uBAA9\uC758 \uC8FC\uC11D\uC744 \uD480\uC5B4 \uD3B8\uC9D1\uD558\uC138\uC694.\n");
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
async function main() {
|
|
2187
|
+
const argv = process.argv.slice(2);
|
|
2188
|
+
const first = argv[0];
|
|
2189
|
+
if (first && HELP_FLAGS.has(first)) {
|
|
2190
|
+
console.log(HELP_TEXT);
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
installFileLogger();
|
|
2194
|
+
if (first === "pm2") {
|
|
2195
|
+
setLogContext("pm2");
|
|
2196
|
+
await pm2Passthrough(argv.slice(1));
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
bootstrapWorkspace();
|
|
2200
|
+
setLogContext(first ?? "devis");
|
|
2201
|
+
if (!first) {
|
|
2202
|
+
await devis();
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
const handler = COMMANDS[first];
|
|
2206
|
+
if (handler) {
|
|
2207
|
+
await handler(argv[1]);
|
|
2208
|
+
} else {
|
|
2209
|
+
setLogContext("up");
|
|
2210
|
+
await up(first);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
main().then(() => process.exit(0)).catch((err) => {
|
|
2214
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2215
|
+
console.error(msg);
|
|
2216
|
+
process.exit(1);
|
|
2217
|
+
});
|