chiwaa 0.1.17 → 0.1.19

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 +100 -5
  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
@@ -241,6 +241,70 @@ function collect(root, skipDotEnv) {
241
241
  return { files, total };
242
242
  }
243
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
+
244
308
  function makeTarGz(files) {
245
309
  const parts = [];
246
310
  const now = Math.floor(Date.now() / 1000);
@@ -328,9 +392,10 @@ function checkNext(dir) {
328
392
  const server = nextServerFeatures(dir);
329
393
  if (server.length) {
330
394
  const shown = server.slice(0, 4).join(", ") + (server.length > 4 ? ` 등 ${server.length}곳` : "");
331
- fail(`이 Next.js 앱은 서버에서 돌아가는 기능을 씁니다(${shown}). CLI 로는 아직 서버 코드를 올릴 수 없습니다.\n` +
332
- `· 그대로 돌리려면: 깃허브 저장소를 chiwaa.kr 에 연결하세요. 서버 기능이 있는 Next.js 를 컨테이너로 실행합니다(https://chiwaa.kr/help).\n` +
333
- `· 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}」라고 말해 보세요.`);
334
399
  }
335
400
  }
336
401
 
@@ -464,7 +529,7 @@ function agentSection(name) {
464
529
 
465
530
  - 배포: \`npx chiwaa deploy -y\` — 끝나면 주소가 나온다. 잘못 올렸으면 \`npx chiwaa rollback\`.
466
531
  - 이 폴더는 \`.chiwaa.json\` 으로 사이트가 정해져 있다. 다른 이름(--name)으로 올리지 않는다.
467
- - 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 는 안 된다.
468
533
  - 글 저장(문의·방명록·게시판)은 사이트 DB: \`npx chiwaa db on\` → \`npx chiwaa db create <표> --read public|owner --write public|owner\`.
469
534
  - 문의·신청서처럼 개인정보가 든 표는 반드시 \`--read owner\`. 사이트에서 안 읽힌다고 public 으로 바꾸지 않는다.
470
535
  - 방문자 글은 화면에 \`textContent\` 로만 넣는다. 비밀번호는 \`<input type="password">\` 로 받는다.
@@ -514,6 +579,8 @@ async function deploy(args) {
514
579
  await login(args);
515
580
  }
516
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);
517
584
  const { out, built } = buildIfNeeded(dir, args);
518
585
  const { files, total } = collect(out, !built);
519
586
  // 서버 함수: 프로젝트 맨 위 api/ 는 빌드 결과에 안 들어가므로 따로 넣는다(서버가 사이트 파일에서 떼어 서버 함수로 돌린다)
@@ -1006,6 +1073,33 @@ async function open(args) {
1006
1073
  if (!args["no-browser"]) openBrowser(url);
1007
1074
  }
1008
1075
 
1076
+ // ---------- pg (관리형 Postgres) ----------
1077
+ async function pg(args) {
1078
+ const sub = args._[1] || "status";
1079
+ const name = await siteName(args);
1080
+ const base = `/api/v1/sites/${encodeURIComponent(name)}/pg`;
1081
+ if (sub === "create") {
1082
+ const r = await api("POST", base);
1083
+ if (r.status !== 200) fail(r.data.error || "Postgres 를 만들지 못했습니다.");
1084
+ console.log(c.green("✓"), r.data.message);
1085
+ return console.log(c.dim("앱에서는 process.env.DATABASE_URL(또는 CHIWAA_PG_URL). 컨테이너 앱에서만 접속됩니다. 다시 배포: npx chiwaa deploy"));
1086
+ }
1087
+ if (sub === "password") {
1088
+ const r = await api("POST", `${base}/password`);
1089
+ if (r.status !== 200) fail(r.data.error || "비밀번호를 바꾸지 못했습니다.");
1090
+ return console.log(c.green("✓"), r.data.message);
1091
+ }
1092
+ if (sub !== "status") fail("쓰는 법: chiwaa pg [status|create|password]");
1093
+ const r = await api("GET", base);
1094
+ if (r.status !== 200) fail(r.data.error || "상태를 받지 못했습니다.");
1095
+ const d = r.data;
1096
+ const lim = d.limit_mb >= 1024 ? `${d.limit_mb / 1024}GB` : `${d.limit_mb}MB`;
1097
+ if (!d.exists) return console.log(`Postgres 없음 — ${c.bold("npx chiwaa pg create")} 로 만듭니다(${lim} · 동시 연결 ${d.conn_limit}개 · 매일 백업).`);
1098
+ if (d.error) fail(d.error);
1099
+ console.log(`${c.bold(d.name)} · 사용 ${(d.bytes / 1048576).toFixed(1)}MB / ${lim} · 연결 ${d.connections}/${d.conn_limit}${d.readonly ? c.red(" · 용량을 넘어 읽기 전용") : ""}`);
1100
+ console.log(c.dim("컨테이너에 CHIWAA_PG_URL 로 들어갑니다(설정값에 DATABASE_URL 이 없으면 DATABASE_URL 로도)."));
1101
+ }
1102
+
1009
1103
  async function usage() {
1010
1104
  const r = await api("GET", "/api/v1/usage");
1011
1105
  if (r.status !== 200) fail(r.data.error || "사용량을 받지 못했습니다.");
@@ -1068,6 +1162,7 @@ const HELP = `chiwaa ${VERSION} — chiwaa.kr 에 사이트를 바로 올립니
1068
1162
  chiwaa domains check 연결됐는지 확인 · domains remove -y 로 연결 끊기
1069
1163
  chiwaa stats 방문자 통계(오늘·최근 7일, 많이 본 페이지, 들어온 곳) --days 30
1070
1164
  chiwaa files 사진 목록·용량 · files put 사진.jpg(주인 사진 올리기) · files on|off(방문자 사진 올리기)
1165
+ chiwaa pg [create|password] 관리형 Postgres(컨테이너 앱의 DATABASE_URL) 상태·만들기·비밀번호 새로
1071
1166
  chiwaa logs 컨테이너 앱 기록(-f 이어서 보기, --branch <브랜치>) 또는 서버 함수 기록 --lines 100, --preview
1072
1167
  chiwaa env 서버 함수 비밀값 목록 · env set 이름(값은 안 보이게 입력) · env rm 이름
1073
1168
  chiwaa lock / unlock 사이트 비밀번호 잠금(아이디 chiwaa) / 풀기 — 공개 전 나만 보기
@@ -1086,7 +1181,7 @@ async function main() {
1086
1181
  const cmd = args._[0];
1087
1182
  if (args.version) return console.log(VERSION);
1088
1183
  if (args.help || !cmd || cmd === "help") return console.log(HELP);
1089
- const cmds = { deploy, login, list, ls: list, versions, rollback, db, files, domains, domain: domains, stats, env, logs, lock, unlock, open, usage, agent, whoami, logout };
1184
+ const cmds = { deploy, login, list, ls: list, versions, rollback, db, files, domains, domain: domains, stats, env, logs, lock, unlock, open, usage, agent, whoami, logout, pg };
1090
1185
  if (!cmds[cmd]) fail(`모르는 명령입니다: ${cmd}\n\n${HELP}`);
1091
1186
  if (cmd !== "login" && cmd !== "logout") await resumePendingLogin();
1092
1187
  await cmds[cmd](args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chiwaa",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "chiwaa.kr 에 사이트를 터미널에서 바로 올리는 도구",
5
5
  "bin": {
6
6
  "chiwaa": "bin/chiwaa.js"