chiwaa 0.1.12 → 0.1.14

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 (2) hide show
  1. package/bin/chiwaa.js +121 -28
  2. package/package.json +1 -1
package/bin/chiwaa.js CHANGED
@@ -124,32 +124,65 @@ async function login(args) {
124
124
  console.log(c.green("✓"), `${me.data.user} 계정으로 로그인했습니다.`);
125
125
  return;
126
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;
127
+ // 앞서 시작한 로그인이 아직 살아 있으면 새 코드를 만들지 않고 이어서 기다린다(AI 도구가 대기 중에 명령을 끊는 경우)
128
+ if (await resumePendingLogin()) return;
129
+ let s = readJSON(PENDING_FILE);
130
+ const resumed = !!s;
131
+ if (!s) {
132
+ const r = await api("POST", "/api/v1/login", { auth: false });
133
+ if (r.status !== 200) fail(r.data.error || "로그인을 시작하지 못했습니다.");
134
+ s = { ...r.data, until: Date.now() + r.data.expires_in * 1000 };
135
+ fs.mkdirSync(CONF_DIR, { recursive: true });
136
+ fs.writeFileSync(PENDING_FILE, JSON.stringify(s), { mode: 0o600 });
137
+ }
138
+ console.log(`브라우저에서 아래 주소를 열고 코드를 확인한 뒤 ${c.bold("승인")}을 눌러 주세요.${resumed ? c.dim(" (앞서 연 로그인을 이어서 기다립니다)") : ""}\n`);
139
+ console.log(` ${c.cyan(s.verify_url)}`);
140
+ console.log(` 코드: ${c.bold(s.user_code)}\n`);
141
+ console.log(c.dim(`승인할 때까지 이 명령이 기다립니다(최대 ${Math.ceil((s.until - Date.now()) / 60000)}분). 중간에 끊겼으면 같은 명령을 다시 실행하면 이어집니다 — 다시 승인할 필요 없음.`));
142
+ if (!args["no-browser"] && !resumed) openBrowser(s.verify_url);
134
143
  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
- }
144
+ while (Date.now() < s.until) {
145
+ await new Promise((r) => setTimeout(r, (s.interval || 3) * 1000));
146
+ const done = await pollLogin(s);
147
+ if (done === "wait") continue;
148
+ process.stdout.write("\n");
149
+ if (done) return console.log(c.green("✓"), `${done} 계정으로 로그인했습니다.`);
150
+ fail("로그인하지 못했습니다. 다시 `chiwaa login` 해 주세요.");
148
151
  }
152
+ fs.rmSync(PENDING_FILE, { force: true });
149
153
  process.stdout.write("\n");
150
154
  fail("시간이 지났습니다. 다시 `chiwaa login` 해 주세요.");
151
155
  }
152
156
 
157
+ // 로그인 승인을 한 번 확인한다: 계정 이름(성공) · "wait"(아직) · null(끝남, 대기 파일 지움)
158
+ const PENDING_FILE = path.join(CONF_DIR, "login-pending.json");
159
+ async function pollLogin(s) {
160
+ const p = await api("POST", "/api/v1/login/poll", { auth: false, body: { device_code: s.device_code } });
161
+ if (p.status === 202) return "wait";
162
+ fs.rmSync(PENDING_FILE, { force: true });
163
+ if (p.status === 200 && p.data.token) {
164
+ saveToken(p.data.token);
165
+ return p.data.user || "내";
166
+ }
167
+ return null;
168
+ }
169
+
170
+ // 승인은 했는데 기다리던 명령이 끊긴 경우: 다음 명령에서 승인 결과를 받아 로그인을 마친다
171
+ async function resumePendingLogin() {
172
+ const s = readJSON(PENDING_FILE);
173
+ if (!s || token()) return false;
174
+ if (!(s.until > Date.now())) {
175
+ fs.rmSync(PENDING_FILE, { force: true });
176
+ return false;
177
+ }
178
+ const done = await pollLogin(s);
179
+ if (done && done !== "wait") {
180
+ console.log(c.green("✓"), `앞서 승인한 로그인을 이어받았습니다(${done} 계정).`);
181
+ return true;
182
+ }
183
+ return false;
184
+ }
185
+
153
186
  // ---------- tar.gz (의존성 없이 직접 만든다) ----------
