chiwaa 0.1.5 → 0.1.7
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 +4 -0
- package/bin/chiwaa.js +117 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,10 @@ npx chiwaa deploy
|
|
|
21
21
|
| `chiwaa db` · `db on` · `db create <표>` · `db rows <표>` | 사이트 DB(문의·방명록 등 글 저장) — 켜기·표 만들기·글 보기 |
|
|
22
22
|
| `chiwaa domains` · `search` · `buy` · `add` · `check` | x-y.net 도메인 찾기·사기(결제는 x-y.net 결제창에서 직접)·붙이기 |
|
|
23
23
|
| `chiwaa stats` | 방문자 통계(오늘·최근 7일·많이 본 페이지·들어온 곳) |
|
|
24
|
+
| `chiwaa deploy --preview` | 운영 주소는 두고 `이름--preview.chiwaa.com` 에 미리보기 |
|
|
25
|
+
| `chiwaa env` · `env set 이름` · `env rm 이름` | 서버 함수(`api/*.js`) 비밀값 — 값은 화면에 안 보이게 입력 |
|
|
26
|
+
| `chiwaa lock` / `unlock` | 사이트 비밀번호 잠금(공개 전 나만 보기) |
|
|
27
|
+
| `chiwaa open` · `usage` | 관리 화면 열기 · 이번 달 사용량 |
|
|
24
28
|
| `chiwaa agent` | AI 도구용 안내(AGENTS.md·CLAUDE.md) 만들기 — 처음 deploy 때 자동 |
|
|
25
29
|
| `chiwaa whoami` / `logout` | 계정 확인 / 로그아웃 |
|
|
26
30
|
|
package/bin/chiwaa.js
CHANGED
|
@@ -255,10 +255,10 @@ function buildIfNeeded(dir, args) {
|
|
|
255
255
|
|
|
256
256
|
// ---------- 사이트 정하기 ----------
|
|
257
257
|
// 사용자가 직접 정한 이름은 바꾸지 않는다 — 규칙에 안 맞으면 거절한다(대문자만 소문자로)
|
|
258
|
-
const NAME_RULE = "사이트 이름은 영문 소문자·숫자·하이픈(-)으로 2~30자, 첫 글자는 영문이나 숫자여야
|
|
258
|
+
const NAME_RULE = "사이트 이름은 영문 소문자·숫자·하이픈(-)으로 2~30자, 첫 글자는 영문이나 숫자여야 합니다(하이픈 두 개 연속은 안 됨). 예: my-shop, cafe2026";
|
|
259
259
|
function checkName(s) {
|
|
260
260
|
const n = String(s).trim().toLowerCase();
|
|
261
|
-
if (!/^[a-z0-9][a-z0-9-]{1,29}$/.test(n)) fail(`쓸 수 없는 사이트 이름입니다: ${s}
|
|
261
|
+
if (!/^[a-z0-9][a-z0-9-]{1,29}$/.test(n) || n.includes("--")) fail(`쓸 수 없는 사이트 이름입니다: ${s}
|
|
262
262
|
${NAME_RULE}`);
|
|
263
263
|
return n;
|
|
264
264
|
}
|
|
@@ -285,7 +285,7 @@ async function resolveSite(dir, args) {
|
|
|
285
285
|
for (;;) {
|
|
286
286
|
const a = await ask(`사이트 이름 ${c.dim(`(엔터 = ${name}.chiwaa.com)`)}: `);
|
|
287
287
|
if (!a) break;
|
|
288
|
-
if (/^[a-z0-9][a-z0-9-]{1,29}$/.test(a.toLowerCase())) { name = a.toLowerCase(); break; }
|
|
288
|
+
if (/^[a-z0-9][a-z0-9-]{1,29}$/.test(a.toLowerCase()) && !a.includes("--")) { name = a.toLowerCase(); break; }
|
|
289
289
|
console.log(c.red("✗"), NAME_RULE);
|
|
290
290
|
}
|
|
291
291
|
const r = await api("GET", `/api/v1/sites/${encodeURIComponent(name)}`);
|
|
@@ -373,19 +373,29 @@ async function deploy(args) {
|
|
|
373
373
|
const site = await resolveSite(dir, args);
|
|
374
374
|
const { out, built } = buildIfNeeded(dir, args);
|
|
375
375
|
const { files, total } = collect(out, !built);
|
|
376
|
+
// 서버 함수: 프로젝트 맨 위 api/ 는 빌드 결과에 안 들어가므로 따로 넣는다(서버가 사이트 파일에서 떼어 서버 함수로 돌린다)
|
|
377
|
+
const apiDir = path.join(dir, "api");
|
|
378
|
+
if (built && fs.existsSync(apiDir) && fs.statSync(apiDir).isDirectory() && !files.some((f) => f.rel === "api")) {
|
|
379
|
+
const extra = collect(apiDir, true);
|
|
380
|
+
files.push({ rel: "api", dir: true }, ...extra.files.map((f) => ({ ...f, rel: `api/${f.rel}` })));
|
|
381
|
+
}
|
|
382
|
+
const fnCount = files.filter((f) => !f.dir && /^api\/[^/_][^/]*\.(js|ts|mjs)$/.test(f.rel)).length;
|
|
383
|
+
if (fnCount) console.log(c.dim(`서버 함수 ${fnCount}개(api/) 함께 올림 → https://<사이트>/api/<이름>`));
|
|
376
384
|
if (!files.some((f) => !f.dir && /^index\.html?$/i.test(f.rel)))
|
|
377
385
|
console.log(c.dim("참고: 맨 위에 index.html 이 없습니다. 주소로 열면 첫 화면이 안 보일 수 있습니다."));
|
|
378
386
|
const fileCount = files.filter((f) => !f.dir).length;
|
|
379
387
|
console.log(c.dim(`올리는 중: ${path.relative(process.cwd(), out) || "."} — 파일 ${fileCount}개, ${(total / 1048576).toFixed(1)}MB`));
|
|
380
388
|
const gz = makeTarGz(files);
|
|
381
389
|
const t0 = Date.now();
|
|
382
|
-
const
|
|
390
|
+
const preview = !!args.preview;
|
|
391
|
+
const r = await api("POST", `/api/v1/sites/${encodeURIComponent(site.name)}/deploy${preview ? "?preview=1" : ""}`, {
|
|
383
392
|
body: gz,
|
|
384
393
|
headers: { "Content-Type": "application/gzip" },
|
|
385
394
|
});
|
|
386
395
|
if (r.status !== 200 || !r.data.ok) fail(r.data.message || r.data.error || `배포하지 못했습니다 (${r.status}).`);
|
|
387
|
-
console.log(c.green("✓"),
|
|
396
|
+
console.log(c.green("✓"), `${preview ? "미리보기 " : ""}배포 완료 ${c.dim(`(${((Date.now() - t0) / 1000).toFixed(1)}초)`)}`);
|
|
388
397
|
console.log(`\n ${c.bold(c.cyan(r.data.url))}\n`);
|
|
398
|
+
if (preview) console.log(c.dim("운영 주소는 그대로입니다. 괜찮으면 --preview 없이 다시 deploy 하세요. (미리보기는 검색엔진에 나오지 않음)\n"));
|
|
389
399
|
}
|
|
390
400
|
|
|
391
401
|
// ---------- 그 밖 ----------
|
|
@@ -660,6 +670,102 @@ async function stats(args) {
|
|
|
660
670
|
if (!s.total.views) console.log(c.dim("\n 아직 집계된 방문이 없습니다."));
|
|
661
671
|
}
|
|
662
672
|
|
|
673
|
+
// ---------- env (서버 함수 비밀값) ----------
|
|
674
|
+
async function env(args) {
|
|
675
|
+
const sub = args._[1] || "ls";
|
|
676
|
+
const name = await siteName(args);
|
|
677
|
+
const base = `/api/v1/sites/${encodeURIComponent(name)}/env`;
|
|
678
|
+
if (sub === "ls") {
|
|
679
|
+
const r = await api("GET", base);
|
|
680
|
+
if (r.status !== 200) fail(r.data.error || "비밀값 목록을 받지 못했습니다.");
|
|
681
|
+
const d = r.data;
|
|
682
|
+
console.log(c.bold(`${name}.chiwaa.com 서버 함수 비밀값`), c.dim(`(${d.names.length}/${d.limit}개 · 값은 다시 보여 주지 않음)`));
|
|
683
|
+
for (const n of d.names) console.log(` ${n}`);
|
|
684
|
+
if (!d.names.length) console.log(c.dim(" 없음. 넣기: npx chiwaa env set 이름 (값은 화면에 안 보이게 입력)"));
|
|
685
|
+
console.log(c.dim(`\n이번 달 서버 함수 실행 ${Number(d.calls_this_month).toLocaleString("ko-KR")}회 · 마지막 실행 ${d.last_used_at ? d.last_used_at.slice(0, 10) : "없음"}`));
|
|
686
|
+
if (d.idle_90d) console.log(c.red("⚠ 90일 넘게 서버 함수를 안 썼습니다. 안 쓰는 비밀값(결제 키 등)은 지우는 게 안전합니다: npx chiwaa env rm 이름"));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const key = args._[2];
|
|
690
|
+
if (!key) fail(`이름을 적어 주세요. 예: npx chiwaa env ${sub} TOSS_SECRET_KEY`);
|
|
691
|
+
if (sub === "set") {
|
|
692
|
+
if (!/^[A-Z][A-Z0-9_]{0,63}$/.test(key)) fail("이름은 영문 대문자로 시작하고 대문자·숫자·밑줄 64자까지입니다. 예: TOSS_SECRET_KEY");
|
|
693
|
+
const value = await readSecret(`${key} 값(화면에 안 보임): `);
|
|
694
|
+
if (!value) fail("값이 비어 있습니다. (AI·자동화에서는 표준입력으로: echo 값 | npx chiwaa env set 이름)");
|
|
695
|
+
const r = await api("POST", base, { body: { name: key, value } });
|
|
696
|
+
if (r.status !== 200) fail(r.data.error || "넣지 못했습니다.");
|
|
697
|
+
return console.log(c.green("✓"), r.data.message);
|
|
698
|
+
}
|
|
699
|
+
if (sub === "rm") {
|
|
700
|
+
const r = await api("POST", `${base}/remove`, { body: { name: key } });
|
|
701
|
+
if (r.status !== 200) fail(r.data.error || "지우지 못했습니다.");
|
|
702
|
+
return console.log(c.green("✓"), r.data.message);
|
|
703
|
+
}
|
|
704
|
+
fail(`모르는 env 명령입니다: ${sub}\n 쓸 수 있는 것: ls · set · rm`);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ---------- lock / unlock / open / usage ----------
|
|
708
|
+
function readSecret(prompt) {
|
|
709
|
+
if (!process.stdin.isTTY) {
|
|
710
|
+
// AI·자동화: 표준입력으로 받는다(명령 줄에 비밀번호를 적지 않게)
|
|
711
|
+
return new Promise((res) => {
|
|
712
|
+
let d = "";
|
|
713
|
+
process.stdin.setEncoding("utf8");
|
|
714
|
+
process.stdin.on("data", (x) => (d += x));
|
|
715
|
+
process.stdin.on("end", () => res(d.split(/\r?\n/)[0]));
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
return new Promise((res) => {
|
|
719
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
720
|
+
let muted = false;
|
|
721
|
+
rl._writeToOutput = (str) => { if (!muted) rl.output.write(str); };
|
|
722
|
+
rl.question(prompt, (a) => { rl.close(); process.stdout.write("\n"); res(a); });
|
|
723
|
+
muted = true;
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async function lock(args) {
|
|
728
|
+
const name = await siteName(args);
|
|
729
|
+
const pw = await readSecret("잠금 비밀번호(6자 이상, 화면에 안 보임): ");
|
|
730
|
+
if (!pw || pw.length < 6) fail("비밀번호는 6자 이상이어야 합니다. (AI·자동화에서는 표준입력으로: echo 비밀번호 | npx chiwaa lock)");
|
|
731
|
+
const r = await api("POST", `/api/v1/sites/${encodeURIComponent(name)}/lock`, { body: { password: pw } });
|
|
732
|
+
if (r.status !== 200) fail(r.data.message || r.data.error || "잠그지 못했습니다.");
|
|
733
|
+
console.log(c.green("✓"), r.data.message);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function unlock(args) {
|
|
737
|
+
const name = await siteName(args);
|
|
738
|
+
const r = await api("POST", `/api/v1/sites/${encodeURIComponent(name)}/unlock`);
|
|
739
|
+
if (r.status !== 200) fail(r.data.message || r.data.error || "잠금을 풀지 못했습니다.");
|
|
740
|
+
console.log(c.green("✓"), r.data.message);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function open(args) {
|
|
744
|
+
const name = await siteName(args);
|
|
745
|
+
const r = await api("GET", `/api/v1/sites/${encodeURIComponent(name)}`);
|
|
746
|
+
if (r.status !== 200) fail(r.data.error || "사이트 정보를 받지 못했습니다.");
|
|
747
|
+
const url = args.site ? r.data.url : r.data.manage_url;
|
|
748
|
+
console.log(`${args.site ? "사이트" : "관리 화면"}: ${c.cyan(url)}`);
|
|
749
|
+
if (!args["no-browser"]) openBrowser(url);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async function usage() {
|
|
753
|
+
const r = await api("GET", "/api/v1/usage");
|
|
754
|
+
if (r.status !== 200) fail(r.data.error || "사용량을 받지 못했습니다.");
|
|
755
|
+
const u = r.data;
|
|
756
|
+
const until = u.expires_at ? ` · ${u.expires_at.slice(0, 10)}까지${u.launch_event ? "(출시 이벤트 무료)" : ""}` : "";
|
|
757
|
+
console.log(c.bold(`${u.plan_name} 요금제`) + c.dim(`${until} · 사이트 ${u.sites.length}개 (한도 ${u.sites_limit})`));
|
|
758
|
+
console.log(c.dim("\n 이번 달 전송량(사이트마다) 저장 한도"));
|
|
759
|
+
const sz = (mb) => (mb >= 1024 ? `${+(mb / 1024).toFixed(1)}GB` : `${mb}MB`);
|
|
760
|
+
for (const s of u.sites) {
|
|
761
|
+
const lim = s.traffic_limit_gb * 1024;
|
|
762
|
+
const pct = lim ? Math.min(100, Math.round((s.traffic_mb / lim) * 100)) : 0;
|
|
763
|
+
const bar = "█".repeat(Math.round(pct / 5)).padEnd(20, "·");
|
|
764
|
+
console.log(` ${s.domain.padEnd(32)} ${bar} ${sz(s.traffic_mb)} / ${s.traffic_limit_gb}GB ${sz(s.storage_mb).padStart(6)}${s.locked ? c.dim(" (잠김)") : ""}`);
|
|
765
|
+
}
|
|
766
|
+
console.log(c.dim("\n한도를 넘어도 추가 요금은 없고, 그 사이트만 그달 말까지 느려집니다."));
|
|
767
|
+
}
|
|
768
|
+
|
|
663
769
|
async function whoami() {
|
|
664
770
|
const r = await api("GET", "/api/v1/me");
|
|
665
771
|
if (r.status !== 200) fail(r.data.error || "로그인 정보를 받지 못했습니다.");
|
|
@@ -679,6 +785,7 @@ const HELP = `chiwaa ${VERSION} — chiwaa.kr 에 사이트를 바로 올립니
|
|
|
679
785
|
--name <이름> 사이트 이름(<이름>.chiwaa.com)
|
|
680
786
|
--dir <폴더> 빌드 결과 폴더를 직접 지정
|
|
681
787
|
--no-build 빌드하지 않고 그대로 올림
|
|
788
|
+
--preview 운영 주소는 두고 이름--preview.chiwaa.com 에 미리보기로 올림
|
|
682
789
|
-y, --yes 묻지 않고 진행(AI 도구·자동화용)
|
|
683
790
|
chiwaa login 브라우저로 로그인 (--token <토큰> 도 가능)
|
|
684
791
|
chiwaa list 내 사이트 목록
|
|
@@ -697,6 +804,10 @@ const HELP = `chiwaa ${VERSION} — chiwaa.kr 에 사이트를 바로 올립니
|
|
|
697
804
|
chiwaa domains add <도메인> 도메인 붙이기(x-y.net 도메인은 바로, 다른 곳 도메인은 넣을 DNS 레코드 안내)
|
|
698
805
|
chiwaa domains check 연결됐는지 확인 · domains remove -y 로 연결 끊기
|
|
699
806
|
chiwaa stats 방문자 통계(오늘·최근 7일, 많이 본 페이지, 들어온 곳) --days 30
|
|
807
|
+
chiwaa env 서버 함수 비밀값 목록 · env set 이름(값은 안 보이게 입력) · env rm 이름
|
|
808
|
+
chiwaa lock / unlock 사이트 비밀번호 잠금(아이디 chiwaa) / 풀기 — 공개 전 나만 보기
|
|
809
|
+
chiwaa open 관리 화면 열기 (--site 는 사이트 열기)
|
|
810
|
+
chiwaa usage 이번 달 전송량·한도, 요금제
|
|
700
811
|
chiwaa agent AI 도구용 안내(AGENTS.md·CLAUDE.md)를 만들거나 새로 고침
|
|
701
812
|
(처음 deploy 할 때 자동으로 만든다)
|
|
702
813
|
chiwaa whoami 로그인한 계정
|
|
@@ -710,7 +821,7 @@ async function main() {
|
|
|
710
821
|
const cmd = args._[0];
|
|
711
822
|
if (args.version) return console.log(VERSION);
|
|
712
823
|
if (args.help || !cmd || cmd === "help") return console.log(HELP);
|
|
713
|
-
const cmds = { deploy, login, list, ls: list, versions, rollback, db, domains, domain: domains, stats, agent, whoami, logout };
|
|
824
|
+
const cmds = { deploy, login, list, ls: list, versions, rollback, db, domains, domain: domains, stats, env, lock, unlock, open, usage, agent, whoami, logout };
|
|
714
825
|
if (!cmds[cmd]) fail(`모르는 명령입니다: ${cmd}\n\n${HELP}`);
|
|
715
826
|
await cmds[cmd](args);
|
|
716
827
|
}
|