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.
- package/README.md +1 -0
- package/bin/chiwaa.js +138 -8
- 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("로그인이 필요합니다. 먼저
|
|
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 =
|
|
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
|
-
|
|
276
|
-
|
|
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("로그인이 필요합니다.
|
|
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
|
|
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
|
}
|