@woniru/we-installer 0.1.0

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.
@@ -0,0 +1,82 @@
1
+ // src/githubDeviceFlow.js
2
+
3
+ function formUrlEncode(obj) {
4
+ return new URLSearchParams(obj).toString();
5
+ }
6
+
7
+ async function postForm(url, data) {
8
+ const res = await fetch(url, {
9
+ method: "POST",
10
+ headers: {
11
+ "Content-Type": "application/x-www-form-urlencoded",
12
+ "Accept": "application/json"
13
+ },
14
+ body: formUrlEncode(data)
15
+ });
16
+
17
+ const json = await res.json().catch(() => ({}));
18
+ if (!res.ok) {
19
+ throw new Error(`POST ${url} failed (HTTP ${res.status}): ${JSON.stringify(json)}`);
20
+ }
21
+ return json;
22
+ }
23
+
24
+ async function requestDeviceCode({ clientId, scope = "repo" }) {
25
+ const data = await postForm("https://github.com/login/device/code", {
26
+ client_id: clientId,
27
+ scope
28
+ });
29
+
30
+ if (!data.device_code || !data.user_code || !data.verification_uri) {
31
+ throw new Error(`Unexpected device code response: ${JSON.stringify(data)}`);
32
+ }
33
+ return data;
34
+ }
35
+
36
+ async function pollForAccessToken({ clientId, deviceCode, intervalSeconds = 5, expiresInSeconds = 900 }) {
37
+ const start = Date.now();
38
+ let waitMs = Math.max(1, intervalSeconds) * 1000;
39
+
40
+ while (true) {
41
+ const elapsed = Date.now() - start;
42
+ if (elapsed > expiresInSeconds * 1000) {
43
+ throw new Error("Device code expired. Run auth again.");
44
+ }
45
+
46
+ const tokenResp = await postForm("https://github.com/login/oauth/access_token", {
47
+ client_id: clientId,
48
+ device_code: deviceCode,
49
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
50
+ });
51
+
52
+ if (tokenResp.access_token) {
53
+ return tokenResp.access_token;
54
+ }
55
+
56
+ // Expected errors: authorization_pending, slow_down, access_denied, expired_token
57
+ const err = tokenResp.error;
58
+
59
+ if (err === "authorization_pending") {
60
+ await new Promise(r => setTimeout(r, waitMs));
61
+ continue;
62
+ }
63
+
64
+ if (err === "slow_down") {
65
+ waitMs += 5000;
66
+ await new Promise(r => setTimeout(r, waitMs));
67
+ continue;
68
+ }
69
+
70
+ if (err === "access_denied") {
71
+ throw new Error("Authorization denied by user in browser.");
72
+ }
73
+
74
+ if (err === "expired_token") {
75
+ throw new Error("Device code expired. Run auth again.");
76
+ }
77
+
78
+ throw new Error(`Unexpected token response: ${JSON.stringify(tokenResp)}`);
79
+ }
80
+ }
81
+
82
+ module.exports = { requestDeviceCode, pollForAccessToken };
@@ -0,0 +1,33 @@
1
+ // src/githubDownload.js
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+
5
+ async function downloadRepoZip({ token, owner, repo, ref, outFile }) {
6
+ const url = `https://api.github.com/repos/${owner}/${repo}/zipball/${encodeURIComponent(ref)}`;
7
+
8
+ const res = await fetch(url, {
9
+ method: "GET",
10
+ headers: {
11
+ "Accept": "application/vnd.github+json",
12
+ "Authorization": `Bearer ${token}`,
13
+ "X-GitHub-Api-Version": "2022-11-28",
14
+ "User-Agent": "we-installer"
15
+ },
16
+ redirect: "follow"
17
+ });
18
+
19
+ if (!res.ok) {
20
+ const body = await res.text().catch(() => "");
21
+ throw new Error(
22
+ `Repo download failed (HTTP ${res.status}). ` +
23
+ `Are you a collaborator on ${owner}/${repo}? ` +
24
+ `Response: ${body.slice(0, 200)}`
25
+ );
26
+ }
27
+
28
+ const buf = Buffer.from(await res.arrayBuffer());
29
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
30
+ fs.writeFileSync(outFile, buf);
31
+ }
32
+
33
+ module.exports = { downloadRepoZip };
package/src/prompt.js ADDED
@@ -0,0 +1,14 @@
1
+ // src/prompt.js
2
+ const readline = require("readline");
3
+
4
+ function ask(question) {
5
+ return new Promise((resolve) => {
6
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
7
+ rl.question(question, (answer) => {
8
+ rl.close();
9
+ resolve(answer);
10
+ });
11
+ });
12
+ }
13
+
14
+ module.exports = { ask };
@@ -0,0 +1,165 @@
1
+ async function executeSqlFileWithDelimiters(conn, sqlText, { onProgress } = {}) {
2
+ // Normalize newlines + strip BOM
3
+ let sql = String(sqlText || "").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
4
+
5
+ let delimiter = ";";
6
+ let buf = "";
7
+
8
+ // State flags
9
+ let inSingle = false;
10
+ let inDouble = false;
11
+ let inBacktick = false;
12
+ let inLineComment = false;
13
+ let inBlockComment = false;
14
+
15
+ function isEscaped(text, idx) {
16
+ // count consecutive backslashes immediately before idx
17
+ let n = 0;
18
+ for (let i = idx - 1; i >= 0 && text[i] === "\\"; i--) n++;
19
+ return (n % 2) === 1;
20
+ }
21
+
22
+ async function flushStatement(stmt) {
23
+ const s = stmt.trim();
24
+ if (!s) return;
25
+
26
+ // Skip pure delimiter directives if they slipped through
27
+ if (/^\s*DELIMITER\s+/i.test(s)) return;
28
+
29
+ onProgress?.(s);
30
+
31
+ // Execute statement as-is
32
+ await conn.query(s);
33
+ }
34
+
35
+ // Handle DELIMITER directives line-by-line, but still allow delimiter detection char-by-char
36
+ // We'll scan char-by-char and also watch for "DELIMITER xyz" at start of a line (not inside strings/comments).
37
+ let lineStart = 0; // index after last '\n'
38
+
39
+ for (let i = 0; i < sql.length; i++) {
40
+ const ch = sql[i];
41
+ const next = sql[i + 1];
42
+
43
+ // Track line starts
44
+ if (i === lineStart) {
45
+ // if not inside strings/comments, check DELIMITER directive
46
+ if (!inSingle && !inDouble && !inBacktick && !inLineComment && !inBlockComment) {
47
+ // capture current line
48
+ const lineEnd = sql.indexOf("\n", i);
49
+ const endIdx = lineEnd === -1 ? sql.length : lineEnd;
50
+ const line = sql.slice(i, endIdx);
51
+
52
+ const m = line.match(/^\s*DELIMITER\s+(.+?)\s*$/i);
53
+ if (m) {
54
+ // Flush anything pending before changing delimiter (should usually be empty)
55
+ if (buf.trim()) {
56
+ await flushStatement(buf);
57
+ buf = "";
58
+ }
59
+ delimiter = m[1];
60
+ // Skip the directive line entirely
61
+ i = endIdx; // loop will i++ then continue
62
+ lineStart = i + 1;
63
+ continue;
64
+ }
65
+ }
66
+ }
67
+
68
+ // Handle end of line comment
69
+ if (inLineComment) {
70
+ buf += ch;
71
+ if (ch === "\n") {
72
+ inLineComment = false;
73
+ lineStart = i + 1;
74
+ }
75
+ continue;
76
+ }
77
+
78
+ // Handle block comment end
79
+ if (inBlockComment) {
80
+ buf += ch;
81
+ if (ch === "*" && next === "/") {
82
+ buf += "/";
83
+ i++;
84
+ inBlockComment = false;
85
+ }
86
+ continue;
87
+ }
88
+
89
+ // Enter comments (only if not inside strings/backticks)
90
+ if (!inSingle && !inDouble && !inBacktick) {
91
+ // -- comment (MySQL treats '-- ' as comment; we’ll accept '--' when followed by space/tab/newline/end)
92
+ if (ch === "-" && next === "-") {
93
+ const after = sql[i + 2];
94
+ if (after === " " || after === "\t" || after === "\n" || after === "\r" || after === undefined) {
95
+ buf += ch + next;
96
+ i++;
97
+ inLineComment = true;
98
+ continue;
99
+ }
100
+ }
101
+ // # comment
102
+ if (ch === "#") {
103
+ buf += ch;
104
+ inLineComment = true;
105
+ continue;
106
+ }
107
+ // /* block comment */
108
+ if (ch === "/" && next === "*") {
109
+ buf += ch + next;
110
+ i++;
111
+ inBlockComment = true;
112
+ continue;
113
+ }
114
+ }
115
+
116
+ // Toggle string/backtick states
117
+ if (!inDouble && !inBacktick && ch === "'" && !isEscaped(sql, i)) {
118
+ inSingle = !inSingle;
119
+ buf += ch;
120
+ continue;
121
+ }
122
+ if (!inSingle && !inBacktick && ch === `"` && !isEscaped(sql, i)) {
123
+ inDouble = !inDouble;
124
+ buf += ch;
125
+ continue;
126
+ }
127
+ if (!inSingle && !inDouble && ch === "`") {
128
+ inBacktick = !inBacktick;
129
+ buf += ch;
130
+ continue;
131
+ }
132
+
133
+ // Check delimiter (only when not inside strings/comments/backticks)
134
+ if (!inSingle && !inDouble && !inBacktick && delimiter) {
135
+ // Multi-char delimiter support (e.g. $$, //)
136
+ if (delimiter.length === 1) {
137
+ if (ch === delimiter) {
138
+ // Statement ends BEFORE delimiter
139
+ await flushStatement(buf);
140
+ buf = "";
141
+ continue; // do not include delimiter char
142
+ }
143
+ } else {
144
+ if (sql.startsWith(delimiter, i)) {
145
+ await flushStatement(buf);
146
+ buf = "";
147
+ i += (delimiter.length - 1);
148
+ continue; // do not include delimiter chars
149
+ }
150
+ }
151
+ }
152
+
153
+ // Normal char
154
+ buf += ch;
155
+
156
+ if (ch === "\n") lineStart = i + 1;
157
+ }
158
+
159
+ // Flush remainder
160
+ if (buf.trim()) {
161
+ await flushStatement(buf);
162
+ }
163
+ }
164
+
165
+ module.exports = { executeSqlFileWithDelimiters };
@@ -0,0 +1,377 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>WE Installer • GitHub Authorization</title>
8
+ <style>
9
+ body {
10
+ font-family: system-ui;
11
+ margin: 24px;
12
+ line-height: 1.4;
13
+ background-color: #F6F8FA;
14
+ }
15
+
16
+ .card {
17
+ background-color: white;
18
+ width: min(720px, calc(100vw - 48px));
19
+ border: 1px solid #ddd;
20
+ border-radius: 12px;
21
+ padding: 20px;
22
+ position: absolute;
23
+ top: 50%;
24
+ left: 50%;
25
+ transform: translate(-50%, -50%);
26
+ box-shadow: 0px 1px 1px 0px #1f23280f, 0px 1px 3px 0px #1f23280f;
27
+ }
28
+
29
+ h2 {
30
+ margin: 0 0 8px 0;
31
+ font-size: 22px;
32
+ }
33
+
34
+ p {
35
+ margin: 8px 0;
36
+ }
37
+
38
+ hr {
39
+ border: none;
40
+ border-top: 1px solid #eee;
41
+ margin: 16px 0;
42
+ }
43
+
44
+ .code {
45
+ font-size: 28px;
46
+ letter-spacing: 2px;
47
+ font-weight: 700;
48
+ padding: 12px 14px;
49
+ border: 1px dashed #888;
50
+ display: inline-block;
51
+ border-radius: 10px;
52
+ user-select: all;
53
+ background: #fff;
54
+ }
55
+
56
+ button,
57
+ a.btn {
58
+ display: inline-block;
59
+ margin-right: 10px;
60
+ margin-top: 12px;
61
+ padding: 10px 14px;
62
+ border-radius: 10px;
63
+ border: 1px solid #333;
64
+ background: #111;
65
+ color: #fff;
66
+ text-decoration: none;
67
+ cursor: pointer;
68
+ font-size: 16px;
69
+ line-height: 1.4;
70
+ }
71
+
72
+ button.secondary {
73
+ background: #fff;
74
+ color: #111;
75
+ }
76
+
77
+ button:disabled {
78
+ opacity: 0.6;
79
+ cursor: not-allowed;
80
+ }
81
+
82
+ .muted {
83
+ color: #666;
84
+ margin-top: 8px;
85
+ }
86
+
87
+ .status {
88
+ margin-top: 18px;
89
+ padding: 10px 12px;
90
+ border-radius: 10px;
91
+ background: #f6f6f6;
92
+ border: 1px solid #eee;
93
+ }
94
+
95
+ .section {
96
+ margin-top: 14px;
97
+ }
98
+
99
+ .badge {
100
+ display: inline-block;
101
+ padding: 2px 8px;
102
+ border-radius: 999px;
103
+ font-size: 12px;
104
+ border: 1px solid #e5e7eb;
105
+ background: #fafafa;
106
+ color: #444;
107
+ vertical-align: middle;
108
+ margin-left: 8px;
109
+ }
110
+
111
+ .path {
112
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
113
+ font-size: 13px;
114
+ background: #f8fafc;
115
+ border: 1px solid #e5e7eb;
116
+ border-radius: 10px;
117
+ padding: 10px 12px;
118
+ margin-top: 10px;
119
+ white-space: pre-wrap;
120
+ word-break: break-word;
121
+ }
122
+
123
+ .row {
124
+ display: flex;
125
+ gap: 10px;
126
+ flex-wrap: wrap;
127
+ align-items: center;
128
+ }
129
+ </style>
130
+ </head>
131
+
132
+ <body>
133
+ <div class="card">
134
+ <h2 id="title">WE Installer</h2>
135
+
136
+ <!-- AUTH SECTION -->
137
+ <div id="authSection" class="section">
138
+ <p>Open GitHub and enter this code:</p>
139
+ <div class="code" id="code">{{USER_CODE}}</div>
140
+
141
+ <div class="row">
142
+ <button id="copy" class="secondary" type="button">Copy code</button>
143
+ <a class="btn" id="githubBtn" href="{{VERIFICATION_URI}}" target="_blank" rel="noreferrer">Open GitHub</a>
144
+ </div>
145
+ </div>
146
+
147
+ <!-- INSTALL LOCATION SECTION -->
148
+ <div id="installSection" class="section" style="display:none;">
149
+ <p>
150
+ This package will be installed in the exact same directory where the command is running:
151
+ </p>
152
+ <div class="path" id="cwdLine">Loading current directory…</div>
153
+
154
+ <div class="muted" id="installChoiceMsg"></div>
155
+
156
+ <div class="row">
157
+ <button id="acceptCwdBtn" class="secondary" type="button">Accept</button>
158
+ <button id="chooseManualBtn" class="secondary" type="button">Choose manually</button>
159
+ </div>
160
+
161
+ <p class="muted">
162
+ If you choose <b>Choose manually</b>, for security reasons you must type the folder path in the terminal.
163
+ </p>
164
+ </div>
165
+
166
+ <!-- STATUS (ALWAYS VISIBLE) -->
167
+ <div class="status" id="status">Status: Waiting...</div>
168
+ <div class="muted" id="whoami"></div>
169
+ <div class="muted" id="keepOpenMsg">Do not close this browser window. This installer will continue using it.</div>
170
+ </div>
171
+
172
+ <script>
173
+ // -----------------------------
174
+ // Elements
175
+ // -----------------------------
176
+ const titleEl = document.getElementById("title");
177
+
178
+ const authSection = document.getElementById("authSection");
179
+ const installSection = document.getElementById("installSection");
180
+
181
+ const codeEl = document.getElementById("code");
182
+ const copyBtn = document.getElementById("copy");
183
+ const githubBtn = document.getElementById("githubBtn");
184
+
185
+ const statusEl = document.getElementById("status");
186
+ const whoamiEl = document.getElementById("whoami");
187
+ const keepOpenEl = document.getElementById("keepOpenMsg");
188
+
189
+ const cwdLineEl = document.getElementById("cwdLine");
190
+ const installChoiceMsgEl = document.getElementById("installChoiceMsg");
191
+ const acceptBtn = document.getElementById("acceptCwdBtn");
192
+ const manualBtn = document.getElementById("chooseManualBtn");
193
+
194
+ // -----------------------------
195
+ // Helpers
196
+ // -----------------------------
197
+ function show(el) {
198
+ if (el) el.style.display = "";
199
+ }
200
+
201
+ function hide(el) {
202
+ if (el) el.style.display = "none";
203
+ }
204
+
205
+ function setTitle(phase) {
206
+ if (!titleEl) return;
207
+ switch (phase) {
208
+ case "auth":
209
+ titleEl.innerText = "GitHub Authorization";
210
+ break;
211
+ case "install":
212
+ titleEl.innerText = "Choose Install Location";
213
+ break;
214
+ case "download":
215
+ titleEl.innerText = "Downloading WoniruEngine";
216
+ break;
217
+ case "npm":
218
+ titleEl.innerText = "Installing Dependencies";
219
+ break;
220
+ case "wizard":
221
+ titleEl.innerText = "Launching Setup Wizard";
222
+ break;
223
+ default:
224
+ titleEl.innerText = "WE Installer";
225
+ }
226
+ }
227
+
228
+ function applyVisibility(phase) {
229
+ // Always show status & keep-open message
230
+ show(statusEl);
231
+ show(keepOpenEl);
232
+ show(whoamiEl);
233
+
234
+ // Default: auth view
235
+ if (phase === "auth" || !phase) {
236
+ show(authSection);
237
+ hide(installSection);
238
+ return;
239
+ }
240
+
241
+ // Install choice view: show install section; hide auth section
242
+ hide(authSection);
243
+ show(installSection);
244
+
245
+ // Optional: once user has chosen, you can disable buttons (keeps UI clear)
246
+ // We'll handle in loadContext().
247
+ }
248
+
249
+ async function safeJsonFetch(url, opts) {
250
+ try {
251
+ const r = await fetch(url, Object.assign({
252
+ cache: "no-store"
253
+ }, opts || {}));
254
+ const j = await r.json();
255
+ return {
256
+ ok: true,
257
+ json: j
258
+ };
259
+ } catch (e) {
260
+ return {
261
+ ok: false,
262
+ error: e
263
+ };
264
+ }
265
+ }
266
+
267
+ // -----------------------------
268
+ // Copy Code
269
+ // -----------------------------
270
+ const initialCode = (codeEl && codeEl.innerText) ? codeEl.innerText : "";
271
+ if (copyBtn && initialCode) {
272
+ copyBtn.addEventListener("click", async () => {
273
+ try {
274
+ await navigator.clipboard.writeText(initialCode);
275
+ copyBtn.innerText = "Copied!";
276
+ setTimeout(() => copyBtn.innerText = "Copy code", 1500);
277
+ } catch {
278
+ alert("Clipboard blocked. Select code and press Ctrl+C.");
279
+ }
280
+ });
281
+ }
282
+
283
+ // -----------------------------
284
+ // Status polling
285
+ // Server returns: { phase, status, userLogin }
286
+ // phases: "auth" | "install" | "download" | "npm" | "wizard"
287
+ // -----------------------------
288
+ let lastPhase = null;
289
+
290
+ async function pollStatus() {
291
+ const res = await safeJsonFetch("/status");
292
+ if (res.ok) {
293
+ const j = res.json || {};
294
+
295
+ if (statusEl) statusEl.innerText = "Status: " + (j.status || "…");
296
+ if (keepOpenEl) keepOpenEl.innerText = "Do not close this browser window. This installer will continue using it.";
297
+
298
+ if (whoamiEl) {
299
+ if (j.userLogin) whoamiEl.innerText = "Authenticated as: " + j.userLogin;
300
+ else whoamiEl.innerText = "";
301
+ }
302
+
303
+ const phase = j.phase || "auth";
304
+ setTitle(phase);
305
+ applyVisibility(phase);
306
+
307
+ // Redirect when wizard is ready
308
+ if (phase && phase !== lastPhase) {
309
+ lastPhase = phase;
310
+ if (phase === "configure") {
311
+ window.location.href = "/configure?ts=" + Date.now();
312
+ return;
313
+ }
314
+ }
315
+ }
316
+
317
+ setTimeout(pollStatus, 1200);
318
+ }
319
+
320
+ // -----------------------------
321
+ // Context polling + install choice
322
+ // Server returns: { cwd, installChoice }
323
+ // installChoice: "accept" | "manual" | null
324
+ // -----------------------------
325
+ async function loadContext() {
326
+ const res = await safeJsonFetch("/context");
327
+ if (!res.ok) return;
328
+
329
+ const j = res.json || {};
330
+ const cwd = j.cwd || "";
331
+ const choice = j.installChoice || null;
332
+
333
+ if (cwdLineEl) cwdLineEl.innerText = cwd;
334
+
335
+ if (installChoiceMsgEl) {
336
+ if (choice === "accept") {
337
+ installChoiceMsgEl.innerText = "Accepted. Return to the terminal. Do not close this window.";
338
+ } else if (choice === "manual") {
339
+ installChoiceMsgEl.innerText = "Manual selection chosen. For security, enter the directory path in the terminal. Do not close this window.";
340
+ } else {
341
+ installChoiceMsgEl.innerText = "Click Accept to continue with this directory, or Choose manually to enter a path in the terminal.";
342
+ }
343
+ }
344
+
345
+ // Disable buttons once selection is made (prevents double-click confusion)
346
+ if (acceptBtn) acceptBtn.disabled = (choice !== null);
347
+ if (manualBtn) manualBtn.disabled = (choice !== null);
348
+ }
349
+
350
+ async function postChoice(choice) {
351
+ await safeJsonFetch("/install-choice", {
352
+ method: "POST",
353
+ headers: {
354
+ "Content-Type": "application/json"
355
+ },
356
+ body: JSON.stringify({
357
+ choice
358
+ })
359
+ });
360
+ await loadContext();
361
+ }
362
+
363
+ if (acceptBtn) acceptBtn.addEventListener("click", () => postChoice("accept"));
364
+ if (manualBtn) manualBtn.addEventListener("click", () => postChoice("manual"));
365
+
366
+ // Initial render
367
+ setTitle("auth");
368
+ applyVisibility("auth");
369
+
370
+ // Start loops
371
+ pollStatus();
372
+ loadContext();
373
+ setInterval(loadContext, 1200);
374
+ </script>
375
+ </body>
376
+
377
+ </html>