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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/bin/chiwaa.js +101 -8
  3. 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
- fail(`이 Next.js 앱은 서버에서 돌아가는 기능을 씁니다(${shown}). CLI 로는 아직 서버 코드를 올릴 수 없습니다.\n` +
331
- `· 그대로 돌리려면: 깃허브 저장소를 chiwaa.kr 에 연결하세요. 서버 기능이 있는 Next.js 를 컨테이너로 실행합니다(https://chiwaa.kr/help).\n` +
332
- `· CLI 로 올리려면: AI 에게 「이 Next.js 앱을 output: 'export' 정적 사이트로 바꾸고, 서버 기능은 맨 위 api/ 폴더의 chiwaa 서버 함수로 옮겨 줘. ${AI_GUIDE}」라고 말해 보세요.`);
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
- - CLI 로는 정적 사이트만 된다(HTML·CSS·JS, Vite·React·Vue·Astro·Next export 빌드) + 서버 함수(api/). Next.js SSR·Express·Dockerfile 같은 서버 코드는 깃허브 저장소를 연결하면 컨테이너로 돈다. PHP 는 안 된다.
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), 500);
843
- const r = await api("GET", `/api/v1/sites/${encodeURIComponent(name)}/logs?lines=${n}${args.preview ? "&preview=1" : ""}`);
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 서버 함수 기록(오류·console.log) --lines 100, --preview
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 는 사이트 열기)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chiwaa",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "chiwaa.kr 에 사이트를 터미널에서 바로 올리는 도구",
5
5
  "bin": {
6
6
  "chiwaa": "bin/chiwaa.js"