@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,490 @@
1
+ // src/authServer.js
2
+ const http = require("http");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const crypto = require("crypto");
6
+ const { spawn } = require("child_process");
7
+ const mysql = require("mysql2/promise");
8
+ const { executeSqlFileWithDelimiters } = require("./sqlRunner.js");
9
+
10
+ // node-redis (install in installer package): npm i redis
11
+ const { createClient } = require("redis");
12
+
13
+ function loadTemplate(fileName, replacements = {}) {
14
+ const templatePath = path.join(__dirname, "..", "templates", fileName);
15
+ let html = fs.readFileSync(templatePath, "utf8");
16
+
17
+ for (const [k, v] of Object.entries(replacements)) {
18
+ html = html.replaceAll(k, String(v));
19
+ }
20
+ return html;
21
+ }
22
+
23
+ function runCommand(cmd, args, { cwd, statusCb }) {
24
+ return new Promise((resolve, reject) => {
25
+ statusCb?.(`Running: ${cmd} ${args.join(" ")}`);
26
+
27
+ const child = spawn(cmd, args, {
28
+ cwd,
29
+ stdio: "inherit",
30
+ shell: process.platform === "win32"
31
+ });
32
+
33
+ child.on("close", (code) => {
34
+ if (code === 0) resolve();
35
+ else reject(new Error(`${cmd} ${args.join(" ")} exited with code ${code}`));
36
+ });
37
+ });
38
+ }
39
+
40
+ function readSchemaSql(installDir) {
41
+ // You can change this default anytime.
42
+ // Put schema at: <installedProject>/db/schema.sql OR <installedProject>/schema.sql
43
+ const candidates = [
44
+ path.join(installDir, "DB Backup", "WoniruCRMMeta.sql"),
45
+ path.join(installDir, "schema.sql")
46
+ ];
47
+
48
+ for (const p of candidates) {
49
+ if (fs.existsSync(p)) return { filePath: p, sql: fs.readFileSync(p, "utf8") };
50
+ }
51
+
52
+ throw new Error("Schema file not found. Expected db/schema.sql or schema.sql in install directory.");
53
+ }
54
+
55
+ async function readJsonBody(req) {
56
+ return new Promise((resolve, reject) => {
57
+ let body = "";
58
+ req.on("data", (chunk) => {
59
+ body += chunk;
60
+ if (body.length > 200_000) reject(new Error("Request body too large"));
61
+ });
62
+ req.on("end", () => {
63
+ try {
64
+ resolve(JSON.parse(body || "{}"));
65
+ } catch {
66
+ reject(new Error("Bad JSON"));
67
+ }
68
+ });
69
+ });
70
+ }
71
+
72
+ function randomKey() {
73
+ // strong, URL-safe-ish key
74
+ return crypto.randomBytes(32).toString("base64url");
75
+ }
76
+
77
+ async function withRedisClient({ host, port, username, password }, fn) {
78
+ const url = `redis://${(password && username != 'default') ? encodeURIComponent(username) + ":" + encodeURIComponent(password) + "@" : ""}${host}:${port}`;
79
+ const client = createClient({ url });
80
+ client.on("error", () => { /* handled by connect/commands */ });
81
+
82
+ await client.connect();
83
+ try {
84
+ return await fn(client);
85
+ } finally {
86
+ try { await client.quit(); } catch { }
87
+ }
88
+ }
89
+
90
+ function startAuthServer({ verificationUri, userCode }) {
91
+ const state = {
92
+ // Progress (auth page uses /status to drive phase + redirect)
93
+ phase: "auth", // auth | install | download | npm | configure
94
+ status: "Waiting for approval in GitHub…",
95
+ userLogin: null,
96
+
97
+ // Install selection
98
+ cwd: process.cwd(),
99
+ installChoice: null, // accept | manual | null
100
+
101
+ // Installer runtime context
102
+ installDir: null, // set by CLI once directory is decided
103
+
104
+ // Configure wizard data
105
+ configure: {
106
+ step: 1,
107
+ redis: {
108
+ existing: false,
109
+ adminKey: "",
110
+ // will be generated only when existing=false and setup runs
111
+ sessUserKey: "",
112
+ permUserKey: "",
113
+ aclFilePath: ""
114
+ },
115
+ db: {
116
+ host: "",
117
+ user: "",
118
+ password: "",
119
+ name: ""
120
+ }
121
+ }
122
+ };
123
+
124
+ const server = http.createServer(async (req, res) => {
125
+ try {
126
+ // -------------------------
127
+ // Status endpoint
128
+ // -------------------------
129
+ if (req.url === "/status") {
130
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
131
+ res.end(JSON.stringify({
132
+ phase: state.phase,
133
+ status: state.status,
134
+ userLogin: state.userLogin
135
+ }));
136
+ return;
137
+ }
138
+
139
+ // -------------------------
140
+ // Context endpoint
141
+ // -------------------------
142
+ if (req.url === "/context") {
143
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
144
+ res.end(JSON.stringify({
145
+ cwd: state.cwd,
146
+ installChoice: state.installChoice
147
+ }));
148
+ return;
149
+ }
150
+
151
+ // -------------------------
152
+ // Install choice (accept/manual)
153
+ // -------------------------
154
+ if (req.url === "/install-choice" && req.method === "POST") {
155
+ const data = await readJsonBody(req);
156
+ const choice = data.choice;
157
+
158
+ if (choice !== "accept" && choice !== "manual") {
159
+ res.writeHead(400, { "Content-Type": "application/json" });
160
+ res.end(JSON.stringify({ ok: false, error: "Invalid choice" }));
161
+ return;
162
+ }
163
+
164
+ state.installChoice = choice;
165
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
166
+ res.end(JSON.stringify({ ok: true }));
167
+ return;
168
+ }
169
+
170
+ // -------------------------
171
+ // Configure: get state (Step 1/2 rendering)
172
+ // -------------------------
173
+ if (req.url === "/configure/state") {
174
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
175
+ res.end(JSON.stringify({
176
+ step: state.configure.step,
177
+ installDir: state.installDir,
178
+ redis: {
179
+ existing: state.configure.redis.existing,
180
+ adminKey: state.configure.redis.adminKey,
181
+ aclFilePath: state.configure.redis.aclFilePath
182
+ },
183
+ db: state.configure.db
184
+ }));
185
+ return;
186
+ }
187
+
188
+ // -------------------------
189
+ // Configure Step 1: apply Redis choice
190
+ //
191
+ // Rules:
192
+ // - Always collect adminKey.
193
+ // - If existing=true:
194
+ // - do NOT create users
195
+ // - just test connection as admin@127.0.0.1:6379 (PING)
196
+ // - If existing=false:
197
+ // - create users (admin/sess_user/perm_user + default off)
198
+ // - set aclfile + ACL SAVE
199
+ // - On success -> advance to step 2
200
+ // -------------------------
201
+ if (req.url === "/configure/redis/apply" && req.method === "POST") {
202
+ const data = await readJsonBody(req);
203
+ const existing = !!data.existing;
204
+ const adminKey = (data.adminKey || "").toString();
205
+
206
+ if (!adminKey || adminKey.trim().length < 8) {
207
+ res.writeHead(400, { "Content-Type": "application/json" });
208
+ res.end(JSON.stringify({ ok: false, error: "Redis admin key must be at least 8 characters." }));
209
+ return;
210
+ }
211
+
212
+ // persist what user entered
213
+ state.configure.redis.existing = existing;
214
+ state.configure.redis.adminKey = adminKey;
215
+
216
+ const host = "127.0.0.1";
217
+ const port = 6379;
218
+ const username = "default";
219
+
220
+ // Attempt connection as admin
221
+ state.status = existing
222
+ ? "Testing Redis connection (admin@127.0.0.1:6379)…"
223
+ : "Connecting to Redis to configure ACL users…";
224
+
225
+ try {
226
+ if (existing) {
227
+ await withRedisClient({ host, port, username: 'admin', password: adminKey }, async (client) => {
228
+ const pong = await client.ping();
229
+ if (!pong) throw new Error("No PING response");
230
+ });
231
+
232
+ state.configure.step = 2; // proceed
233
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
234
+ res.end(JSON.stringify({ ok: true, nextStep: 2 }));
235
+ return;
236
+ }
237
+
238
+ // Not existing: we configure ACL users + save aclfile
239
+ // Ensure we have an installDir to write the ACL file into
240
+ if (!state.installDir) {
241
+ throw new Error("Installer installDir not set yet. (Internal error: missing install directory)");
242
+ }
243
+
244
+ /* const aclDir = path.join(state.installDir, "config");
245
+ fs.mkdirSync(aclDir, { recursive: true });
246
+
247
+ const aclFilePath = path.join(aclDir, "redis-users.acl");
248
+ state.configure.redis.aclFilePath = aclFilePath; */
249
+
250
+ state.status = "Building Redis docker image (no-cache)…";
251
+ await runCommand("docker", ["compose", "build", "--no-cache"], {
252
+ cwd: state.installDir,
253
+ statusCb: (s) => (state.status = s)
254
+ });
255
+
256
+ state.status = "Starting Redis container…";
257
+ await runCommand("docker", ["compose", "up", "-d"], {
258
+ cwd: state.installDir,
259
+ statusCb: (s) => (state.status = s)
260
+ });
261
+
262
+ // Optional: small delay before first connect (helps on slower machines)
263
+ await new Promise(r => setTimeout(r, 1200));
264
+
265
+ const sessKey = randomKey();
266
+ const permKey = randomKey();
267
+
268
+ await withRedisClient({ host, port, username, password: adminKey }, async (client) => {
269
+ // Create/modify users as per your rules
270
+ // admin user
271
+ await client.sendCommand(["ACL", "SETUSER", "admin", "ON", `>${adminKey}`, "+@all"]);
272
+
273
+ // session user
274
+ await client.sendCommand([
275
+ "ACL", "SETUSER", "sess_user",
276
+ "ON", `>${sessKey}`,
277
+ "~sess:*",
278
+ "+ping", "+get", "+set", "+del",
279
+ "+expire", "+pexpire", "+ttl", "+pttl", "+exists"
280
+ ]);
281
+
282
+ // permission user
283
+ await client.sendCommand([
284
+ "ACL", "SETUSER", "perm_user",
285
+ "ON", `>${permKey}`,
286
+ "~userPerm:*",
287
+ "+ping", "+get", "+set", "+del",
288
+ "+expire", "+pexpire", "+ttl", "+pttl", "+exists",
289
+ "+hget", "+hset", "+hdel", "+hmget", "+hgetall",
290
+ "+sadd", "+srem", "+smembers", "+scard", "+scan",
291
+ "+hscan", "+sscan"
292
+ ]);
293
+
294
+ // Turn off default user
295
+ await client.sendCommand(["ACL", "SETUSER", "default", "OFF"]);
296
+
297
+ // Point Redis at our aclfile, then save
298
+ // await client.sendCommand(["CONFIG", "SET", "aclfile", aclFilePath]);
299
+ await client.sendCommand(["ACL", "SAVE"]);
300
+ });
301
+
302
+ // store generated keys in state (we’ll persist later to real secrets storage)
303
+ state.configure.redis.sessUserKey = sessKey;
304
+ state.configure.redis.permUserKey = permKey;
305
+
306
+ state.configure.step = 2; // proceed
307
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
308
+ res.end(JSON.stringify({ ok: true, nextStep: 2 }));
309
+ return;
310
+
311
+ } catch (err) {
312
+ res.writeHead(400, { "Content-Type": "application/json", "Cache-Control": "no-store" });
313
+ res.end(JSON.stringify({ ok: false, error: err.message || "Redis step failed" }));
314
+ return;
315
+ }
316
+ }
317
+
318
+ // -------------------------
319
+ // Configure Step 2: apply DB (MySQL)
320
+ // - Save DB inputs
321
+ // - Test connection
322
+ // - Create DB if missing (if permitted)
323
+ // - Apply schema.sql using mysql2 (remote-safe)
324
+ // - On success -> advance to step 3
325
+ // -------------------------
326
+ if (req.url === "/configure/db/apply" && req.method === "POST") {
327
+ const data = await readJsonBody(req);
328
+
329
+ const host = (data.host || "").toString().trim();
330
+ const user = (data.user || "").toString().trim();
331
+ const password = (data.password || "").toString();
332
+ const dbName = (data.name || "").toString().trim();
333
+
334
+ if (!host) {
335
+ res.writeHead(400, { "Content-Type": "application/json" });
336
+ res.end(JSON.stringify({ ok: false, error: "DB host is required." }));
337
+ return;
338
+ }
339
+ if (!user) {
340
+ res.writeHead(400, { "Content-Type": "application/json" });
341
+ res.end(JSON.stringify({ ok: false, error: "DB user is required." }));
342
+ return;
343
+ }
344
+ if (!dbName) {
345
+ res.writeHead(400, { "Content-Type": "application/json" });
346
+ res.end(JSON.stringify({ ok: false, error: "DB name is required." }));
347
+ return;
348
+ }
349
+ if (!state.installDir) {
350
+ res.writeHead(400, { "Content-Type": "application/json" });
351
+ res.end(JSON.stringify({ ok: false, error: "Install directory not set (internal error)." }));
352
+ return;
353
+ }
354
+
355
+ // persist
356
+ state.configure.db.host = host;
357
+ state.configure.db.user = user;
358
+ state.configure.db.password = password;
359
+ state.configure.db.name = dbName;
360
+
361
+ try {
362
+ state.status = "Testing database connection…";
363
+
364
+ // 1) connect without selecting database (so we can CREATE DATABASE if needed)
365
+ const adminConn = await mysql.createConnection({
366
+ host,
367
+ user,
368
+ password,
369
+ // NOTE: no database here on purpose
370
+ // Enable multi statements only when we apply schema below
371
+ multipleStatements: false
372
+ });
373
+
374
+ // 2) ensure database exists (best-effort)
375
+ state.status = `Ensuring database exists: ${dbName}…`;
376
+ await adminConn.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``);
377
+ await adminConn.end();
378
+
379
+ state.status = "Reading schema file…";
380
+ const { filePath, sql } = readSchemaSql(state.installDir);
381
+
382
+ state.status = "Applying database schema…";
383
+
384
+ const dbConn = await mysql.createConnection({
385
+ host,
386
+ user,
387
+ password,
388
+ database: dbName,
389
+ multipleStatements: false // IMPORTANT: we execute statements one-by-one
390
+ });
391
+
392
+ await executeSqlFileWithDelimiters(dbConn, sql, {
393
+ onProgress: () => {
394
+ // keep this light; don’t spam huge SQL into status
395
+ state.status = "Applying database schema…";
396
+ }
397
+ });
398
+
399
+ await dbConn.end();
400
+
401
+ // 5) advance to step 3
402
+ state.configure.step = 3;
403
+ state.status = `Database ready ✅ (${dbName})`;
404
+
405
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
406
+ res.end(JSON.stringify({ ok: true, nextStep: 3, schemaFile: filePath }));
407
+ return;
408
+
409
+ } catch (err) {
410
+ res.writeHead(400, { "Content-Type": "application/json", "Cache-Control": "no-store" });
411
+ res.end(JSON.stringify({
412
+ ok: false,
413
+ error: err.message || "Database step failed"
414
+ }));
415
+ return;
416
+ }
417
+ }
418
+
419
+ // -------------------------
420
+ // Configure: done/exit signal
421
+ // - Browser calls this on Finish and on unload
422
+ // - CLI can end after server closes
423
+ // -------------------------
424
+ if (req.url === "/configure/done" && (req.method === "POST" || req.method === "GET")) {
425
+ state.status = "Installer finished. Closing…";
426
+
427
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
428
+ res.end(JSON.stringify({ ok: true }));
429
+
430
+ // Close the HTTP server right after responding
431
+ setTimeout(() => {
432
+ try { server.close(); } catch { }
433
+ }, 1000);
434
+
435
+ return;
436
+ }
437
+
438
+ // -------------------------
439
+ // Pages
440
+ // -------------------------
441
+ if (req.url === "/" || req.url.startsWith("/?")) {
442
+ const html = loadTemplate("auth.html", {
443
+ "{{USER_CODE}}": userCode,
444
+ "{{VERIFICATION_URI}}": verificationUri
445
+ });
446
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
447
+ res.end(html);
448
+ return;
449
+ }
450
+
451
+ if (req.url === "/configure" || req.url.startsWith("/configure?")) {
452
+ const html = loadTemplate("configure.html", {});
453
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
454
+ res.end(html);
455
+ return;
456
+ }
457
+
458
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
459
+ res.end("Not Found");
460
+ } catch (err) {
461
+ res.writeHead(500, { "Content-Type": "application/json" });
462
+ res.end(JSON.stringify({ ok: false, error: err.message || "Server error" }));
463
+ }
464
+ });
465
+
466
+ return new Promise((resolve, reject) => {
467
+ server.on("error", reject);
468
+ server.listen(0, "127.0.0.1", () => {
469
+ const port = server.address().port;
470
+
471
+ resolve({
472
+ url: `http://127.0.0.1:${port}/`,
473
+
474
+ // setters used by CLI
475
+ setStatus: (s) => { state.status = String(s); },
476
+ setPhase: (p) => { state.phase = String(p); },
477
+ setUserLogin: (login) => { state.userLogin = login ? String(login) : null; },
478
+ setInstallDir: (dir) => { state.installDir = dir ? String(dir) : null; },
479
+
480
+ // getters used by CLI
481
+ getInstallChoice: () => state.installChoice,
482
+ getCwd: () => state.cwd,
483
+
484
+ close: () => new Promise((r) => server.close(() => r()))
485
+ });
486
+ });
487
+ });
488
+ }
489
+
490
+ module.exports = { startAuthServer };
@@ -0,0 +1,123 @@
1
+ // src/download.js
2
+ const os = require("os");
3
+ const path = require("path");
4
+ const fs = require("fs");
5
+ const { spawn } = require("child_process");
6
+
7
+ const { ask } = require("./prompt");
8
+ const { downloadRepoZip } = require("./githubDownload");
9
+ const { extractZipStripTopDir } = require("./extractZip");
10
+
11
+ function parseRepo(repoStr) {
12
+ const [owner, repo] = String(repoStr || "").split("/");
13
+ if (!owner || !repo) throw new Error("WE_GITHUB_REPO must be like: Owner/Repo");
14
+ return { owner, repo };
15
+ }
16
+
17
+ function dirExists(p) {
18
+ try { return fs.statSync(p).isDirectory(); }
19
+ catch { return false; }
20
+ }
21
+
22
+ function isDirEmpty(p) {
23
+ const entries = fs.readdirSync(p);
24
+ return entries.length === 0;
25
+ }
26
+
27
+ async function waitForInstallChoice(ui) {
28
+ ui?.setPhase("install");
29
+ ui?.setStatus("Choose install location in the browser: Accept (use current directory) or Choose manually…");
30
+
31
+ while (true) {
32
+ const choice = ui?.getInstallChoice?.();
33
+ if (choice === "accept" || choice === "manual") return choice;
34
+ await new Promise(r => setTimeout(r, 400));
35
+ }
36
+ }
37
+
38
+ async function runNpmInstall(targetDir, ui) {
39
+ return new Promise((resolve, reject) => {
40
+ ui?.setPhase("npm");
41
+ ui?.setStatus("Installing dependencies (npm install)…");
42
+
43
+ console.log("\nRunning npm install...\n");
44
+
45
+ const child = spawn("npm", ["install"], {
46
+ cwd: targetDir,
47
+ stdio: "inherit",
48
+ shell: process.platform === "win32"
49
+ });
50
+
51
+ child.on("close", (code) => {
52
+ if (code === 0) {
53
+ ui?.setStatus("Dependencies installed successfully ✅");
54
+ resolve();
55
+ } else {
56
+ ui?.setStatus("npm install failed ❌");
57
+ reject(new Error(`npm install exited with code ${code}`));
58
+ }
59
+ });
60
+ });
61
+ }
62
+
63
+ async function runDownload({ token, ui }) {
64
+ const repoStr = 'yathanshsharma/woniru-crm';
65
+ const ref = "main";
66
+ const { owner, repo } = parseRepo(repoStr);
67
+
68
+ const choice = await waitForInstallChoice(ui);
69
+
70
+ const defaultDir = path.join(process.cwd(), "woniru-engine");
71
+ let targetDir;
72
+
73
+ if (choice === "accept") {
74
+ targetDir = defaultDir;
75
+ console.log(`Using default install directory: ${targetDir}`);
76
+ } else {
77
+ console.log("Manual selection chosen in browser.");
78
+ const answer = await ask(`Install location (must be empty) [default: ${defaultDir}]: `);
79
+ targetDir = (answer && answer.trim()) ? path.resolve(answer.trim()) : defaultDir;
80
+ }
81
+
82
+ // enforce empty or non-existent directory
83
+ if (dirExists(targetDir)) {
84
+ if (!isDirEmpty(targetDir)) {
85
+ ui?.setStatus(`Failed ❌ Install directory not empty: ${targetDir}`);
86
+ throw new Error(`Install directory must be empty: ${targetDir}`);
87
+ }
88
+ } else {
89
+ fs.mkdirSync(targetDir, { recursive: true });
90
+ }
91
+
92
+ const zipPath = path.join(os.tmpdir(), `we-installer-${repo}-${Date.now()}.zip`);
93
+
94
+ ui?.setPhase("download");
95
+ ui?.setStatus(`Downloading ${owner}/${repo}@${ref}…`);
96
+ console.log(`Downloading ${owner}/${repo}@${ref}...`);
97
+
98
+ await downloadRepoZip({ token, owner, repo, ref, outFile: zipPath });
99
+
100
+ ui?.setStatus(`Extracting into: ${targetDir}…`);
101
+ console.log(`Extracting to: ${targetDir}...`);
102
+
103
+ extractZipStripTopDir(zipPath, targetDir);
104
+
105
+ ui?.setStatus("Download complete ✅");
106
+
107
+ // ---------------------
108
+ // NEW: npm stage
109
+ // ---------------------
110
+ await runNpmInstall(targetDir, ui);
111
+ ui.setInstallDir(targetDir);
112
+
113
+ // Move to wizard phase
114
+ ui?.setPhase("configure");
115
+ ui?.setStatus("Installation complete. Opening configuration wizard…");
116
+
117
+ console.log("\n🎉 Installation complete.");
118
+ console.log(`Path: ${targetDir}`);
119
+
120
+ return { targetDir };
121
+ }
122
+
123
+ module.exports = { runDownload };
@@ -0,0 +1,32 @@
1
+ // src/extractZip.js
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const AdmZip = require("adm-zip");
5
+
6
+ function ensureDir(dir) {
7
+ fs.mkdirSync(dir, { recursive: true });
8
+ }
9
+
10
+ function extractZipStripTopDir(zipFile, destDir) {
11
+ ensureDir(destDir);
12
+
13
+ const zip = new AdmZip(zipFile);
14
+ const entries = zip.getEntries();
15
+
16
+ // GitHub zipball structure: <repo>-<hash>/<files...>
17
+ // We strip the first folder segment.
18
+ for (const e of entries) {
19
+ if (e.isDirectory) continue;
20
+
21
+ const parts = e.entryName.split("/").filter(Boolean);
22
+ if (parts.length < 2) continue;
23
+
24
+ const relativePath = parts.slice(1).join("/"); // drop root folder
25
+ const outPath = path.join(destDir, relativePath);
26
+
27
+ ensureDir(path.dirname(outPath));
28
+ fs.writeFileSync(outPath, e.getData());
29
+ }
30
+ }
31
+
32
+ module.exports = { extractZipStripTopDir };
@@ -0,0 +1,22 @@
1
+ // src/githubApi.js
2
+ async function getGithubUser({ token }) {
3
+ const res = await fetch("https://api.github.com/user", {
4
+ method: "GET",
5
+ headers: {
6
+ "Accept": "application/vnd.github+json",
7
+ "Authorization": `Bearer ${token}`,
8
+ "X-GitHub-Api-Version": "2022-11-28",
9
+ "User-Agent": "we-npx-installer"
10
+ }
11
+ });
12
+
13
+ const data = await res.json().catch(() => ({}));
14
+
15
+ if (!res.ok) {
16
+ throw new Error(`Failed to fetch GitHub user (HTTP ${res.status}): ${JSON.stringify(data)}`);
17
+ }
18
+
19
+ return data; // contains login, id, name, etc
20
+ }
21
+
22
+ module.exports = { getGithubUser };