chiwaa 0.0.0-stage → 0.1.1

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 +30 -2
  2. package/bin/chiwaa.js +419 -0
  3. package/package.json +20 -4
package/README.md CHANGED
@@ -1,3 +1,31 @@
1
- # Temporary Holding Version
1
+ # chiwaa
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [chiwaa.kr](https://chiwaa.kr) 에 사이트를 터미널에서 바로 올립니다.
4
+
5
+ ```bash
6
+ npx chiwaa deploy
7
+ ```
8
+
9
+ 처음이면 브라우저로 로그인하고, 사이트 이름을 정한 뒤 `<이름>.chiwaa.com` 주소를 알려 줍니다.
10
+ `package.json` 에 `build` 스크립트가 있으면 먼저 빌드하고(Vite·React·Vue·Astro·Next export 등) 결과 폴더를 올립니다.
11
+
12
+ ## 명령
13
+
14
+ | 명령 | 하는 일 |
15
+ |---|---|
16
+ | `chiwaa deploy [폴더]` | 빌드해서 올리기 (`--name`, `--dir`, `--no-build`, `-y`) |
17
+ | `chiwaa login` | 브라우저로 로그인 (`--token <토큰>` 도 가능) |
18
+ | `chiwaa list` | 내 사이트 목록 |
19
+ | `chiwaa versions` | 남아 있는 버전 |
20
+ | `chiwaa rollback [버전]` | 한 단계 전 또는 지정 버전으로 되돌리기 |
21
+ | `chiwaa whoami` / `logout` | 계정 확인 / 로그아웃 |
22
+
23
+ ## AI 도구(Claude Code·Codex·Cursor)에서
24
+
25
+ 묻는 것 없이 끝까지 가려면 `-y` 를 붙입니다. 로그인은 `CHIWAA_TOKEN` 환경변수로도 됩니다.
26
+
27
+ ```bash
28
+ npx chiwaa deploy -y --name my-site
29
+ ```
30
+
31
+ 폴더에 생기는 `.chiwaa.json` 은 어느 사이트에 올릴지 적어 둔 파일입니다(비밀값 없음).
package/bin/chiwaa.js ADDED
@@ -0,0 +1,419 @@
1
+ #!/usr/bin/env node
2
+ // chiwaa CLI — chiwaa.kr 에 사이트를 터미널에서 바로 올린다. 의존성 없음(Node 18+).
3
+ "use strict";
4
+
5
+ const fs = require("fs");
6
+ const os = require("os");
7
+ const path = require("path");
8
+ const zlib = require("zlib");
9
+ const { spawnSync, spawn } = require("child_process");
10
+ const readline = require("readline");
11
+
12
+ const VERSION = require("../package.json").version;
13
+ const API = (process.env.CHIWAA_API || "https://chiwaa.kr").replace(/\/$/, "");
14
+ const CONF_DIR = path.join(os.homedir(), ".chiwaa");
15
+ const CONF_FILE = path.join(CONF_DIR, "config.json");
16
+ const LINK_FILE = ".chiwaa.json";
17
+ const TTY = process.stdout.isTTY && process.stdin.isTTY;
18
+
19
+ // 빌드 도구 → 결과 폴더 (관리 화면 build_detect 와 같은 기준)
20
+ const OUTPUTS = [
21
+ ["react-scripts", "build"],
22
+ ["next", "out"],
23
+ ["@angular/cli", "dist"],
24
+ ["vite", "dist"],
25
+ ["@vue/cli-service", "dist"],
26
+ ["astro", "dist"],
27
+ ["parcel", "dist"],
28
+ ];
29
+ // 그대로 올리는 사이트에서 빼는 것
30
+ const SKIP = new Set([".git", "node_modules", ".chiwaa.json", ".DS_Store", "Thumbs.db", ".vercel", ".netlify"]);
31
+
32
+ const c = {
33
+ dim: (s) => (TTY ? `\x1b[2m${s}\x1b[0m` : s),
34
+ bold: (s) => (TTY ? `\x1b[1m${s}\x1b[0m` : s),
35
+ green: (s) => (TTY ? `\x1b[32m${s}\x1b[0m` : s),
36
+ red: (s) => (TTY ? `\x1b[31m${s}\x1b[0m` : s),
37
+ cyan: (s) => (TTY ? `\x1b[36m${s}\x1b[0m` : s),
38
+ };
39
+
40
+ class Fail extends Error {}
41
+ const fail = (msg) => {
42
+ throw new Fail(msg);
43
+ };
44
+
45
+ // ---------- 설정 ----------
46
+ function readJSON(file) {
47
+ try {
48
+ return JSON.parse(fs.readFileSync(file, "utf8"));
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ function token() {
54
+ return process.env.CHIWAA_TOKEN || (readJSON(CONF_FILE) || {}).token || null;
55
+ }
56
+ function saveToken(t) {
57
+ fs.mkdirSync(CONF_DIR, { recursive: true });
58
+ fs.writeFileSync(CONF_FILE, JSON.stringify({ token: t }, null, 2), { mode: 0o600 });
59
+ }
60
+
61
+ // ---------- API ----------
62
+ async function api(method, p, { body, headers = {}, auth = true } = {}) {
63
+ const h = { "User-Agent": `chiwaa-cli/${VERSION}`, ...headers };
64
+ if (auth) {
65
+ const t = token();
66
+ if (!t) fail("로그인이 필요합니다. 먼저 아래 명령으로 로그인하세요(브라우저에서 승인).\n\n npx chiwaa login\n");
67
+ h.Authorization = `Bearer ${t}`;
68
+ }
69
+ let payload = body;
70
+ if (body && !Buffer.isBuffer(body)) {
71
+ payload = JSON.stringify(body);
72
+ h["Content-Type"] = "application/json";
73
+ }
74
+ let res;
75
+ try {
76
+ res = await fetch(API + p, { method, headers: h, body: payload });
77
+ } catch (e) {
78
+ fail(`chiwaa.kr 에 연결하지 못했습니다 (${e.cause?.code || e.message}). 인터넷 연결을 확인하세요.`);
79
+ }
80
+ let data = {};
81
+ try {
82
+ data = await res.json();
83
+ } catch {}
84
+ return { status: res.status, data };
85
+ }
86
+
87
+ // ---------- 인자 ----------
88
+ function parseArgs(argv) {
89
+ const out = { _: [] };
90
+ for (let i = 0; i < argv.length; i++) {
91
+ const a = argv[i];
92
+ if (a.startsWith("--")) {
93
+ const [k, v] = a.slice(2).split("=", 2);
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];
96
+ else out[k] = true;
97
+ } else if (a === "-y") out.yes = true;
98
+ else if (a === "-h") out.help = true;
99
+ else if (a === "-v") out.version = true;
100
+ else out._.push(a);
101
+ }
102
+ return out;
103
+ }
104
+
105
+ function ask(q) {
106
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
107
+ return new Promise((r) => rl.question(q, (a) => (rl.close(), r(a.trim()))));
108
+ }
109
+
110
+ function openBrowser(url) {
111
+ const cmd = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]]
112
+ : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
113
+ try {
114
+ spawn(cmd[0], cmd[1], { stdio: "ignore", detached: true }).unref();
115
+ } catch {}
116
+ }
117
+
118
+ // ---------- login ----------
119
+ async function login(args) {
120
+ if (args.token) {
121
+ saveToken(args.token);
122
+ const me = await api("GET", "/api/v1/me");
123
+ if (me.status !== 200) fail(me.data.error || "토큰이 맞지 않습니다.");
124
+ console.log(c.green("✓"), `${me.data.user} 계정으로 로그인했습니다.`);
125
+ return;
126
+ }
127
+ const s = await api("POST", "/api/v1/login", { auth: false });
128
+ if (s.status !== 200) fail(s.data.error || "로그인을 시작하지 못했습니다.");
129
+ console.log(`브라우저에서 아래 주소를 열고 코드를 확인한 뒤 ${c.bold("승인")}을 눌러 주세요.\n`);
130
+ console.log(` ${c.cyan(s.data.verify_url)}`);
131
+ console.log(` 코드: ${c.bold(s.data.user_code)}\n`);
132
+ if (!args["no-browser"]) openBrowser(s.data.verify_url);
133
+ const until = Date.now() + s.data.expires_in * 1000;
134
+ process.stdout.write(c.dim("승인을 기다리는 중…"));
135
+ while (Date.now() < until) {
136
+ await new Promise((r) => setTimeout(r, (s.data.interval || 3) * 1000));
137
+ const p = await api("POST", "/api/v1/login/poll", { auth: false, body: { device_code: s.data.device_code } });
138
+ if (p.status === 200 && p.data.token) {
139
+ saveToken(p.data.token);
140
+ process.stdout.write("\n");
141
+ console.log(c.green("✓"), `${p.data.user} 계정으로 로그인했습니다.`);
142
+ return;
143
+ }
144
+ if (p.status !== 202) {
145
+ process.stdout.write("\n");
146
+ fail(p.data.error || "로그인하지 못했습니다.");
147
+ }
148
+ }
149
+ process.stdout.write("\n");
150
+ fail("시간이 지났습니다. 다시 `chiwaa login` 해 주세요.");
151
+ }
152
+
153
+ // ---------- tar.gz (의존성 없이 직접 만든다) ----------
154
+ function tarHeader(name, size, mode, type, mtime) {
155
+ const b = Buffer.alloc(512);
156
+ const put = (s, off, len) => b.write(s, off, len, "utf8");
157
+ const oct = (n, len) => n.toString(8).padStart(len - 1, "0") + "\0";
158
+ put(name, 0, 100);
159
+ put(oct(mode, 8), 100, 8);
160
+ put(oct(0, 8), 108, 8);
161
+ put(oct(0, 8), 116, 8);
162
+ put(oct(size, 12), 124, 12);
163
+ put(oct(mtime, 12), 136, 12);
164
+ put(" ", 148, 8);
165
+ put(type, 156, 1);
166
+ put("ustar\0", 257, 6);
167
+ put("00", 263, 2);
168
+ let sum = 0;
169
+ for (const x of b) sum += x;
170
+ put(oct(sum, 7).padStart(7, "0").slice(0, 6) + "\0 ", 148, 8);
171
+ return b;
172
+ }
173
+ // 100바이트를 넘는 이름(한글 포함)은 pax 확장 헤더로 넘긴다
174
+ function tarEntry(name, size, mode, type, mtime) {
175
+ if (Buffer.byteLength(name) < 100 && /^[\x20-\x7e]*$/.test(name)) return [tarHeader(name, size, mode, type, mtime)];
176
+ // 레코드 = "<길이> path=<이름>\n", 길이는 자기 자릿수까지 포함한다
177
+ const base = Buffer.byteLength(` path=${name}\n`);
178
+ let len = base + String(base).length;
179
+ if (String(len).length !== String(base).length) len = base + String(len).length;
180
+ const body = Buffer.from(`${len} path=${name}\n`);
181
+ const pad = Buffer.alloc((512 - (body.length % 512)) % 512);
182
+ const short = "PaxHeader/" + Buffer.from(name).subarray(-80).toString("latin1").replace(/[^\x20-\x7e]/g, "_");
183
+ return [tarHeader(short, body.length, 0o644, "x", mtime), body, pad, tarHeader(short, size, mode, type, mtime)];
184
+ }
185
+
186
+ function collect(root, skipDotEnv) {
187
+ const files = [];
188
+ let total = 0;
189
+ const walk = (dir, rel) => {
190
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
191
+ if (SKIP.has(e.name)) continue;
192
+ if (skipDotEnv && /^\.env(\..*)?$/.test(e.name)) continue;
193
+ const abs = path.join(dir, e.name);
194
+ const r = rel ? `${rel}/${e.name}` : e.name;
195
+ if (e.isSymbolicLink()) continue;
196
+ if (e.isDirectory()) {
197
+ files.push({ rel: r, dir: true });
198
+ walk(abs, r);
199
+ } else if (e.isFile()) {
200
+ const st = fs.statSync(abs);
201
+ total += st.size;
202
+ files.push({ rel: r, abs, size: st.size, mtime: Math.floor(st.mtimeMs / 1000) });
203
+ }
204
+ }
205
+ };
206
+ walk(root, "");
207
+ return { files, total };
208
+ }
209
+
210
+ function makeTarGz(files) {
211
+ const parts = [];
212
+ const now = Math.floor(Date.now() / 1000);
213
+ for (const f of files) {
214
+ if (f.dir) {
215
+ parts.push(...tarEntry(`./${f.rel}/`, 0, 0o755, "5", now));
216
+ } else {
217
+ parts.push(...tarEntry(`./${f.rel}`, f.size, 0o644, "0", f.mtime));
218
+ const data = fs.readFileSync(f.abs);
219
+ parts.push(data, Buffer.alloc((512 - (data.length % 512)) % 512));
220
+ }
221
+ }
222
+ parts.push(Buffer.alloc(1024));
223
+ return zlib.gzipSync(Buffer.concat(parts), { level: 6 });
224
+ }
225
+
226
+ // ---------- 빌드 ----------
227
+ function run(cmd, cwd) {
228
+ console.log(c.dim(`$ ${cmd}`));
229
+ const r = spawnSync(cmd, { cwd, stdio: "inherit", shell: true });
230
+ if (r.status !== 0) fail(`명령이 실패했습니다: ${cmd}`);
231
+ }
232
+
233
+ function buildIfNeeded(dir, args) {
234
+ const pkg = readJSON(path.join(dir, "package.json"));
235
+ if (!pkg || !pkg.scripts || !pkg.scripts.build) return { out: dir, built: false };
236
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
237
+ const found = OUTPUTS.find(([n]) => deps[n]);
238
+ let outName = args.dir || (found ? found[1] : "dist");
239
+ if (args["no-build"]) {
240
+ console.log(c.dim("빌드는 건너뜁니다(--no-build)."));
241
+ } else {
242
+ if (!fs.existsSync(path.join(dir, "node_modules"))) run("npm install", dir);
243
+ run("npm run build", dir);
244
+ }
245
+ let out = path.join(dir, outName);
246
+ if (!fs.existsSync(out)) {
247
+ const alt = ["dist", "build", "out"].find((d) => fs.existsSync(path.join(dir, d, "index.html")));
248
+ if (!alt) fail(`빌드 결과 폴더(${outName})가 없습니다. --dir <폴더> 로 알려 주세요.`);
249
+ out = path.join(dir, alt);
250
+ }
251
+ if (found && found[0] === "next" && !fs.existsSync(path.join(out, "index.html")))
252
+ fail("Next.js 는 next.config 에 output: 'export' 를 넣어야 올릴 수 있습니다.");
253
+ return { out, built: true };
254
+ }
255
+
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
+ // 폴더 이름으로 기본 이름을 지어 줄 때만 쓴다
267
+ function slug(s) {
268
+ let n = String(s).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 30);
269
+ if (n.length < 2) n = `site-${n}`.replace(/-$/, "");
270
+ return n;
271
+ }
272
+
273
+ async function resolveSite(dir, args) {
274
+ const link = readJSON(path.join(dir, LINK_FILE));
275
+ if (link && link.site && !args.name) {
276
+ const r = await api("GET", `/api/v1/sites/${encodeURIComponent(link.site)}`);
277
+ if (r.status === 200) return linkSite(dir, r.data);
278
+ console.log(c.dim(`${LINK_FILE} 의 사이트(${link.site})를 찾지 못해 새로 정합니다.`));
279
+ }
280
+ let name = args.name ? checkName(args.name) : slug(path.basename(path.resolve(dir)));
281
+ if (args.name) {
282
+ const r = await api("GET", `/api/v1/sites/${encodeURIComponent(name)}`);
283
+ if (r.status === 200) return linkSite(dir, r.data);
284
+ } else if (TTY && !args.yes) {
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
+ }
291
+ const r = await api("GET", `/api/v1/sites/${encodeURIComponent(name)}`);
292
+ if (r.status === 200) return linkSite(dir, r.data);
293
+ }
294
+ for (let i = 0; i < 4; i++) {
295
+ const tryName = i === 0 ? name : `${name.slice(0, 25)}-${Math.random().toString(36).slice(2, 6)}`;
296
+ const r = await api("POST", "/api/v1/sites", { body: { name: tryName } });
297
+ if (r.status === 201) {
298
+ console.log(c.green("✓"), `새 사이트를 만들었습니다: ${c.bold(r.data.domain)}`);
299
+ return linkSite(dir, r.data);
300
+ }
301
+ if (r.data.code !== "name_taken" || args.name) fail(r.data.error || "사이트를 만들지 못했습니다.");
302
+ }
303
+ fail("쓸 수 있는 이름을 찾지 못했습니다. --name <이름> 으로 정해 주세요.");
304
+ }
305
+
306
+ function linkSite(dir, site) {
307
+ if (site.source === "github")
308
+ fail(`${site.domain} 은 깃허브(${site.repo})로 배포하는 사이트입니다. CLI 로는 다른 이름을 쓰세요: --name <새 이름>`);
309
+ fs.writeFileSync(path.join(dir, LINK_FILE), JSON.stringify({ site: site.name }, null, 2) + "\n");
310
+ return site;
311
+ }
312
+
313
+ // ---------- deploy ----------
314
+ async function deploy(args) {
315
+ const dir = path.resolve(args._[1] || ".");
316
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) fail(`폴더가 없습니다: ${dir}`);
317
+ if (!token()) {
318
+ if (!TTY) fail("로그인이 필요합니다. 먼저 아래 명령으로 로그인하고(브라우저에서 승인), 같은 배포 명령을 다시 실행하세요.\n\n npx chiwaa login\n\n (자동화·AI 도구에서는 환경변수 CHIWAA_TOKEN 으로도 됩니다)");
319
+ await login(args);
320
+ }
321
+ const site = await resolveSite(dir, args);
322
+ const { out, built } = buildIfNeeded(dir, args);
323
+ const { files, total } = collect(out, !built);
324
+ if (!files.some((f) => !f.dir && /^index\.html?$/i.test(f.rel)))
325
+ console.log(c.dim("참고: 맨 위에 index.html 이 없습니다. 주소로 열면 첫 화면이 안 보일 수 있습니다."));
326
+ const fileCount = files.filter((f) => !f.dir).length;
327
+ console.log(c.dim(`올리는 중: ${path.relative(process.cwd(), out) || "."} — 파일 ${fileCount}개, ${(total / 1048576).toFixed(1)}MB`));
328
+ const gz = makeTarGz(files);
329
+ const t0 = Date.now();
330
+ const r = await api("POST", `/api/v1/sites/${encodeURIComponent(site.name)}/deploy`, {
331
+ body: gz,
332
+ headers: { "Content-Type": "application/gzip" },
333
+ });
334
+ if (r.status !== 200 || !r.data.ok) fail(r.data.message || r.data.error || `배포하지 못했습니다 (${r.status}).`);
335
+ console.log(c.green("✓"), `배포 완료 ${c.dim(`(${((Date.now() - t0) / 1000).toFixed(1)}초)`)}`);
336
+ console.log(`\n ${c.bold(c.cyan(r.data.url))}\n`);
337
+ }
338
+
339
+ // ---------- 그 밖 ----------
340
+ async function list() {
341
+ const r = await api("GET", "/api/v1/sites");
342
+ if (r.status !== 200) fail(r.data.error || "목록을 받지 못했습니다.");
343
+ if (!r.data.sites.length) return console.log("아직 사이트가 없습니다. 사이트 폴더에서 `chiwaa deploy` 를 실행하세요.");
344
+ for (const s of r.data.sites) console.log(`${s.url} ${c.dim(s.source === "cli" ? "CLI" : `GitHub ${s.repo}`)}`);
345
+ }
346
+
347
+ async function siteName(args) {
348
+ if (args.name) return checkName(args.name);
349
+ const link = readJSON(path.join(process.cwd(), LINK_FILE));
350
+ if (link && link.site) return link.site;
351
+ fail(`어느 사이트인지 모릅니다. 사이트 폴더에서 실행하거나 --name <이름> 을 붙여 주세요.`);
352
+ }
353
+
354
+ async function versions(args) {
355
+ const name = await siteName(args);
356
+ const r = await api("GET", `/api/v1/sites/${encodeURIComponent(name)}`);
357
+ if (r.status !== 200) fail(r.data.error || "사이트 정보를 받지 못했습니다.");
358
+ console.log(c.bold(r.data.domain));
359
+ for (const v of r.data.releases) console.log(` ${v}${v === r.data.live ? c.green(" ← 지금 보이는 버전") : ""}`);
360
+ console.log(c.dim("\n되돌리기: chiwaa rollback <버전>"));
361
+ }
362
+
363
+ async function rollback(args) {
364
+ const name = await siteName(args);
365
+ const release = args._[1];
366
+ if (release && !/^\d{8}-\d{6}$/.test(release)) fail(`버전 이름이 잘못됐습니다: ${release} (chiwaa versions 로 확인)`);
367
+ const r = await api("POST", `/api/v1/sites/${encodeURIComponent(name)}/rollback`, { body: release ? { release } : {} });
368
+ if (r.status !== 200) fail(r.data.message || r.data.error || "되돌리지 못했습니다.");
369
+ console.log(c.green("✓"), r.data.message);
370
+ }
371
+
372
+ async function whoami() {
373
+ const r = await api("GET", "/api/v1/me");
374
+ if (r.status !== 200) fail(r.data.error || "로그인 정보를 받지 못했습니다.");
375
+ console.log(`${r.data.user} · ${r.data.plan_name} 요금제 · 사이트 ${r.data.sites}/${r.data.sites_limit}`);
376
+ }
377
+
378
+ function logout() {
379
+ try {
380
+ fs.unlinkSync(CONF_FILE);
381
+ } catch {}
382
+ console.log("로그아웃했습니다.");
383
+ }
384
+
385
+ const HELP = `chiwaa ${VERSION} — chiwaa.kr 에 사이트를 바로 올립니다
386
+
387
+ chiwaa deploy [폴더] 지금 폴더(또는 지정 폴더)를 빌드해서 올립니다
388
+ --name <이름> 사이트 이름(<이름>.chiwaa.com)
389
+ --dir <폴더> 빌드 결과 폴더를 직접 지정
390
+ --no-build 빌드하지 않고 그대로 올림
391
+ -y, --yes 묻지 않고 진행(AI 도구·자동화용)
392
+ chiwaa login 브라우저로 로그인 (--token <토큰> 도 가능)
393
+ chiwaa list 내 사이트 목록
394
+ chiwaa versions 남아 있는 버전 보기
395
+ chiwaa rollback [버전] 한 단계 전(또는 지정 버전)으로 되돌리기
396
+ chiwaa whoami 로그인한 계정
397
+ chiwaa logout 로그아웃
398
+
399
+ 환경변수: CHIWAA_TOKEN (로그인 대신), CHIWAA_API (기본 https://chiwaa.kr)
400
+ `;
401
+
402
+ async function main() {
403
+ const args = parseArgs(process.argv.slice(2));
404
+ const cmd = args._[0];
405
+ if (args.version) return console.log(VERSION);
406
+ if (args.help || !cmd || cmd === "help") return console.log(HELP);
407
+ const cmds = { deploy, login, list, ls: list, versions, rollback, whoami, logout };
408
+ if (!cmds[cmd]) fail(`모르는 명령입니다: ${cmd}\n\n${HELP}`);
409
+ await cmds[cmd](args);
410
+ }
411
+
412
+ main().catch((e) => {
413
+ if (e instanceof Fail) {
414
+ console.error(c.red("✗"), e.message);
415
+ } else {
416
+ console.error(c.red("✗"), "예상하지 못한 오류:", e && e.stack ? e.stack : e);
417
+ }
418
+ process.exitCode = 1;
419
+ });
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "chiwaa",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "chiwaa.kr 에 사이트를 터미널에서 바로 올리는 도구",
5
+ "bin": {
6
+ "chiwaa": "bin/chiwaa.js"
7
+ },
8
+ "files": [
9
+ "bin"
10
+ ],
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "homepage": "https://chiwaa.kr",
15
+ "keywords": [
16
+ "deploy",
17
+ "hosting",
18
+ "static",
19
+ "chiwaa"
20
+ ],
21
+ "license": "MIT"
22
+ }