chiwaa 0.1.19 → 0.1.20

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 +108 -2
  2. package/package.json +1 -1
package/bin/chiwaa.js CHANGED
@@ -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", "read", "write", "page", "days", "lines", "branch"].includes(k) && argv[i + 1] && !argv[i + 1].startsWith("-")) out[k] = argv[++i];
95
+ else if (["name", "token", "dir", "read", "write", "page", "days", "lines", "branch", "file"].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;
@@ -1089,7 +1089,19 @@ async function pg(args) {
1089
1089
  if (r.status !== 200) fail(r.data.error || "비밀번호를 바꾸지 못했습니다.");
1090
1090
  return console.log(c.green("✓"), r.data.message);
1091
1091
  }
1092
- if (sub !== "status") fail("쓰는 법: chiwaa pg [status|create|password]");
1092
+ if (sub === "query") {
1093
+ let sql = args._.slice(2).join(" ");
1094
+ if (args.file) sql = fs.readFileSync(String(args.file), "utf8");
1095
+ else if (!sql && !process.stdin.isTTY) sql = await readStdin();
1096
+ if (!sql.trim()) fail('실행할 SQL 을 적어 주세요. 예: chiwaa pg query "select * from users limit 5" · 파일은 --file 001.sql');
1097
+ const r = await pgQuery(base, sql);
1098
+ if (args.json) return console.log(JSON.stringify(r));
1099
+ printPgResult(r);
1100
+ if (r.error) process.exitCode = 1;
1101
+ return;
1102
+ }
1103
+ if (sub === "shell") return pgShell(base);
1104
+ if (sub !== "status") fail("쓰는 법: chiwaa pg [status|create|password|shell|query]");
1093
1105
  const r = await api("GET", base);
1094
1106
  if (r.status !== 200) fail(r.data.error || "상태를 받지 못했습니다.");
1095
1107
  const d = r.data;
@@ -1098,6 +1110,98 @@ async function pg(args) {
1098
1110
  if (d.error) fail(d.error);
1099
1111
  console.log(`${c.bold(d.name)} · 사용 ${(d.bytes / 1048576).toFixed(1)}MB / ${lim} · 연결 ${d.connections}/${d.conn_limit}${d.readonly ? c.red(" · 용량을 넘어 읽기 전용") : ""}`);
1100
1112
  console.log(c.dim("컨테이너에 CHIWAA_PG_URL 로 들어갑니다(설정값에 DATABASE_URL 이 없으면 DATABASE_URL 로도)."));
1113
+ console.log(c.dim('들여다보기: npx chiwaa pg shell · npx chiwaa pg query "select …"'));
1114
+ }
1115
+
1116
+ function readStdin() {
1117
+ return new Promise((res) => {
1118
+ let d = "";
1119
+ process.stdin.setEncoding("utf8");
1120
+ process.stdin.on("data", (x) => (d += x));
1121
+ process.stdin.on("end", () => res(d));
1122
+ });
1123
+ }
1124
+
1125
+ async function pgQuery(base, sql) {
1126
+ const r = await api("POST", `${base}/query`, { body: { sql } });
1127
+ if (r.status !== 200) fail(r.data.error || r.data.message || `SQL 을 실행하지 못했습니다 (${r.status}).`);
1128
+ return r.data;
1129
+ }
1130
+
1131
+ // 한글·한자 등은 터미널에서 두 칸
1132
+ const cellWidth = (s) => [...s].reduce((n, ch) => n + (/[ᄀ-ᅟ⺀-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]/.test(ch) ? 2 : 1), 0);
1133
+ function cellText(v) {
1134
+ if (v === null) return c.dim("null");
1135
+ const s = String(v).replace(/\r?\n/g, "↵");
1136
+ if (cellWidth(s) <= 60) return s;
1137
+ let out = "";
1138
+ for (const ch of s) { if (cellWidth(out + ch) > 57) break; out += ch; }
1139
+ return out + "…";
1140
+ }
1141
+
1142
+ function printPgResult(r) {
1143
+ for (const x of r.results || []) {
1144
+ if (!x.columns) { console.log(c.dim(x.command)); continue; }
1145
+ const rows = x.rows.map((row) => row.map(cellText));
1146
+ const plain = (s) => s.replace(/\x1b\[\d+m/g, "");
1147
+ const w = x.columns.map((col, i) => Math.max(cellWidth(col), ...rows.map((row) => cellWidth(plain(row[i])))));
1148
+ const pad = (s, i) => s + " ".repeat(Math.max(0, w[i] - cellWidth(plain(s))));
1149
+ console.log(" " + x.columns.map((col, i) => c.bold(pad(col, i))).join(" │ "));
1150
+ console.log("─" + w.map((n) => "─".repeat(n)).join("─┼─") + "─");
1151
+ for (const row of rows) console.log(" " + row.map(pad).join(" │ "));
1152
+ const more = x.total > x.rows.length ? ` — ${x.rows.length}줄만 보여 줍니다(limit 을 붙여 나눠 보세요)` : "";
1153
+ console.log(c.dim(`(${x.total}줄${more})`));
1154
+ }
1155
+ for (const n of r.notices || []) console.log(c.dim(n));
1156
+ if (r.error) console.error(c.red("✗"), r.error);
1157
+ }
1158
+
1159
+ // 셸에서는 오류가 나도 끝내지 않는다
1160
+ async function shellRun(base, sql) {
1161
+ try {
1162
+ printPgResult(await pgQuery(base, sql));
1163
+ } catch (e) {
1164
+ if (!(e instanceof Fail)) throw e;
1165
+ console.error(c.red("✗"), e.message);
1166
+ }
1167
+ }
1168
+
1169
+ // 대화형: 문장은 ; 로 끝내면 보낸다. 줄마다 바로 서버에서 실행된다(따로 트랜잭션을 잇지 않음 — 문장마다 새 연결).
1170
+ async function pgShell(base) {
1171
+ const st = await api("GET", base);
1172
+ if (st.status !== 200) fail(st.data.error || "상태를 받지 못했습니다.");
1173
+ if (!st.data.exists) fail(`Postgres 가 없습니다. ${c.bold("npx chiwaa pg create")} 로 먼저 만드세요.`);
1174
+ const db = st.data.name;
1175
+ console.log(`${c.bold(db)} 에 연결했습니다. SQL 은 ; 로 끝내면 실행됩니다. ${c.dim("\\dt 표 목록 · \\d 표 칸 · \\q 끝내기")}`);
1176
+ console.log(c.dim("문장마다 새 연결이라 BEGIN … COMMIT 은 한 번에(한 줄 또는 ; 가 여러 개인 한 묶음) 보내야 합니다. 문장마다 30초 제한."));
1177
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: !!process.stdin.isTTY });
1178
+ const META = {
1179
+ "\\dt": "select table_schema as schema, table_name as 표, table_type as 종류 from information_schema.tables where table_schema not in ('pg_catalog','information_schema') order by 1, 2",
1180
+ };
1181
+ let buf = "";
1182
+ let closed = false; // 표준입력으로 받으면 입력이 먼저 끝나 닫힌다(남은 줄은 그대로 실행)
1183
+ rl.on("close", () => (closed = true));
1184
+ const prompt = () => { if (closed || !process.stdin.isTTY) return; rl.setPrompt(buf ? `${db}-> ` : `${db}=> `); rl.prompt(); };
1185
+ prompt();
1186
+ for await (const line of rl) {
1187
+ const t = line.trim();
1188
+ if (!buf && (t === "\\q" || t === "exit" || t === "quit")) break;
1189
+ if (!buf && t.startsWith("\\")) {
1190
+ const m = t.match(/^\\d\s+([A-Za-z_][\w.]*)$/);
1191
+ const sql = META[t] || (m && `select column_name as 칸, data_type as 형식, is_nullable as 빈값, column_default as 기본값 from information_schema.columns where table_name = '${m[1].split(".").pop()}' order by ordinal_position`);
1192
+ if (sql) await shellRun(base, sql);
1193
+ else console.log(c.dim("쓸 수 있는 것: \\dt · \\d <표> · \\q (그 밖의 psql 명령은 안 됩니다)"));
1194
+ } else if (t) {
1195
+ buf += (buf ? "\n" : "") + line;
1196
+ if (t.endsWith(";")) {
1197
+ await shellRun(base, buf);
1198
+ buf = "";
1199
+ }
1200
+ }
1201
+ prompt();
1202
+ }
1203
+ if (buf.trim()) await shellRun(base, buf); // 마지막 문장에 ; 를 빠뜨린 경우
1204
+ rl.close();
1101
1205
  }
