chiwaa 0.1.0 → 0.1.2

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 +1 -0
  2. package/bin/chiwaa.js +138 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,6 +18,7 @@ npx chiwaa deploy
18
18
  | `chiwaa list` | 내 사이트 목록 |
19
19
  | `chiwaa versions` | 남아 있는 버전 |
20
20
  | `chiwaa rollback [버전]` | 한 단계 전 또는 지정 버전으로 되돌리기 |
21
+ | `chiwaa db` · `db on` · `db create <표>` · `db rows <표>` | 사이트 DB(문의·방명록 등 글 저장) — 켜기·표 만들기·글 보기 |
21
22
  | `chiwaa whoami` / `logout` | 계정 확인 / 로그아웃 |
22
23
 
23
24
  ## AI 도구(Claude Code·Codex·Cursor)에서
package/bin/chiwaa.js CHANGED
@@ -63,7 +63,7 @@ async function api(method, p, { body, headers = {}, auth = true } = {}) {
63
63
  const h = { "User-Agent": `chiwaa-cli/${VERSION}`, ...headers };
64
64
  if (auth) {
65
65
  const t = token();
66
- if (!t) fail("로그인이 필요합니다. 먼저 `chiwaa login` 을 실행하세요.");
66
+ if (!t) fail("로그인이 필요합니다. 먼저 아래 명령으로 로그인하세요(브라우저에서 승인).\n\n npx chiwaa login\n");
67
67
  h.Authorization = `Bearer ${t}`;
68
68
  }
69
69
  let payload = body;
@@ -92,7 +92,7 @@ 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"].includes(k) && argv[i + 1] && !argv[i + 1].startsWith("-")) out[k] = argv[++i];
95
+ else if (["name", "token", "dir", "read", "write", "page"].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;
@@ -254,6 +254,16 @@ function buildIfNeeded(dir, args) {
254
254
  }
255
255
 
256
256
  // ---------- 사이트 정하기 ----------
257
+ // 사용자가 직접 정한 이름은 바꾸지 않는다 — 규칙에 안 맞으면 거절한다(대문자만 소문자로)
258
+ const NAME_RULE = "사이트 이름은 영문 소문자·숫자·하이픈(-)으로 2~30자, 첫 글자는 영문이나 숫자여야 합니다. 예: my-shop, cafe2026";
259
+ function checkName(s) {
260
+ const n = String(s).trim().toLowerCase();
261
+ if (!/^[a-z0-9][a-z0-9-]{1,29}$/.test(n)) fail(`쓸 수 없는 사이트 이름입니다: ${s}
262
+ ${NAME_RULE}`);
263
+ return n;
264
+ }
265
+
266
+ // 폴더 이름으로 기본 이름을 지어 줄 때만 쓴다
257
267
  function slug(s) {
258
268
  let n = String(s).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 30);
259
269
  if (n.length < 2) n = `site-${n}`.replace(/-$/, "");
@@ -267,13 +277,17 @@ async function resolveSite(dir, args) {
267
277
  if (r.status === 200) return linkSite(dir, r.data);
268
278
  console.log(c.dim(`${LINK_FILE} 의 사이트(${link.site})를 찾지 못해 새로 정합니다.`));
269
279
  }
270
- let name = slug(args.name || path.basename(path.resolve(dir)));
280
+ let name = args.name ? checkName(args.name) : slug(path.basename(path.resolve(dir)));
271
281
  if (args.name) {
272
282
  const r = await api("GET", `/api/v1/sites/${encodeURIComponent(name)}`);
273
283
  if (r.status === 200) return linkSite(dir, r.data);
274
284
  } else if (TTY && !args.yes) {
275
- const a = await ask(`사이트 이름 ${c.dim(`(${name}.chiwaa.com)`)}: `);
276
- if (a) name = slug(a);
285
+ for (;;) {
286
+ const a = await ask(`사이트 이름 ${c.dim(`(엔터 = ${name}.chiwaa.com)`)}: `);
287
+ if (!a) break;
288
+ if (/^[a-z0-9][a-z0-9-]{1,29}$/.test(a.toLowerCase())) { name = a.toLowerCase(); break; }
289
+ console.log(c.red("✗"), NAME_RULE);
290
+ }
277
291
  const r = await api("GET", `/api/v1/sites/${encodeURIComponent(name)}`);
278
292
  if (r.status === 200) return linkSite(dir, r.data);
279
293
  }
@@ -301,7 +315,7 @@ async function deploy(args) {
301
315
  const dir = path.resolve(args._[1] || ".");
302
316
  if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) fail(`폴더가 없습니다: ${dir}`);
303
317
  if (!token()) {
304
- if (!TTY) fail("로그인이 필요합니다. `chiwaa login` 을 하거나 CHIWAA_TOKEN 을 넣어 주세요.");
318
+ if (!TTY) fail("로그인이 필요합니다. 먼저 아래 명령으로 로그인하고(브라우저에서 승인), 같은 배포 명령을 다시 실행하세요.\n\n npx chiwaa login\n\n (자동화·AI 도구에서는 환경변수 CHIWAA_TOKEN 으로도 됩니다)");
305
319
  await login(args);
306
320
  }
307
321
  const site = await resolveSite(dir, args);
@@ -331,7 +345,7 @@ async function list() {
331
345
  }
332
346
 
333
347
  async function siteName(args) {
334
- if (args.name) return slug(args.name);
348
+ if (args.name) return checkName(args.name);
335
349
  const link = readJSON(path.join(process.cwd(), LINK_FILE));
336
350
  if (link && link.site) return link.site;
337
351
  fail(`어느 사이트인지 모릅니다. 사이트 폴더에서 실행하거나 --name <이름> 을 붙여 주세요.`);
@@ -355,6 +369,115 @@ async function rollback(args) {
355
369
  console.log(c.green("✓"), r.data.message);
356
370
  }
357
371
 
372
+ // ---------- db (사이트 DB) ----------
373
+ // 데이터가 지워지는 일(끄기·표 지우기·글 지우기)은 일부러 없다 — chiwaa.kr 사이트 화면에서만 한다.
374
+ const RULE_KO = { public: "누구나", owner: "나만(chiwaa.kr 화면)" };
375
+
376
+ function snippet(table) {
377
+ return `<!-- 글 보내기(저장) -->
378
+ <script src="/_chiwaa/data/v1/chiwaa.js"></script>
379
+ <form data-chiwaa-table="${table}" data-chiwaa-done="보냈습니다. 감사합니다.">
380
+ <input name="이름" placeholder="이름" required>
381
+ <textarea name="내용" placeholder="내용" required></textarea>
382
+ <input name="_pw" type="password" placeholder="비밀번호(내 글 수정·삭제용, 4자 이상)">
383
+ <input name="_hp" style="display:none" tabindex="-1" autocomplete="off">
384
+ <button type="submit">보내기</button>
385
+ </form>
386
+
387
+ <!-- 글 목록 보기(읽기 권한이 public 일 때) -->
388
+ <ul id="${table}-list"></ul>
389
+ <script>
390
+ chiwaa.table("${table}").list({ limit: 20 }).then(function (items) {
391
+ var ul = document.getElementById("${table}-list");
392
+ items.forEach(function (item) {
393
+ var li = document.createElement("li");
394
+ li.textContent = (item["이름"] || "") + ": " + (item["내용"] || ""); // 방문자 글은 textContent 로만
395
+ ul.appendChild(li);
396
+ });
397
+ });
398
+ </script>
399
+
400
+ <!-- 스크립트로 직접:
401
+ chiwaa.table("${table}").add({ 제목, 내용, _pw }) 글쓰기(_pw 를 넣으면 그 비밀번호로 수정·삭제 가능)
402
+ chiwaa.table("${table}").list({ limit: 20, before: 마지막id }) 목록(최신순, 다음 쪽은 before)
403
+ chiwaa.table("${table}").get(id) 글 하나(상세 화면)
404
+ chiwaa.table("${table}").update(id, { 제목, 내용 }, 비밀번호) 글쓴이가 고치기
405
+ chiwaa.table("${table}").remove(id, 비밀번호) 글쓴이가 지우기
406
+ 댓글은 표를 하나 더 만들고(예: comments) 글 번호를 함께 저장 → .list({ where: { post_id: id } }) -->`;
407
+ }
408
+
409
+ function ruleArg(v, label) {
410
+ if (v === undefined || v === true) return undefined;
411
+ if (!["public", "owner"].includes(v)) fail(`${label} 는 public(누구나) 또는 owner(나만) 입니다: ${v}`);
412
+ return v;
413
+ }
414
+
415
+ async function db(args) {
416
+ const sub = args._[1] || "status";
417
+ const name = await siteName(args);
418
+ const base = `/api/v1/sites/${encodeURIComponent(name)}/db`;
419
+ const table = args._[2];
420
+ const needTable = () => {
421
+ if (!table) fail(`표 이름을 적어 주세요. 예: npx chiwaa db ${sub} guestbook`);
422
+ if (!/^[a-z][a-z0-9_]{0,39}$/.test(table)) fail(`표 이름은 영문 소문자로 시작하고 영문 소문자·숫자·밑줄(_) 40자까지입니다: ${table}`);
423
+ return table;
424
+ };
425
+
426
+ if (sub === "status") {
427
+ const r = await api("GET", base);
428
+ if (r.status !== 200) fail(r.data.error || "DB 정보를 받지 못했습니다.");
429
+ if (!r.data.enabled) return console.log(`${name}.chiwaa.com 의 DB는 꺼져 있습니다. 켜려면: npx chiwaa db on`);
430
+ console.log(c.bold(`${name}.chiwaa.com DB`), c.dim(`· 쓴 양 ${(r.data.bytes / 1048576).toFixed(1)}MB / ${r.data.limit_mb}MB · 오늘 저장 ${r.data.writes_today}/${r.data.limit_writes}`));
431
+ if (!r.data.tables.length) return console.log("아직 표가 없습니다. 예: npx chiwaa db create guestbook --read public --write public");
432
+ for (const t of r.data.tables)
433
+ console.log(` ${t.name} ${c.dim(`읽기 ${RULE_KO[t.read_rule]} · 쓰기 ${RULE_KO[t.write_rule]} · 글 ${t.records}개`)}`);
434
+ return;
435
+ }
436
+ if (sub === "on") {
437
+ const r = await api("POST", `${base}/on`);
438
+ if (r.status !== 200) fail(r.data.error || "DB를 켜지 못했습니다.");
439
+ return console.log(c.green("✓"), r.data.message);
440
+ }
441
+ if (sub === "create") {
442
+ needTable();
443
+ const read = ruleArg(args.read, "--read") || "owner";
444
+ const write = ruleArg(args.write, "--write") || "public";
445
+ const r = await api("POST", `${base}/tables`, { body: { table, read, write } });
446
+ if (r.status !== 201) fail(r.data.error || "표를 만들지 못했습니다.");
447
+ console.log(c.green("✓"), `「${table}」 표를 만들었습니다. 읽기 ${RULE_KO[read]} · 쓰기 ${RULE_KO[write]}`);
448
+ console.log(c.dim("\n사이트 HTML 에 아래를 넣고 다시 deploy 하면 됩니다. 저장된 글은 npx chiwaa db rows " + table + "\n"));
449
+ return console.log(snippet(table));
450
+ }
451
+ if (sub === "rules") {
452
+ needTable();
453
+ const read = ruleArg(args.read, "--read");
454
+ const write = ruleArg(args.write, "--write");
455
+ if (!read && !write) fail("바꿀 권한을 --read public|owner 나 --write public|owner 로 알려 주세요.");
456
+ const r = await api("POST", `${base}/tables/${table}/rules`, { body: { read, write } });
457
+ if (r.status !== 200) fail(r.data.error || "권한을 바꾸지 못했습니다.");
458
+ return console.log(c.green("✓"), `「${table}」 읽기 ${RULE_KO[r.data.read_rule]} · 쓰기 ${RULE_KO[r.data.write_rule]}`);
459
+ }
460
+ if (sub === "rows") {
461
+ needTable();
462
+ const page = Math.max(parseInt(args.page, 10) || 1, 1);
463
+ const r = await api("GET", `${base}/tables/${table}/rows?page=${page}`);
464
+ if (r.status !== 200) fail(r.data.error || "글을 받지 못했습니다.");
465
+ const items = r.data.items || [];
466
+ if (!items.length) return console.log(page > 1 ? "이 쪽에는 글이 없습니다." : "아직 저장된 글이 없습니다.");
467
+ for (const it of items) {
468
+ const { id, created_at, _editable, ...rest } = it;
469
+ const d = new Date(created_at);
470
+ const p2 = (n) => String(n).padStart(2, "0");
471
+ const when = isNaN(d) ? String(created_at) : `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}`;
472
+ console.log(`${c.dim(`#${id} ${when}`)} ${JSON.stringify(rest)}`);
473
+ }
474
+ if (items.length >= 20) console.log(c.dim(`다음 쪽: npx chiwaa db rows ${table} --page ${page + 1}`));
475
+ return;
476
+ }
477
+ if (sub === "code") return console.log(snippet(needTable()));
478
+ fail(`모르는 db 명령입니다: ${sub}\n 쓸 수 있는 것: status · on · create · rules · rows · code`);
479
+ }
480
+
358
481
  async function whoami() {
359
482
  const r = await api("GET", "/api/v1/me");
360
483
  if (r.status !== 200) fail(r.data.error || "로그인 정보를 받지 못했습니다.");
@@ -379,6 +502,13 @@ const HELP = `chiwaa ${VERSION} — chiwaa.kr 에 사이트를 바로 올립니
379
502
  chiwaa list 내 사이트 목록
380
503
  chiwaa versions 남아 있는 버전 보기
381
504
  chiwaa rollback [버전] 한 단계 전(또는 지정 버전)으로 되돌리기
505
+ chiwaa db 사이트 DB 상태·표 목록
506
+ chiwaa db on DB 켜기
507
+ chiwaa db create <표> 표 만들기 (--read public|owner, --write public|owner)
508
+ 예: chiwaa db create guestbook --read public --write public
509
+ chiwaa db rules <표> 읽기·쓰기 권한 바꾸기
510
+ chiwaa db rows <표> 저장된 글 보기 (--page 2)
511
+ chiwaa db code <표> 사이트에 붙일 코드 다시 보기
382
512
  chiwaa whoami 로그인한 계정
383
513
  chiwaa logout 로그아웃
384
514
 
@@ -390,7 +520,7 @@ async function main() {
390
520
  const cmd = args._[0];
391
521
  if (args.version) return console.log(VERSION);
392
522
  if (args.help || !cmd || cmd === "help") return console.log(HELP);
393
- const cmds = { deploy, login, list, ls: list, versions, rollback, whoami, logout };
523
+ const cmds = { deploy, login, list, ls: list, versions, rollback, db, whoami, logout };
394
524
  if (!cmds[cmd]) fail(`모르는 명령입니다: ${cmd}\n\n${HELP}`);
395
525
  await cmds[cmd](args);
396
526
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chiwaa",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "chiwaa.kr 에 사이트를 터미널에서 바로 올리는 도구",
5
5
  "bin": {
6
6
  "chiwaa": "bin/chiwaa.js"