chiwaa 0.1.18 → 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.
- package/bin/chiwaa.js +136 -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;
|
|
@@ -1073,6 +1073,137 @@ async function open(args) {
|
|
|
1073
1073
|
if (!args["no-browser"]) openBrowser(url);
|
|
1074
1074
|
}
|
|
1075
1075
|
|
|
1076
|
+
// ---------- pg (관리형 Postgres) ----------
|
|
1077
|
+
async function pg(args) {
|
|
1078
|
+
const sub = args._[1] || "status";
|
|
1079
|
+
const name = await siteName(args);
|
|
1080
|
+
const base = `/api/v1/sites/${encodeURIComponent(name)}/pg`;
|
|
1081
|
+
if (sub === "create") {
|
|
1082
|
+
const r = await api("POST", base);
|
|
1083
|
+
if (r.status !== 200) fail(r.data.error || "Postgres 를 만들지 못했습니다.");
|
|
1084
|
+
console.log(c.green("✓"), r.data.message);
|
|
1085
|
+
return console.log(c.dim("앱에서는 process.env.DATABASE_URL(또는 CHIWAA_PG_URL). 컨테이너 앱에서만 접속됩니다. 다시 배포: npx chiwaa deploy"));
|
|
1086
|
+
}
|
|
1087
|
+
if (sub === "password") {
|
|
1088
|
+
const r = await api("POST", `${base}/password`);
|
|
1089
|
+
if (r.status !== 200) fail(r.data.error || "비밀번호를 바꾸지 못했습니다.");
|
|
1090
|
+
return console.log(c.green("✓"), r.data.message);
|
|
1091
|
+
}
|
|
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]");
|
|
1105
|
+
const r = await api("GET", base);
|
|
1106
|
+
if (r.status !== 200) fail(r.data.error || "상태를 받지 못했습니다.");
|
|
1107
|
+
const d = r.data;
|
|
1108
|
+
const lim = d.limit_mb >= 1024 ? `${d.limit_mb / 1024}GB` : `${d.limit_mb}MB`;
|
|
1109
|
+
if (!d.exists) return console.log(`Postgres 없음 — ${c.bold("npx chiwaa pg create")} 로 만듭니다(${lim} · 동시 연결 ${d.conn_limit}개 · 매일 백업).`);
|
|
1110
|
+
if (d.error) fail(d.error);
|
|
1111
|
+
console.log(`${c.bold(d.name)} · 사용 ${(d.bytes / 1048576).toFixed(1)}MB / ${lim} · 연결 ${d.connections}/${d.conn_limit}${d.readonly ? c.red(" · 용량을 넘어 읽기 전용") : ""}`);
|
|
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();
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1076
1207
|
async function usage() {
|
|
1077
1208
|
const r = await api("GET", "/api/v1/usage");
|
|
1078
1209
|
if (r.status !== 200) fail(r.data.error || "사용량을 받지 못했습니다.");
|
|
@@ -1135,6 +1266,9 @@ const HELP = `chiwaa ${VERSION} — chiwaa.kr 에 사이트를 바로 올립니
|
|
|
1135
1266
|
chiwaa domains check 연결됐는지 확인 · domains remove -y 로 연결 끊기
|
|
1136
1267
|
chiwaa stats 방문자 통계(오늘·최근 7일, 많이 본 페이지, 들어온 곳) --days 30
|
|
1137
1268
|
chiwaa files 사진 목록·용량 · files put 사진.jpg(주인 사진 올리기) · files on|off(방문자 사진 올리기)
|
|
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·자동화)
|
|
1138
1272
|
chiwaa logs 컨테이너 앱 기록(-f 이어서 보기, --branch <브랜치>) 또는 서버 함수 기록 --lines 100, --preview
|
|
1139
1273
|
chiwaa env 서버 함수 비밀값 목록 · env set 이름(값은 안 보이게 입력) · env rm 이름
|
|
1140
1274
|
chiwaa lock / unlock 사이트 비밀번호 잠금(아이디 chiwaa) / 풀기 — 공개 전 나만 보기
|
|
@@ -1153,7 +1287,7 @@ async function main() {
|
|
|
1153
1287
|
const cmd = args._[0];
|
|
1154
1288
|
if (args.version) return console.log(VERSION);
|
|
1155
1289
|
if (args.help || !cmd || cmd === "help") return console.log(HELP);
|
|
1156
|
-
const cmds = { deploy, login, list, ls: list, versions, rollback, db, files, domains, domain: domains, stats, env, logs, lock, unlock, open, usage, agent, whoami, logout };
|
|
1290
|
+
const cmds = { deploy, login, list, ls: list, versions, rollback, db, files, domains, domain: domains, stats, env, logs, lock, unlock, open, usage, agent, whoami, logout, pg };
|
|
1157
1291
|
if (!cmds[cmd]) fail(`모르는 명령입니다: ${cmd}\n\n${HELP}`);
|
|
1158
1292
|
if (cmd !== "login" && cmd !== "logout") await resumePendingLogin();
|
|
1159
1293
|
await cmds[cmd](args);
|