1102
1206
 
1103
1207
  async function usage() {
@@ -1163,6 +1267,8 @@ const HELP = `chiwaa ${VERSION} — chiwaa.kr 에 사이트를 바로 올립니
1163
1267
  chiwaa stats 방문자 통계(오늘·최근 7일, 많이 본 페이지, 들어온 곳) --days 30
1164
1268
  chiwaa files 사진 목록·용량 · files put 사진.jpg(주인 사진 올리기) · files on|off(방문자 사진 올리기)
1165
1269
  chiwaa pg [create|password] 관리형 Postgres(컨테이너 앱의 DATABASE_URL) 상태·만들기·비밀번호 새로
1270
+ chiwaa pg shell 내 Postgres 에 SQL 대화형으로(\\dt 표 목록 · \\d 표 · \\q)
1271
+ chiwaa pg query "<SQL>" SQL 한 번 실행 · --file 001.sql(마이그레이션) · --json(AI·자동화)
1166
1272
  chiwaa logs 컨테이너 앱 기록(-f 이어서 보기, --branch <브랜치>) 또는 서버 함수 기록 --lines 100, --preview
1167
1273
  chiwaa env 서버 함수 비밀값 목록 · env set 이름(값은 안 보이게 입력) · env rm 이름
1168
1274
  chiwaa lock / unlock 사이트 비밀번호 잠금(아이디 chiwaa) / 풀기 — 공개 전 나만 보기
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chiwaa",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "chiwaa.kr 에 사이트를 터미널에서 바로 올리는 도구",
5
5
  "bin": {
6
6
  "chiwaa": "bin/chiwaa.js"