154
187
  function tarHeader(name, size, mode, type, mtime) {
155
188
  const b = Buffer.alloc(512);
@@ -236,12 +269,20 @@ function buildIfNeeded(dir, args) {
236
269
  const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
237
270
  const found = OUTPUTS.find(([n]) => deps[n]);
238
271
  let outName = args.dir || (found ? found[1] : "dist");
239
- if (found && found[0] === "next" && !args.dir) checkNext(dir);
272
+ let restore = null;
273
+ if (found && found[0] === "next" && !args.dir) {
274
+ checkNext(dir);
275
+ if (!args["no-build"]) restore = wrapNextConfig(dir);
276
+ }
240
277
  if (args["no-build"]) {
241
278
  console.log(c.dim("빌드는 건너뜁니다(--no-build)."));
242
279
  } else {
243
- if (!fs.existsSync(path.join(dir, "node_modules"))) run("npm install", dir);
244
- run("npm run build", dir);
280
+ try {
281
+ if (!fs.existsSync(path.join(dir, "node_modules"))) run("npm install", dir);
282
+ run("npm run build", dir);
283
+ } finally {
284
+ if (restore) restore();
285
+ }
245
286
  }
246
287
  let out = path.join(dir, outName);
247
288
  if (!fs.existsSync(out)) {
@@ -289,8 +330,56 @@ function checkNext(dir) {
289
330
  fail(`이 Next.js 앱은 서버에서 돌아가는 기능을 씁니다(${shown}). chiwaa 는 화면 파일(HTML·JS)을 올리는 곳이라 이대로는 열리지 않습니다.\n` +
290
331
  `AI 에게 「이 Next.js 앱을 chiwaa 에 올릴 수 있게 output: 'export' 정적 사이트로 바꾸고, API·middleware 같은 서버 기능은 맨 위 api/ 폴더의 chiwaa 서버 함수로 옮겨 줘. ${AI_GUIDE}」라고 말해 보세요.`);
291
332
  }
333
+ }
334
+
335
+ // 서버 기능이 없는 Next.js 는 next.config 를 그대로 두고 이번 빌드만 정적 내보내기로 덧씌운다(빌드 서버 prepare_next 와 같은 덧씌우기).
336
+ // 빌드가 끝나면(실패·Ctrl+C 포함) 원래 설정으로 되돌린다. 되돌리는 함수, 덧씌울 필요가 없으면 null.
337
+ function wrapNextConfig(dir) {
292
338
  const conf = ["next.config.js", "next.config.mjs", "next.config.cjs", "next.config.ts"].find((n) => fs.existsSync(path.join(dir, n)));
293
- if (!conf || !/output\s*:\s*['"]export['"]/.test(fs.readFileSync(path.join(dir, conf), "utf8"))) fail(NEXT_EXPORT_MSG);
339
+ if (conf && /output\s*:\s*['"]export['"]/.test(fs.readFileSync(path.join(dir, conf), "utf8"))) return null;
340
+ const ext = conf ? path.extname(conf) : ".mjs";
341
+ const target = path.join(dir, conf || "next.config.mjs");
342
+ const orig = `next.config.chiwaa-orig${ext}`;
343
+ const body = [
344
+ ' const base = (typeof orig === "function" ? await orig(phase, ctx) : await orig) || {};',
345
+ " // cacheComponents·partialPrefetching(PPR)은 정적 내보내기와 함께 쓸 수 없다(Next 16 기본 설정에 들어 있음)",
346
+ " const { cacheComponents, partialPrefetching, ...rest } = base;",
347
+ ' return { ...rest, output: "export", images: { ...(rest.images || {}), unoptimized: true } };',
348
+ "",
349
+ ].join("\n");
350
+ let text;
351
+ if (!conf) {
352
+ text = 'export default { output: "export", images: { unoptimized: true } };\n';
353
+ } else {
354
+ const pkg = readJSON(path.join(dir, "package.json")) || {};
355
+ const cjs = ext === ".cjs" || (ext === ".js" && pkg.type !== "module");
356
+ const from = ext === ".ts" ? "next.config.chiwaa-orig" : orig;
357
+ text = cjs
358
+ ? `// @ts-nocheck
359
+ const orig = require("./${from}");
360
+ module.exports = async (phase, ctx) => {
361
+ ${body}};
362
+ `
363
+ : `// @ts-nocheck
364
+ import orig from "./${from}";
365
+ export default async function chiwaa(phase, ctx) {
366
+ ${body}}
367
+ `;
368
+ fs.renameSync(target, path.join(dir, orig));
369
+ }
370
+ fs.writeFileSync(target, text);
371
+ let done = false;
372
+ const restore = () => {
373
+ if (done) return;
374
+ done = true;
375
+ fs.rmSync(target, { force: true });
376
+ if (conf) fs.renameSync(path.join(dir, orig), target);
377
+ process.removeListener("SIGINT", onInt);
378
+ };
379
+ const onInt = () => { restore(); process.exit(130); };
380
+ process.on("SIGINT", onInt);
381
+ console.log(c.dim("next.config 는 그대로 두고, 이번 빌드만 정적 사이트로 내보냅니다(output: 'export')."));
382
+ return restore;
294
383
  }
295
384
 
296
385
  // 첫 화면(index.html)이 없는 폴더를 올리려 하면, 앱이나 HTML 이 들어 있는 하위 폴더를 알려 주고 멈춘다
@@ -419,7 +508,7 @@ async function deploy(args) {
419
508
  const dir = path.resolve(args._[1] || ".");
420
509
  if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) fail(`폴더가 없습니다: ${dir}`);
421
510
  if (!token()) {
422
- if (!TTY) fail("로그인이 필요합니다. 먼저 아래 명령으로 로그인하고(브라우저에서 승인), 같은 배포 명령을 다시 실행하세요.\n\n npx chiwaa login\n\n (자동화·AI 도구에서는 환경변수 CHIWAA_TOKEN 으로도 됩니다)");
511
+ if (!TTY) fail("로그인이 필요합니다. 먼저 아래 명령으로 로그인하고(브라우저에서 승인), 같은 배포 명령을 다시 실행하세요.\n\n npx chiwaa login\n\n (login 은 승인할 때까지 기다리는 명령입니다. 끊겼으면 다시 실행하면 같은 승인을 이어서 기다립니다. 자동화에서는 환경변수 CHIWAA_TOKEN 으로도 됩니다)");
423
512
  await login(args);
424
513
  }
425
514
  const site = await resolveSite(dir, args);
@@ -432,7 +521,7 @@ async function deploy(args) {
432
521
  files.push({ rel: "api", dir: true }, ...extra.files.map((f) => ({ ...f, rel: `api/${f.rel}` })));
433
522
  }
434
523
  const fnCount = files.filter((f) => !f.dir && /^api\/[^/_][^/]*\.(js|ts|mjs)$/.test(f.rel)).length;
435
- if (fnCount) console.log(c.dim(`서버 함수 ${fnCount}개(api/) 함께 올림 → https://<사이트>/api/<이름>`));
524
+ if (fnCount) console.log(c.dim(`서버 함수 ${fnCount}개(api/) 함께 올림 → https://<사이트>/api/<이름> (서버 함수는 1분 안에 켜집니다 — 그 전에는 404 일 수 있음)`));
436
525
  if (!files.some((f) => !f.dir && f.rel === "index.html")) fail(noIndexMessage(out));
437
526
  const fileCount = files.filter((f) => !f.dir).length;
438
527
  console.log(c.dim(`올리는 중: ${path.relative(process.cwd(), out) || "."} — 파일 ${fileCount}개, ${(total / 1048576).toFixed(1)}MB`));
@@ -513,7 +602,7 @@ function snippet(table, read, write) {
513
602
  <button type="submit">보내기</button>
514
603
  </form>
515
604
 
516
- <!-- 글 목록 보기(읽기 권한이 public 일 때) -->
605
+ ${read === "public" ? `<!-- 글 목록 보기 -->
517
606
  <ul id="${table}-list"></ul>
518
607
  <script>
519
608
  chiwaa.table("${table}").list({ limit: 20 }).then(function (items) {
@@ -526,7 +615,10 @@ function snippet(table, read, write) {
526
615
  });
527
616
  </script>
528
617
 
529
- <!-- 스크립트로 직접:
618
+ ` : `<!-- 읽기가 「나만」이라 받은 글을 사이트에 보여 주지 않습니다(목록을 붙이지 마세요).
619
+ 받은 글 보기: chiwaa.kr → 내 사이트 → 이 사이트 「자세히」 → 「DB」 → ${table} 줄의 「데이터 보기」, 또는 npx chiwaa db rows ${table} -->
620
+
621
+ `}<!-- 스크립트로 직접:
530
622
  chiwaa.table("${table}").add({ 제목, 내용, _pw }) 글쓰기(_pw 를 넣으면 그 비밀번호로 수정·삭제 가능)
531
623
  chiwaa.table("${table}").list({ limit: 20, before: 마지막id }) 목록(최신순, 다음 쪽은 before)
532
624
  chiwaa.table("${table}").get(id) 글 하나(상세 화면)
@@ -963,6 +1055,7 @@ async function main() {
963
1055
  if (args.help || !cmd || cmd === "help") return console.log(HELP);
964
1056
  const cmds = { deploy, login, list, ls: list, versions, rollback, db, files, domains, domain: domains, stats, env, logs, lock, unlock, open, usage, agent, whoami, logout };
965
1057
  if (!cmds[cmd]) fail(`모르는 명령입니다: ${cmd}\n\n${HELP}`);
1058
+ if (cmd !== "login" && cmd !== "logout") await resumePendingLogin();
966
1059
  await cmds[cmd](args);
967
1060
  }
968
1061
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chiwaa",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "chiwaa.kr 에 사이트를 터미널에서 바로 올리는 도구",
5
5
  "bin": {
6
6
  "chiwaa": "bin/chiwaa.js"