chiwaa 0.1.16 → 0.1.18
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/README.md +2 -0
- package/bin/chiwaa.js +101 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ npx chiwaa deploy --preview # 미리보기 주소
|
|
|
14
14
|
| `vercel logs` · `vercel rollback` | `npx chiwaa logs` · `npx chiwaa rollback` |
|
|
15
15
|
| `vercel domains add` | `npx chiwaa domains add` |
|
|
16
16
|
|
|
17
|
+
서버 기능 있는 Next.js·Express(`npm start`)·Dockerfile 앱은 소스를 보내 컨테이너로 실행합니다(앱은 `PORT`=3000 에서 듣기). 기록은 `npx chiwaa logs -f`.
|
|
18
|
+
|
|
17
19
|
AI 에이전트(Claude Code·Codex·Cursor)는 https://chiwaa.kr/llms.txt 를 읽고 배포합니다. CI 에서는 환경변수 `CHIWAA_TOKEN`.
|
|
18
20
|
|
|
19
21
|
처음이면 브라우저에서 로그인을 승인하고 사이트 이름을 정합니다. 끝나면 `<이름>.chiwaa.com` 주소가 나옵니다.
|
package/bin/chiwaa.js
CHANGED
|
@@ -92,11 +92,12 @@ function parseArgs(argv) {
|
|
|
92
92
|
if (a.startsWith("--")) {
|
|
93
93
|
const [k, v] = a.slice(2).split("=", 2);
|
|
94
94
|
if (v !== undefined) out[k] = v;
|
|
95
|
-
else if (["name", "token", "dir", "read", "write", "page", "days", "lines"].includes(k) && argv[i + 1] && !argv[i + 1].startsWith("-")) out[k] = argv[++i];
|
|
95
|
+
else if (["name", "token", "dir", "read", "write", "page", "days", "lines", "branch"].includes(k) && argv[i + 1] && !argv[i + 1].startsWith("-")) out[k] = argv[++i];
|
|
96
96
|
else out[k] = true;
|
|
97
97
|
} else if (a === "-y") out.yes = true;
|
|
98
98
|
else if (a === "-h") out.help = true;
|
|
99
99
|
else if (a === "-v") out.version = true;
|
|
100
|
+
else if (a === "-f") out.f = true;
|
|
100
101
|
else out._.push(a);
|
|
101
102
|
}
|
|
102
103
|
return out;
|
|
@@ -240,6 +241,70 @@ function collect(root, skipDotEnv) {
|
|
|
240
241
|
return { files, total };
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
// ---------- 컨테이너(서버 앱) ----------
|
|
245
|
+
// 빌드 서버 chiwaa-deployd 의 판단과 같다: 서버 기능 Next.js · 정적 빌드 도구가 아닌 Node 서버(start) · 정적 결과 없는 Dockerfile 앱
|
|
246
|
+
function containerKind(dir) {
|
|
247
|
+
const pkg = readJSON(path.join(dir, "package.json"));
|
|
248
|
+
const deps = pkg ? { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) } : {};
|
|
249
|
+
const scripts = (pkg && pkg.scripts) || {};
|
|
250
|
+
if (deps.next && nextServerFeatures(dir).length) return "next";
|
|
251
|
+
if (fs.existsSync(path.join(dir, "index.html"))) return null;
|
|
252
|
+
const staticTool = OUTPUTS.some(([n]) => deps[n]);
|
|
253
|
+
if (!staticTool && fs.existsSync(path.join(dir, "Dockerfile"))) return "docker";
|
|
254
|
+
if (!staticTool && String(scripts.start || "").trim()) return "node";
|
|
255
|
+
if (["requirements.txt", "pyproject.toml", "Pipfile", "manage.py"].some((f) => fs.existsSync(path.join(dir, f))))
|
|
256
|
+
fail(`파이썬 앱은 폴더 맨 위에 Dockerfile 을 넣어 주세요. 앱은 환경변수 PORT(3000)에서 들어야 합니다.\n` +
|
|
257
|
+
`AI 에게 「이 파이썬 앱을 chiwaa 컨테이너로 올릴 수 있게 Dockerfile 을 만들어 줘(PORT 환경변수 사용). ${AI_GUIDE}」라고 말해 보세요.`);
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// .gitignore 의 단순한 줄(이름·폴더/·*.확장자)을 따른다. 소스를 올릴 때 빼는 것.
|
|
262
|
+
const SOURCE_SKIP = new Set([...SKIP, ".next", "out", ".turbo", ".cache", ".DS_Store"]);
|
|
263
|
+
function ignoreRules(dir) {
|
|
264
|
+
const p = path.join(dir, ".gitignore");
|
|
265
|
+
if (!fs.existsSync(p)) return () => false;
|
|
266
|
+
const rules = fs.readFileSync(p, "utf8").split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && !l.startsWith("!"));
|
|
267
|
+
const names = new Set(rules.filter((r) => !r.includes("*")).map((r) => r.replace(/^\/+|\/+$/g, "")).filter((r) => !r.includes("/")));
|
|
268
|
+
const exts = rules.filter((r) => /^\*\.[\w.]+$/.test(r)).map((r) => r.slice(1));
|
|
269
|
+
return (name) => names.has(name) || exts.some((x) => name.endsWith(x));
|
|
270
|
+
}
|
|
271
|
+
function collectSource(root) {
|
|
272
|
+
const ignored = ignoreRules(root);
|
|
273
|
+
const files = [];
|
|
274
|
+
let total = 0;
|
|
275
|
+
const walk = (d, rel) => {
|
|
276
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
277
|
+
if (SOURCE_SKIP.has(e.name) || ignored(e.name) || /^\.env(\..*)?$/.test(e.name) || e.isSymbolicLink()) continue;
|
|
278
|
+
const abs = path.join(d, e.name);
|
|
279
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
280
|
+
if (e.isDirectory()) { files.push({ rel: r, dir: true }); walk(abs, r); }
|
|
281
|
+
else if (e.isFile()) { const st = fs.statSync(abs); total += st.size; files.push({ rel: r, abs, size: st.size, mtime: Math.floor(st.mtimeMs / 1000) }); }
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
walk(root, "");
|
|
285
|
+
return { files, total };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function deployContainer(dir, site, kind, args) {
|
|
289
|
+
const label = { next: "Next.js 서버", node: "Node 서버(npm start)", docker: "Dockerfile" }[kind];
|
|
290
|
+
const { files, total } = collectSource(dir);
|
|
291
|
+
const fileCount = files.filter((f) => !f.dir).length;
|
|
292
|
+
console.log(`${c.bold("컨테이너")}로 올립니다 — ${label}. 소스 ${fileCount}개(${(total / 1048576).toFixed(1)}MB)를 보내면 chiwaa 빌드 서버가 빌드합니다(보통 1~3분).`);
|
|
293
|
+
console.log(c.dim("node_modules·.git·.next·.env*·.gitignore 에 적힌 것은 보내지 않습니다. 앱은 환경변수 PORT(3000)에서 들어야 합니다."));
|
|
294
|
+
const gz = makeTarGz(files);
|
|
295
|
+
const t0 = Date.now();
|
|
296
|
+
const preview = !!args.preview;
|
|
297
|
+
const r = await api("POST", `/api/v1/sites/${encodeURIComponent(site.name)}/deploy?source=1${preview ? "&preview=1" : ""}`, {
|
|
298
|
+
body: gz,
|
|
299
|
+
headers: { "Content-Type": "application/gzip" },
|
|
300
|
+
});
|
|
301
|
+
if (r.status !== 200 || !r.data.ok) fail((r.data.message || r.data.error || `배포하지 못했습니다 (${r.status}).`) + `\n기록: npx chiwaa logs${preview ? " --preview" : ""}`);
|
|
302
|
+
console.log(c.green("✓"), `${preview ? "미리보기 " : ""}배포 완료 ${c.dim(`(${((Date.now() - t0) / 1000).toFixed(0)}초)`)}`);
|
|
303
|
+
console.log(c.dim(r.data.message || ""));
|
|
304
|
+
console.log(`\n ${c.bold(c.cyan(r.data.url))}\n`);
|
|
305
|
+
console.log(c.dim("앱 기록: npx chiwaa logs -f"));
|
|
306
|
+
}
|
|
307
|
+
|
|
243
308
|
function makeTarGz(files) {
|
|
244
309
|
const parts = [];
|
|
245
310
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -327,9 +392,10 @@ function checkNext(dir) {
|
|
|
327
392
|
const server = nextServerFeatures(dir);
|
|
328
393
|
if (server.length) {
|
|
329
394
|
const shown = server.slice(0, 4).join(", ") + (server.length > 4 ? ` 등 ${server.length}곳` : "");
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
`·
|
|
395
|
+
// --dir·--no-build 로 결과물만 올리려 할 때만 여기 온다(그냥 deploy 하면 컨테이너로 올라감)
|
|
396
|
+
fail(`이 Next.js 앱은 서버에서 돌아가는 기능을 씁니다(${shown}). --dir·--no-build 로는 결과물(정적 파일)만 올라가 서버 기능이 빠집니다.\n` +
|
|
397
|
+
`· 그대로 돌리려면: 옵션 없이 \`npx chiwaa deploy\` — 컨테이너로 올라갑니다.\n` +
|
|
398
|
+
`· 정적 사이트로 바꾸려면: AI 에게 「이 Next.js 앱을 output: 'export' 정적 사이트로 바꾸고, 서버 기능은 맨 위 api/ 폴더의 chiwaa 서버 함수로 옮겨 줘. ${AI_GUIDE}」라고 말해 보세요.`);
|
|
333
399
|
}
|
|
334
400
|
}
|
|
335
401
|
|
|
@@ -463,7 +529,7 @@ function agentSection(name) {
|
|
|
463
529
|
|
|
464
530
|
- 배포: \`npx chiwaa deploy -y\` — 끝나면 주소가 나온다. 잘못 올렸으면 \`npx chiwaa rollback\`.
|
|
465
531
|
- 이 폴더는 \`.chiwaa.json\` 으로 사이트가 정해져 있다. 다른 이름(--name)으로 올리지 않는다.
|
|
466
|
-
-
|
|
532
|
+
- 정적 사이트(HTML·CSS·JS, Vite·React·Vue·Astro·Next export)와 서버 함수(api/)는 결과물을 올리고, 서버 기능 Next.js·Express(start)·Dockerfile 앱은 소스를 보내 컨테이너로 돈다(요금제별 개수). 앱은 PORT(3000)에서 듣는다. 기록은 \`npx chiwaa logs -f\`. PHP 는 안 된다.
|
|
467
533
|
- 글 저장(문의·방명록·게시판)은 사이트 DB: \`npx chiwaa db on\` → \`npx chiwaa db create <표> --read public|owner --write public|owner\`.
|
|
468
534
|
- 문의·신청서처럼 개인정보가 든 표는 반드시 \`--read owner\`. 사이트에서 안 읽힌다고 public 으로 바꾸지 않는다.
|
|
469
535
|
- 방문자 글은 화면에 \`textContent\` 로만 넣는다. 비밀번호는 \`<input type="password">\` 로 받는다.
|
|
@@ -513,6 +579,8 @@ async function deploy(args) {
|
|
|
513
579
|
await login(args);
|
|
514
580
|
}
|
|
515
581
|
const site = await resolveSite(dir, args);
|
|
582
|
+
const ckind = args["no-build"] || args.dir ? null : containerKind(dir);
|
|
583
|
+
if (ckind) return deployContainer(dir, site, ckind, args);
|
|
516
584
|
const { out, built } = buildIfNeeded(dir, args);
|
|
517
585
|
const { files, total } = collect(out, !built);
|
|
518
586
|
// 서버 함수: 프로젝트 맨 위 api/ 는 빌드 결과에 안 들어가므로 따로 넣는다(서버가 사이트 파일에서 떼어 서버 함수로 돌린다)
|
|
@@ -839,10 +907,12 @@ async function stats(args) {
|
|
|
839
907
|
// ---------- logs (서버 함수 기록) ----------
|
|
840
908
|
async function logs(args) {
|
|
841
909
|
const name = await siteName(args);
|
|
842
|
-
const n = Math.min(Math.max(parseInt(args.lines, 10) || 30, 1),
|
|
843
|
-
const
|
|
910
|
+
const n = Math.min(Math.max(parseInt(args.lines, 10) || 30, 1), 2000);
|
|
911
|
+
const q = `${args.preview ? "&preview=1" : ""}${args.branch ? `&branch=${encodeURIComponent(args.branch)}` : ""}`;
|
|
912
|
+
const r = await api("GET", `/api/v1/sites/${encodeURIComponent(name)}/logs?lines=${n}${q}`);
|
|
844
913
|
if (r.status !== 200) fail(r.data.error || "기록을 받지 못했습니다.");
|
|
845
914
|
const d = r.data;
|
|
915
|
+
if (d.kind === "container") return containerLogs(name, n, q, d, args.f || args.follow);
|
|
846
916
|
console.log(c.bold(`${d.site} 서버 함수 기록`), c.dim(`(최근 ${n}건 · 1분쯤 늦을 수 있음)`));
|
|
847
917
|
if (d.functions.length) console.log(c.dim(` 함수: ${d.functions.map((f) => "/api/" + f).join(" ")}`));
|
|
848
918
|
if (!d.entries.length) return console.log(c.dim(" 아직 기록이 없습니다. 오류가 나거나 console.log 를 쓰면 여기에 남습니다."));
|
|
@@ -856,6 +926,29 @@ async function logs(args) {
|
|
|
856
926
|
}
|
|
857
927
|
}
|
|
858
928
|
|
|
929
|
+
// 컨테이너 앱 기록: 상태 한 줄 + 기록. -f 면 3초마다 새 줄만 이어서 보여 준다(Ctrl+C 로 끝).
|
|
930
|
+
async function containerLogs(name, n, q, d, follow) {
|
|
931
|
+
const st = d.status || {};
|
|
932
|
+
const state = st.error ? "상태 모름" : st.running ? `실행 중 · 메모리 ${st.memory_used || "?"} / ${st.memory_mb}MB` : "잠듦(다음 요청에서 깨어남)";
|
|
933
|
+
console.log(c.bold(`${d.site} 앱 기록`), c.dim(`(${state})`));
|
|
934
|
+
const print = (text, seen) => {
|
|
935
|
+
const lines = String(text || "").split("\n").filter((l) => l.trim());
|
|
936
|
+
let start = 0;
|
|
937
|
+
if (seen) { const i = lines.lastIndexOf(seen); start = i >= 0 ? i + 1 : 0; }
|
|
938
|
+
for (const l of lines.slice(start)) console.log(l.replace(/^(\S+)Z /, (m, t) => c.dim(t.slice(5, 19).replace("T", " ") + " ")));
|
|
939
|
+
return lines.length ? lines[lines.length - 1] : seen;
|
|
940
|
+
};
|
|
941
|
+
let last = print(d.text, null);
|
|
942
|
+
if (!d.text || !String(d.text).trim()) console.log(c.dim(" 아직 기록이 없습니다. 앱이 console.log·print 로 쓰거나 오류가 나면 여기에 남습니다."));
|
|
943
|
+
if (!follow) return;
|
|
944
|
+
console.log(c.dim("— 이어서 봅니다(Ctrl+C 로 끝)"));
|
|
945
|
+
for (;;) {
|
|
946
|
+
await new Promise((res) => setTimeout(res, 3000));
|
|
947
|
+
const r = await api("GET", `/api/v1/sites/${encodeURIComponent(name)}/logs?lines=200${q}`);
|
|
948
|
+
if (r.status === 200 && r.data.kind === "container") last = print(r.data.text, last);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
859
952
|
// ---------- env (서버 함수 비밀값) ----------
|
|
860
953
|
async function env(args) {
|
|
861
954
|
const sub = args._[1] || "ls";
|
|
@@ -1042,7 +1135,7 @@ const HELP = `chiwaa ${VERSION} — chiwaa.kr 에 사이트를 바로 올립니
|
|
|
1042
1135
|
chiwaa domains check 연결됐는지 확인 · domains remove -y 로 연결 끊기
|
|
1043
1136
|
chiwaa stats 방문자 통계(오늘·최근 7일, 많이 본 페이지, 들어온 곳) --days 30
|
|
1044
1137
|
chiwaa files 사진 목록·용량 · files put 사진.jpg(주인 사진 올리기) · files on|off(방문자 사진 올리기)
|
|
1045
|
-
chiwaa logs
|
|
1138
|
+
chiwaa logs 컨테이너 앱 기록(-f 이어서 보기, --branch <브랜치>) 또는 서버 함수 기록 --lines 100, --preview
|
|
1046
1139
|
chiwaa env 서버 함수 비밀값 목록 · env set 이름(값은 안 보이게 입력) · env rm 이름
|
|
1047
1140
|
chiwaa lock / unlock 사이트 비밀번호 잠금(아이디 chiwaa) / 풀기 — 공개 전 나만 보기
|
|
1048
1141
|
chiwaa open 관리 화면 열기 (--site 는 사이트 열기)
|