ngx-git 1.0.5 → 1.0.6

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/cli.js +1510 -80
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -1,5 +1,685 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // /**
4
+ // * NGX CLI v4.0
5
+ // * Pushes code directly to your GitHub repos via NGX server (OAuth proxy).
6
+ // * No git installation needed. No commit setup. No branch config.
7
+ // */
8
+
9
+ // const fs = require("fs");
10
+ // const path = require("path");
11
+ // const http = require("http");
12
+ // const os = require("os");
13
+ // const zlib = require("zlib");
14
+ // const { doOpen } = require("./ngx-open");
15
+ // const axios = require("axios");
16
+
17
+ // const _log = console.log;
18
+ // console.log = () => {};
19
+ // require("dotenv").config();
20
+ // console.log = _log;
21
+
22
+ // const VERSION = "1.0.5";
23
+ // const CONFIG_PATH = path.join(os.homedir(), ".ngxconfig");
24
+ // const CB_PORT = 9988; // local callback server port for OAuth
25
+
26
+ // const encoded = atob("aHR0cHM6Ly9uZ3gubmdpbnhzb2Z0Lm9ubGluZS9hcGk=");
27
+ // // console.log(encoded);
28
+ // // ─── Default ignore patterns ──────────────────────────────────────────────────
29
+ // const DEFAULT_IGNORE = new Set([
30
+ // ".env",
31
+ // "node_modules",
32
+ // ".git",
33
+ // ".DS_Store",
34
+ // "Thumbs.db",
35
+ // "dist",
36
+ // "build",
37
+ // ".cache",
38
+ // "coverage",
39
+ // ".nyc_output",
40
+ // "tmp",
41
+ // "temp",
42
+ // "__pycache__",
43
+ // ".pytest_cache",
44
+ // ".mypy_cache",
45
+ // "*.log",
46
+ // "*.sqlite",
47
+ // "*.db",
48
+ // "*.pem",
49
+ // "*.key",
50
+ // "*.p12",
51
+ // "*.pfx",
52
+ // "*.cer",
53
+ // "*.crt",
54
+ // "secrets",
55
+ // ".secrets",
56
+ // ]);
57
+
58
+ // // ─── Config ───────────────────────────────────────────────────────────────────
59
+ // function cfg() {
60
+ // try {
61
+ // return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
62
+ // } catch {
63
+ // return {};
64
+ // }
65
+ // }
66
+ // function saveCfg(d) {
67
+ // fs.writeFileSync(CONFIG_PATH, JSON.stringify(d, null, 2));
68
+ // }
69
+
70
+ // function getServer() {
71
+ // return cfg().server || process.env.NGX_SERVER || encoded;
72
+ // }
73
+ // function getToken() {
74
+ // return cfg().token || process.env.NGX_TOKEN || null;
75
+ // }
76
+ // function getUsername() {
77
+ // return cfg().username || null;
78
+ // }
79
+ // function authHdr() {
80
+ // const t = getToken();
81
+ // if (!t) fatal("Not logged in. Run: ngx login");
82
+ // return { Authorization: `Bearer ${t}`, "Content-Type": "application/json" };
83
+ // }
84
+
85
+ // // ─── Output ───────────────────────────────────────────────────────────────────
86
+ // const C = {
87
+ // cyan: (s) => `\x1b[96m${s}\x1b[0m`,
88
+ // green: (s) => `\x1b[92m${s}\x1b[0m`,
89
+ // grey: (s) => `\x1b[90m${s}\x1b[0m`,
90
+ // red: (s) => `\x1b[91m${s}\x1b[0m`,
91
+ // yel: (s) => `\x1b[93m${s}\x1b[0m`,
92
+ // bold: (s) => `\x1b[1m${s}\x1b[0m`,
93
+ // };
94
+ // function fatal(msg) {
95
+ // console.error(C.red("✗ ") + msg);
96
+ // process.exit(1);
97
+ // }
98
+ // function ok(msg) {
99
+ // console.log(C.green("✓ ") + msg);
100
+ // }
101
+ // function info(msg) {
102
+ // console.log(C.grey(" ") + msg);
103
+ // }
104
+ // function step(msg) {
105
+ // process.stdout.write(`\r${C.cyan("›")} ${msg} `);
106
+ // }
107
+
108
+ // // ─── Ignore logic ─────────────────────────────────────────────────────────────
109
+ // function loadIgnorePatterns(folderPath) {
110
+ // const pats = new Set(DEFAULT_IGNORE);
111
+ // // Read .ngxignore
112
+ // const ngxFile = path.join(folderPath, ".ngxignore");
113
+ // if (fs.existsSync(ngxFile))
114
+ // fs.readFileSync(ngxFile, "utf-8")
115
+ // .split("\n")
116
+ // .map((l) => l.trim())
117
+ // .filter((l) => l && !l.startsWith("#"))
118
+ // .forEach((p) => pats.add(p));
119
+ // // Read .gitignore
120
+ // const gitFile = path.join(folderPath, ".gitignore");
121
+ // if (fs.existsSync(gitFile))
122
+ // fs.readFileSync(gitFile, "utf-8")
123
+ // .split("\n")
124
+ // .map((l) => l.trim())
125
+ // .filter((l) => l && !l.startsWith("#"))
126
+ // .forEach((p) => pats.add(p));
127
+ // return pats;
128
+ // }
129
+
130
+ // function matchesIgnore(name, patterns) {
131
+ // for (const p of patterns) {
132
+ // if (p === name) return true;
133
+ // if (p.startsWith("*.") && name.endsWith(p.slice(1))) return true;
134
+ // if (p.endsWith("*") && name.startsWith(p.slice(0, -1))) return true;
135
+ // if (p.startsWith("*") && name.endsWith(p.slice(1))) return true;
136
+ // }
137
+ // return false;
138
+ // }
139
+
140
+ // // ─── Collect files from folder ────────────────────────────────────────────────
141
+ // // Returns array of { path: "relative/path", content_b64: "..." }
142
+ // function collectFiles(folderPath) {
143
+ // const ignore = loadIgnorePatterns(folderPath);
144
+ // const files = [];
145
+
146
+ // function walk(dir, rel) {
147
+ // const entries = fs.readdirSync(dir, { withFileTypes: true });
148
+ // for (const entry of entries) {
149
+ // if (matchesIgnore(entry.name, ignore)) continue;
150
+ // const fullPath = path.join(dir, entry.name);
151
+ // const relPath = rel ? `${rel}/${entry.name}` : entry.name;
152
+ // if (entry.isDirectory()) {
153
+ // walk(fullPath, relPath);
154
+ // } else if (entry.isFile()) {
155
+ // const buf = fs.readFileSync(fullPath);
156
+ // const compressed = zlib.gzipSync(buf);
157
+
158
+ // // Skip binary files > 50 MB (GitHub limit)
159
+ // if (buf.length > 50 * 1024 * 1024) {
160
+ // console.log(
161
+ // C.yel(
162
+ // ` ⚠ Skipping large file: ${relPath} (${(buf.length / 1024 / 1024).toFixed(1)} MB)`,
163
+ // ),
164
+ // );
165
+ // continue;
166
+ // }
167
+
168
+ // files.push({
169
+ // path: relPath,
170
+ // content_b64: compressed.toString("base64"),
171
+ // compressed: true,
172
+ // });
173
+ // }
174
+ // }
175
+ // }
176
+ // walk(folderPath, "");
177
+ // return files;
178
+ // }
179
+
180
+ // // ─── LOGIN — GitHub OAuth ─────────────────────────────────────────────────────
181
+ // async function doLogin() {
182
+ // const existing = getToken();
183
+ // if (existing) {
184
+ // ok(`Already logged in as ${C.bold(getUsername())}`);
185
+ // info("Run 'ngx logout' to switch accounts.");
186
+ // return;
187
+ // }
188
+
189
+ // console.log(`\n${C.bold("NGX GitHub Login")}`);
190
+
191
+ // const loginUrl = `${getServer()}/auth/github`;
192
+ // console.log(C.grey(`Opening: ${loginUrl}\n`));
193
+
194
+ // // Try to open browser
195
+ // try {
196
+ // const { default: open } = await import("open").catch(() => ({
197
+ // default: null,
198
+ // }));
199
+ // if (open) await open(loginUrl);
200
+ // else console.log(C.yel(`Open this in your browser:\n ${loginUrl}\n`));
201
+ // } catch {
202
+ // console.log(C.yel(`Open this in your browser:\n ${loginUrl}\n`));
203
+ // }
204
+
205
+ // // Spin local HTTP server to receive token
206
+ // return new Promise((resolve, reject) => {
207
+ // const srv = http.createServer((req, res) => {
208
+ // const CORS = {
209
+ // "Access-Control-Allow-Origin": "*",
210
+ // "Access-Control-Allow-Headers": "Content-Type",
211
+ // };
212
+ // if (req.method === "OPTIONS") {
213
+ // res.writeHead(204, CORS);
214
+ // res.end();
215
+ // return;
216
+ // }
217
+ // if (req.method === "POST" && req.url === "/token") {
218
+ // let body = "";
219
+ // req.on("data", (d) => (body += d));
220
+ // req.on("end", () => {
221
+ // res.writeHead(200, { "Content-Type": "application/json", ...CORS });
222
+ // res.end(JSON.stringify({ ok: true }));
223
+ // try {
224
+ // const { token, username } = JSON.parse(body);
225
+ // const c = cfg();
226
+ // c.token = token;
227
+ // c.username = username;
228
+ // saveCfg(c);
229
+ // srv.close();
230
+ // console.log();
231
+ // ok(`Logged in as ${C.bold(username)}`);
232
+ // resolve();
233
+ // } catch (e) {
234
+ // reject(e);
235
+ // }
236
+ // });
237
+ // } else {
238
+ // res.writeHead(404, CORS);
239
+ // res.end();
240
+ // }
241
+ // });
242
+
243
+ // srv.on("error", (e) => {
244
+ // if (e.code === "EADDRINUSE")
245
+ // fatal(
246
+ // `Port ${CB_PORT} is busy. Kill whatever is using it and try again.`,
247
+ // );
248
+ // reject(e);
249
+ // });
250
+ // console.log(
251
+ // `\n${C.bold("If Not Auto Login You Get Token on Browser (exit) Copy Pest")}`,
252
+ // );
253
+ // console.log(`\n${C.bold("Like this ngx token eyJhbGc.....")}`);
254
+
255
+ // srv.listen(CB_PORT, () =>
256
+ // info(`Waiting for GitHub callback (port ${CB_PORT})...`),
257
+ // );
258
+ // setTimeout(() => {
259
+ // srv.close();
260
+ // fatal("Login timed out (5 min).");
261
+ // }, 300_000);
262
+ // });
263
+ // }
264
+
265
+ // // ─── PUSH — upload files to GitHub via server ────────────────────────────────
266
+ // async function doPush(repoArg, opts = {}) {
267
+ // const username = getUsername();
268
+ // if (!username) fatal("Not logged in. Run: ngx login");
269
+ // const server = getServer();
270
+ // const folder = path.resolve(opts.path || ".");
271
+ // if (!fs.existsSync(folder)) fatal(`Folder not found: ${folder}`);
272
+
273
+ // const repoName = repoArg || path.basename(folder);
274
+ // const isPrivate = opts.visibility !== "public";
275
+ // const commitMsg = opts.message || `ngx push — ${new Date().toLocaleString()}`;
276
+ // const branch = opts.branch || "main";
277
+
278
+ // step(`Scanning ${C.bold(folder)}...`);
279
+ // const files = collectFiles(folder);
280
+ // if (!files.length) fatal("No files found to push (everything was ignored).");
281
+ // process.stdout.write(`\n`);
282
+
283
+ // // Show summary
284
+ // const totalKB =
285
+ // files.reduce((s, f) => s + Buffer.from(f.content_b64, "base64").length, 0) /
286
+ // 1024;
287
+ // info(
288
+ // `${files.length} files • ${totalKB.toFixed(1)} KB → ${C.bold(username + "/" + repoName)}`,
289
+ // );
290
+
291
+ // step("Pushing to GitHub...");
292
+ // try {
293
+ // const res = await axios.post(
294
+ // `${server}/push`,
295
+ // {
296
+ // repoName,
297
+ // files,
298
+ // message: commitMsg,
299
+ // branch,
300
+ // createIfMissing: true,
301
+ // private: isPrivate,
302
+ // },
303
+ // { headers: authHdr(), maxBodyLength: Infinity, timeout: 120_000 },
304
+ // );
305
+
306
+ // process.stdout.write("\n");
307
+ // ok(`Pushed ${C.bold(res.data.files)} files to ${C.bold(res.data.repo)}`);
308
+ // info(`Branch : ${res.data.branch}`);
309
+ // info(`Commit : ${res.data.commit}`);
310
+ // info(`GitHub : ${C.cyan(res.data.url)}`);
311
+ // } catch (e) {
312
+ // process.stdout.write("\n");
313
+ // fatal(e.response?.data?.error || e.message);
314
+ // }
315
+ // }
316
+
317
+ // // ─── CLONE — clone repo from GitHub ──────────────────────────────────────────
318
+ // // Uses GitHub API to fetch file tree + blob contents, writes to disk.
319
+ // // No `git` binary needed.
320
+ // async function doClone(target, destArg, opts = {}) {
321
+ // if (!target || !target.includes("/"))
322
+ // fatal("Usage: ngx clone owner/repo [dest]");
323
+ // const [owner, repoName] = target.split("/");
324
+ // const dest = path.resolve(destArg || repoName);
325
+ // const server = getServer();
326
+
327
+ // step(`Fetching file tree for ${C.bold(target)}...`);
328
+ // let treeData;
329
+ // try {
330
+ // const res = await axios.get(`${server}/tree/${owner}/${repoName}`, {
331
+ // headers: authHdr(),
332
+ // });
333
+ // treeData = res.data;
334
+ // } catch (e) {
335
+ // process.stdout.write("\n");
336
+ // if (e.response?.status === 404)
337
+ // fatal("Repo not found or you don't have access");
338
+ // fatal(e.response?.data?.error || e.message);
339
+ // }
340
+
341
+ // const files = treeData.files || [];
342
+ // const branch = treeData.branch;
343
+ // process.stdout.write("\n");
344
+ // info(`${files.length} files on branch ${branch}`);
345
+
346
+ // // GitHub raw content URL for each file
347
+ // const ghToken = null; // optional: could expose via /me route
348
+ // const ghHeaders = { Accept: "application/vnd.github.raw+json", ...authHdr() };
349
+ // // We'll download via the server proxy or directly from GitHub raw
350
+ // const repoInfo = await axios.get(`${server}/repos/${owner}/${repoName}`, {
351
+ // headers: authHdr(),
352
+ // });
353
+ // const cloneBase = `https://raw.githubusercontent.com/${owner}/${repoName}/${branch}`;
354
+
355
+ // // For private repos we need the GitHub token — fetch it via server
356
+ // let rawHeaders = {};
357
+ // try {
358
+ // // The server can expose a /me/ghtoken route for the logged-in user's own repos
359
+ // const tk = await axios.get(`${server}/me/ghtoken`, { headers: authHdr() });
360
+ // if (tk.data?.ghToken)
361
+ // rawHeaders["Authorization"] = `Bearer ${tk.data.ghToken}`;
362
+ // } catch {}
363
+
364
+ // if (fs.existsSync(dest)) {
365
+ // info(`Destination ./${path.basename(dest)}/ already exists — overwriting`);
366
+ // } else {
367
+ // fs.mkdirSync(dest, { recursive: true });
368
+ // }
369
+
370
+ // let done = 0;
371
+ // for (const file of files) {
372
+ // step(`Downloading ${done + 1}/${files.length} ${C.grey(file.path)}`);
373
+ // try {
374
+ // const raw = await axios.get(`${cloneBase}/${file.path}`, {
375
+ // headers: rawHeaders,
376
+ // responseType: "arraybuffer",
377
+ // });
378
+ // const outPath = path.join(dest, file.path);
379
+ // fs.mkdirSync(path.dirname(outPath), { recursive: true });
380
+ // fs.writeFileSync(outPath, raw.data);
381
+ // done++;
382
+ // } catch (e) {
383
+ // process.stdout.write("\n");
384
+ // info(
385
+ // C.yel(
386
+ // `Could not download: ${file.path} — ${e.response?.status || e.message}`,
387
+ // ),
388
+ // );
389
+ // }
390
+ // }
391
+ // process.stdout.write("\n");
392
+ // ok(
393
+ // `Cloned ${C.bold(target)} (${done}/${files.length} files) → ./${path.basename(dest)}/`,
394
+ // );
395
+ // }
396
+
397
+ // // ─── REPOS ────────────────────────────────────────────────────────────────────
398
+ // async function listRepos() {
399
+ // if (!getToken()) fatal("Not logged in");
400
+ // step("Fetching repos from GitHub...");
401
+ // try {
402
+ // const res = await axios.get(`${getServer()}/repos`, { headers: authHdr() });
403
+ // process.stdout.write("\n");
404
+ // const repos = res.data.repos || [];
405
+ // if (!repos.length) {
406
+ // info("No repos found.");
407
+ // return;
408
+ // }
409
+ // console.log(`\n${C.bold(getUsername() + "'s GitHub repos:")}\n`);
410
+ // repos.forEach((r) => {
411
+ // const icon = r.private ? "🔒" : "🌐";
412
+ // const lang = r.language ? C.grey(r.language.padEnd(14)) : " ".repeat(14);
413
+ // const upd = r.pushed_at ? timeSince(r.pushed_at) : "—";
414
+ // console.log(
415
+ // ` ${icon} ${C.cyan(r.name.padEnd(32))} ${lang} ${C.grey(upd)}`,
416
+ // );
417
+ // });
418
+ // console.log();
419
+ // } catch (e) {
420
+ // process.stdout.write("\n");
421
+ // fatal(e.response?.data?.error || e.message);
422
+ // }
423
+ // }
424
+
425
+ // // ─── CREATE REPO ──────────────────────────────────────────────────────────────
426
+ // async function createRepo(name, opts = {}) {
427
+ // if (!name) fatal('Usage: ngx create <reponame> [--public] [--desc "..."]');
428
+ // step(`Creating ${C.bold(name)} on GitHub...`);
429
+ // try {
430
+ // const res = await axios.post(
431
+ // `${getServer()}/repos`,
432
+ // {
433
+ // name,
434
+ // private: opts.visibility !== "public",
435
+ // description: opts.desc || "",
436
+ // },
437
+ // { headers: authHdr() },
438
+ // );
439
+ // process.stdout.write("\n");
440
+ // ok(`Created ${C.bold(res.data.full_name)}`);
441
+ // info(`GitHub : ${C.cyan(`https://github.com/${res.data.full_name}`)}`);
442
+ // info(`Private: ${res.data.private}`);
443
+ // } catch (e) {
444
+ // process.stdout.write("\n");
445
+ // fatal(e.response?.data?.error || e.message);
446
+ // }
447
+ // }
448
+
449
+ // // ─── DELETE ───────────────────────────────────────────────────────────────────
450
+ // async function deleteRepo(name) {
451
+ // if (!name) fatal("Usage: ngx delete <reponame>");
452
+ // step(`Deleting ${C.bold(name)}...`);
453
+ // try {
454
+ // const res = await axios.delete(`${getServer()}/repos/${name}`, {
455
+ // headers: authHdr(),
456
+ // });
457
+ // process.stdout.write("\n");
458
+ // ok(res.data.message);
459
+ // } catch (e) {
460
+ // process.stdout.write("\n");
461
+ // fatal(e.response?.data?.error || e.message);
462
+ // }
463
+ // }
464
+
465
+ // // ─── VISIBILITY ───────────────────────────────────────────────────────────────
466
+ // async function setVisibility(name, vis) {
467
+ // if (!name || !["public", "private"].includes(vis))
468
+ // fatal("Usage: ngx visibility <repo> public|private");
469
+ // try {
470
+ // const res = await axios.patch(
471
+ // `${getServer()}/repos/${name}/visibility`,
472
+ // { visibility: vis },
473
+ // { headers: authHdr() },
474
+ // );
475
+ // ok(res.data.message);
476
+ // } catch (e) {
477
+ // fatal(e.response?.data?.error || e.message);
478
+ // }
479
+ // }
480
+
481
+ // // ─── LOGS ─────────────────────────────────────────────────────────────────────
482
+ // async function showLogs() {
483
+ // if (!getToken()) fatal("Not logged in");
484
+ // try {
485
+ // const res = await axios.get(`${getServer()}/logs`, { headers: authHdr() });
486
+ // const logs = res.data.logs || [];
487
+ // if (!logs.length) {
488
+ // info("No activity yet.");
489
+ // return;
490
+ // }
491
+ // console.log();
492
+ // logs.forEach((l) => {
493
+ // const ts = new Date(l.ts).toLocaleString();
494
+ // const det =
495
+ // l.details && Object.keys(l.details).length
496
+ // ? C.grey(" " + JSON.stringify(l.details))
497
+ // : "";
498
+ // console.log(
499
+ // ` ${C.grey(ts)} ${C.cyan(l.action.padEnd(18))} ${l.repo || ""}${det}`,
500
+ // );
501
+ // });
502
+ // console.log();
503
+ // } catch (e) {
504
+ // fatal(e.response?.data?.error || e.message);
505
+ // }
506
+ // }
507
+
508
+ // // ─── UTILS ────────────────────────────────────────────────────────────────────
509
+ // function timeSince(d) {
510
+ // const s = Math.floor((Date.now() - new Date(d)) / 1000);
511
+ // if (s < 60) return "just now";
512
+ // if (s < 3600) return `${Math.floor(s / 60)}m ago`;
513
+ // if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
514
+ // return `${Math.floor(s / 86400)}d ago`;
515
+ // }
516
+
517
+ // function parseArgs(rawArgs) {
518
+ // const flags = {};
519
+ // const rest = [];
520
+ // for (let i = 0; i < rawArgs.length; i++) {
521
+ // if (rawArgs[i].startsWith("--")) {
522
+ // const key = rawArgs[i].slice(2);
523
+ // const val =
524
+ // rawArgs[i + 1] && !rawArgs[i + 1].startsWith("--")
525
+ // ? rawArgs[++i]
526
+ // : true;
527
+ // flags[key] = val;
528
+ // } else {
529
+ // rest.push(rawArgs[i]);
530
+ // }
531
+ // }
532
+ // return { flags, rest };
533
+ // }
534
+
535
+ // // ─── MAIN ─────────────────────────────────────────────────────────────────────
536
+ // const [, , cmd, ...rawArgs] = process.argv;
537
+ // const { flags, rest } = parseArgs(rawArgs || []);
538
+
539
+ // (async () => {
540
+ // switch (cmd) {
541
+ // case "-v":
542
+ // case "--version":
543
+ // console.log(`ngx ${VERSION}`);
544
+ // break;
545
+
546
+ // case "token":
547
+ // if (!rest[0]) fatal("Usage: ngx token <JWT>");
548
+
549
+ // try {
550
+ // const decoded = JSON.parse(
551
+ // Buffer.from(rest[0].split(".")[1], "base64").toString(),
552
+ // );
553
+
554
+ // const c = cfg();
555
+ // c.token = rest[0];
556
+ // c.username = decoded.username; // 🔥 fix
557
+ // saveCfg(c);
558
+
559
+ // ok(`Token saved for ${decoded.username}`);
560
+ // } catch {
561
+ // fatal("Invalid JWT token");
562
+ // }
563
+ // break;
564
+
565
+ // case "open":
566
+ // if (!rest[0]) fatal("Usage: ngx open <reponame>");
567
+ // await doOpen(rest[0], getUsername()); // 👈 pass here
568
+
569
+ // break;
570
+
571
+ // case "login":
572
+ // await doLogin();
573
+ // break;
574
+
575
+ // case "logout":
576
+ // const c2 = cfg();
577
+ // delete c2.token;
578
+ // delete c2.username;
579
+ // saveCfg(c2);
580
+ // ok("Logged out");
581
+ // break;
582
+
583
+ // case "whoami":
584
+ // const u = getUsername();
585
+ // if (u) console.log(`Logged in as: ${C.bold(u)} "nginxSoft"`);
586
+ // else console.log("Not logged in");
587
+ // break;
588
+
589
+ // // ngx push [reponame] [--path <dir>] [--public] [--message "msg"] [--branch main]
590
+ // case "push":
591
+ // await doPush(rest[0], {
592
+ // path: flags.path,
593
+ // visibility: flags.public ? "public" : flags.visibility || "private",
594
+ // message: flags.message || flags.m,
595
+ // branch: flags.branch || flags.b,
596
+ // });
597
+ // break;
598
+
599
+ // // ngx clone owner/repo [dest]
600
+ // case "clone":
601
+ // if (!rest[0]) fatal("Usage: ngx clone owner/repo [dest]");
602
+ // await doClone(rest[0], rest[1], {});
603
+ // break;
604
+
605
+ // // ngx repos
606
+ // case "repos":
607
+ // await listRepos();
608
+ // break;
609
+
610
+ // // ngx create <name> [--public] [--desc "..."]
611
+ // case "create":
612
+ // await createRepo(rest[0], {
613
+ // visibility: flags.public ? "public" : "private",
614
+ // desc: flags.desc || flags.description || "",
615
+ // });
616
+ // break;
617
+
618
+ // // ngx delete <name>
619
+ // case "delete":
620
+ // if (!rest[0]) fatal("Usage: ngx delete <reponame>");
621
+ // await deleteRepo(rest[0]);
622
+ // break;
623
+
624
+ // // ngx visibility <repo> public|private
625
+ // case "visibility":
626
+ // await setVisibility(rest[0], rest[1]);
627
+ // break;
628
+
629
+ // // ngx logs
630
+ // case "logs":
631
+ // await showLogs();
632
+ // break;
633
+
634
+ // // ngx config server <url>
635
+ // case "config":
636
+ // if (rest[0] === "server" && rest[1]) {
637
+ // const c3 = cfg();
638
+ // c3.server = rest[1];
639
+ // saveCfg(c3);
640
+ // ok(`Server set to ${rest[1]}`);
641
+ // } else console.log("Usage: ngx config server <url>");
642
+ // break;
643
+
644
+ // default:
645
+ // console.log(`
646
+ // ${C.bold("NGX v" + VERSION)} — GitHub proxy CLI
647
+
648
+ // ${C.cyan("Auth:")}
649
+ // ngx login GitHub OAuth (opens browser)
650
+ // ngx logout Clear saved session
651
+ // ngx whoami Show current user
652
+
653
+ // ${C.cyan("Push / Clone:")}
654
+ // ngx push Push current folder to GitHub
655
+ // ngx push my-repo Push with specific repo name
656
+ // ngx push my-repo --public Push as public repo
657
+ // ngx push my-repo --path ./src Push a subfolder
658
+ // ngx push my-repo --message "feat: x" Custom commit message
659
+ // ngx clone owner/repo Clone repo (downloads all files)
660
+ // ngx clone owner/repo ./local-dir Clone into specific directory
661
+
662
+ // ${C.cyan("Repos:")}
663
+ // ngx repos List your GitHub repos
664
+ // ngx create <name> [--public] Create a new GitHub repo
665
+ // ngx delete <name> Delete a GitHub repo
666
+ // ngx visibility <repo> public|private Change repo visibility
667
+
668
+ // ${C.cyan("Logs:")}
669
+ // ngx logs Your NGX activity log
670
+
671
+ // ${C.cyan("Config:")}
672
+ // ngx config server <url> Point to a different NGX server
673
+ // ngx -v Show version
674
+
675
+ // ${C.grey("Auto-ignored on push:")}
676
+ // ${C.grey(".env node_modules .git build *.key *.pem secrets")}
677
+ // ${C.grey("Add extras in .ngxignore or .gitignore")}
678
+ // `);
679
+ // }
680
+ // })();
681
+ // #!/usr/bin/env node
682
+
3
683
  /**
4
684
  * NGX CLI v4.0
5
685
  * Pushes code directly to your GitHub repos via NGX server (OAuth proxy).
@@ -11,7 +691,7 @@ const path = require("path");
11
691
  const http = require("http");
12
692
  const os = require("os");
13
693
  const zlib = require("zlib");
14
-
694
+ const readline = require("readline");
15
695
  const axios = require("axios");
16
696
 
17
697
  const _log = console.log;
@@ -19,12 +699,15 @@ console.log = () => {};
19
699
  require("dotenv").config();
20
700
  console.log = _log;
21
701
 
22
- const VERSION = "1.0.5";
702
+ const VERSION = "1.0.6";
23
703
  const CONFIG_PATH = path.join(os.homedir(), ".ngxconfig");
24
- const CB_PORT = 9988; // local callback server port for OAuth
704
+ const CB_PORT = 9988;
705
+
706
+ const encoded = Buffer.from(
707
+ "aHR0cHM6Ly9uZ3gubmdpbnhzb2Z0Lm9ubGluZS9hcGk=",
708
+ "base64",
709
+ ).toString("utf-8");
25
710
 
26
- const encoded = atob("aHR0cHM6Ly9uZ3gubmdpbnhzb2Z0Lm9ubGluZS9hcGk=");
27
- // console.log(encoded);
28
711
  // ─── Default ignore patterns ──────────────────────────────────────────────────
29
712
  const DEFAULT_IGNORE = new Set([
30
713
  ".env",
@@ -91,6 +774,7 @@ const C = {
91
774
  yel: (s) => `\x1b[93m${s}\x1b[0m`,
92
775
  bold: (s) => `\x1b[1m${s}\x1b[0m`,
93
776
  };
777
+
94
778
  function fatal(msg) {
95
779
  console.error(C.red("✗ ") + msg);
96
780
  process.exit(1);
@@ -108,7 +792,6 @@ function step(msg) {
108
792
  // ─── Ignore logic ─────────────────────────────────────────────────────────────
109
793
  function loadIgnorePatterns(folderPath) {
110
794
  const pats = new Set(DEFAULT_IGNORE);
111
- // Read .ngxignore
112
795
  const ngxFile = path.join(folderPath, ".ngxignore");
113
796
  if (fs.existsSync(ngxFile))
114
797
  fs.readFileSync(ngxFile, "utf-8")
@@ -116,7 +799,6 @@ function loadIgnorePatterns(folderPath) {
116
799
  .map((l) => l.trim())
117
800
  .filter((l) => l && !l.startsWith("#"))
118
801
  .forEach((p) => pats.add(p));
119
- // Read .gitignore
120
802
  const gitFile = path.join(folderPath, ".gitignore");
121
803
  if (fs.existsSync(gitFile))
122
804
  fs.readFileSync(gitFile, "utf-8")
@@ -138,7 +820,6 @@ function matchesIgnore(name, patterns) {
138
820
  }
139
821
 
140
822
  // ─── Collect files from folder ────────────────────────────────────────────────
141
- // Returns array of { path: "relative/path", content_b64: "..." }
142
823
  function collectFiles(folderPath) {
143
824
  const ignore = loadIgnorePatterns(folderPath);
144
825
  const files = [];
@@ -153,9 +834,6 @@ function collectFiles(folderPath) {
153
834
  walk(fullPath, relPath);
154
835
  } else if (entry.isFile()) {
155
836
  const buf = fs.readFileSync(fullPath);
156
- const compressed = zlib.gzipSync(buf);
157
-
158
- // Skip binary files > 50 MB (GitHub limit)
159
837
  if (buf.length > 50 * 1024 * 1024) {
160
838
  console.log(
161
839
  C.yel(
@@ -164,7 +842,7 @@ function collectFiles(folderPath) {
164
842
  );
165
843
  continue;
166
844
  }
167
-
845
+ const compressed = zlib.gzipSync(buf);
168
846
  files.push({
169
847
  path: relPath,
170
848
  content_b64: compressed.toString("base64"),
@@ -177,6 +855,33 @@ function collectFiles(folderPath) {
177
855
  return files;
178
856
  }
179
857
 
858
+ // ─── Utils ────────────────────────────────────────────────────────────────────
859
+ function timeSince(d) {
860
+ const s = Math.floor((Date.now() - new Date(d)) / 1000);
861
+ if (s < 60) return "just now";
862
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
863
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
864
+ return `${Math.floor(s / 86400)}d ago`;
865
+ }
866
+
867
+ function parseArgs(rawArgs) {
868
+ const flags = {};
869
+ const rest = [];
870
+ for (let i = 0; i < rawArgs.length; i++) {
871
+ if (rawArgs[i].startsWith("--")) {
872
+ const key = rawArgs[i].slice(2);
873
+ const val =
874
+ rawArgs[i + 1] && !rawArgs[i + 1].startsWith("--")
875
+ ? rawArgs[++i]
876
+ : true;
877
+ flags[key] = val;
878
+ } else {
879
+ rest.push(rawArgs[i]);
880
+ }
881
+ }
882
+ return { flags, rest };
883
+ }
884
+
180
885
  // ─── LOGIN — GitHub OAuth ─────────────────────────────────────────────────────
181
886
  async function doLogin() {
182
887
  const existing = getToken();
@@ -187,11 +892,9 @@ async function doLogin() {
187
892
  }
188
893
 
189
894
  console.log(`\n${C.bold("NGX GitHub Login")}`);
190
-
191
895
  const loginUrl = `${getServer()}/auth/github`;
192
896
  console.log(C.grey(`Opening: ${loginUrl}\n`));
193
897
 
194
- // Try to open browser
195
898
  try {
196
899
  const { default: open } = await import("open").catch(() => ({
197
900
  default: null,
@@ -202,7 +905,6 @@ async function doLogin() {
202
905
  console.log(C.yel(`Open this in your browser:\n ${loginUrl}\n`));
203
906
  }
204
907
 
205
- // Spin local HTTP server to receive token
206
908
  return new Promise((resolve, reject) => {
207
909
  const srv = http.createServer((req, res) => {
208
910
  const CORS = {
@@ -248,6 +950,11 @@ async function doLogin() {
248
950
  reject(e);
249
951
  });
250
952
 
953
+ console.log(
954
+ `\n${C.bold("If Not Auto Login You Get Token on Browser (exit) Copy Paste")}`,
955
+ );
956
+ console.log(`\n${C.bold("Like this: ngx token eyJhbGc.....")}`);
957
+
251
958
  srv.listen(CB_PORT, () =>
252
959
  info(`Waiting for GitHub callback (port ${CB_PORT})...`),
253
960
  );
@@ -258,7 +965,7 @@ async function doLogin() {
258
965
  });
259
966
  }
260
967
 
261
- // ─── PUSH — upload files to GitHub via server ────────────────────────────────
968
+ // ─── PUSH ────────────────────────────────────────────────────────────────────
262
969
  async function doPush(repoArg, opts = {}) {
263
970
  const username = getUsername();
264
971
  if (!username) fatal("Not logged in. Run: ngx login");
@@ -274,9 +981,8 @@ async function doPush(repoArg, opts = {}) {
274
981
  step(`Scanning ${C.bold(folder)}...`);
275
982
  const files = collectFiles(folder);
276
983
  if (!files.length) fatal("No files found to push (everything was ignored).");
277
- process.stdout.write(`\n`);
984
+ process.stdout.write("\n");
278
985
 
279
- // Show summary
280
986
  const totalKB =
281
987
  files.reduce((s, f) => s + Buffer.from(f.content_b64, "base64").length, 0) /
282
988
  1024;
@@ -298,7 +1004,6 @@ async function doPush(repoArg, opts = {}) {
298
1004
  },
299
1005
  { headers: authHdr(), maxBodyLength: Infinity, timeout: 120_000 },
300
1006
  );
301
-
302
1007
  process.stdout.write("\n");
303
1008
  ok(`Pushed ${C.bold(res.data.files)} files to ${C.bold(res.data.repo)}`);
304
1009
  info(`Branch : ${res.data.branch}`);
@@ -310,9 +1015,7 @@ async function doPush(repoArg, opts = {}) {
310
1015
  }
311
1016
  }
312
1017
 
313
- // ─── CLONE — clone repo from GitHub ──────────────────────────────────────────
314
- // Uses GitHub API to fetch file tree + blob contents, writes to disk.
315
- // No `git` binary needed.
1018
+ // ─── CLONE ────────────────────────────────────────────────────────────────────
316
1019
  async function doClone(target, destArg, opts = {}) {
317
1020
  if (!target || !target.includes("/"))
318
1021
  fatal("Usage: ngx clone owner/repo [dest]");
@@ -339,24 +1042,15 @@ async function doClone(target, destArg, opts = {}) {
339
1042
  process.stdout.write("\n");
340
1043
  info(`${files.length} files on branch ${branch}`);
341
1044
 
342
- // GitHub raw content URL for each file
343
- const ghToken = null; // optional: could expose via /me route
344
- const ghHeaders = { Accept: "application/vnd.github.raw+json", ...authHdr() };
345
- // We'll download via the server proxy or directly from GitHub raw
346
- const repoInfo = await axios.get(`${server}/repos/${owner}/${repoName}`, {
347
- headers: authHdr(),
348
- });
349
- const cloneBase = `https://raw.githubusercontent.com/${owner}/${repoName}/${branch}`;
350
-
351
- // For private repos we need the GitHub token — fetch it via server
352
1045
  let rawHeaders = {};
353
1046
  try {
354
- // The server can expose a /me/ghtoken route for the logged-in user's own repos
355
1047
  const tk = await axios.get(`${server}/me/ghtoken`, { headers: authHdr() });
356
1048
  if (tk.data?.ghToken)
357
1049
  rawHeaders["Authorization"] = `Bearer ${tk.data.ghToken}`;
358
1050
  } catch {}
359
1051
 
1052
+ const cloneBase = `https://raw.githubusercontent.com/${owner}/${repoName}/${branch}`;
1053
+
360
1054
  if (fs.existsSync(dest)) {
361
1055
  info(`Destination ./${path.basename(dest)}/ already exists — overwriting`);
362
1056
  } else {
@@ -418,7 +1112,7 @@ async function listRepos() {
418
1112
  }
419
1113
  }
420
1114
 
421
- // ─── CREATE REPO ──────────────────────────────────────────────────────────────
1115
+ // ─── CREATE ───────────────────────────────────────────────────────────────────
422
1116
  async function createRepo(name, opts = {}) {
423
1117
  if (!name) fatal('Usage: ngx create <reponame> [--public] [--desc "..."]');
424
1118
  step(`Creating ${C.bold(name)} on GitHub...`);
@@ -501,34 +1195,770 @@ async function showLogs() {
501
1195
  }
502
1196
  }
503
1197
 
504
- // ─── UTILS ────────────────────────────────────────────────────────────────────
505
- function timeSince(d) {
506
- const s = Math.floor((Date.now() - new Date(d)) / 1000);
507
- if (s < 60) return "just now";
508
- if (s < 3600) return `${Math.floor(s / 60)}m ago`;
509
- if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
510
- return `${Math.floor(s / 86400)}d ago`;
1198
+ // ══════════════════════════════════════════════════════════════════════════════
1199
+ // ─── NGX OPEN — Repo Shell Mode ───────────────────────────────────────────────
1200
+ // ══════════════════════════════════════════════════════════════════════════════
1201
+
1202
+ // ─── Virtual FS state ─────────────────────────────────────────────────────────
1203
+ let _repoFiles = [];
1204
+ let _cwd = "/";
1205
+ let _repoName = "";
1206
+ let _dirty = new Map();
1207
+ let _deleted = new Set();
1208
+ let _owner = "";
1209
+
1210
+ // ─── Path helpers ─────────────────────────────────────────────────────────────
1211
+ function normPath(p) {
1212
+ const parts = p.split("/").filter(Boolean);
1213
+ const stack = [];
1214
+ for (const part of parts) {
1215
+ if (part === "..") stack.pop();
1216
+ else if (part !== ".") stack.push(part);
1217
+ }
1218
+ return "/" + stack.join("/");
511
1219
  }
512
1220
 
513
- function parseArgs(rawArgs) {
514
- const flags = {};
515
- const rest = [];
516
- for (let i = 0; i < rawArgs.length; i++) {
517
- if (rawArgs[i].startsWith("--")) {
518
- const key = rawArgs[i].slice(2);
519
- const val =
520
- rawArgs[i + 1] && !rawArgs[i + 1].startsWith("--")
521
- ? rawArgs[++i]
522
- : true;
523
- flags[key] = val;
1221
+ function resolvePath(input) {
1222
+ if (!input) return _cwd;
1223
+ if (input.startsWith("/")) return normPath(input);
1224
+ return normPath(_cwd + "/" + input);
1225
+ }
1226
+
1227
+ function relativePath(abs) {
1228
+ return abs === "/" ? "" : abs.slice(1);
1229
+ }
1230
+
1231
+ // ─── Fetch repo files ─────────────────────────────────────────────────────────
1232
+ async function fetchRepo(owner, repo) {
1233
+ const server = getServer();
1234
+ try {
1235
+ const treeRes = await axios.get(`${server}/tree/${owner}/${repo}`, {
1236
+ headers: authHdr(),
1237
+ });
1238
+ const files = treeRes.data.files || [];
1239
+ const branch = treeRes.data.branch;
1240
+
1241
+ let rawHeaders = {};
1242
+ try {
1243
+ const tk = await axios.get(`${server}/me/ghtoken`, {
1244
+ headers: authHdr(),
1245
+ });
1246
+ if (tk.data?.ghToken)
1247
+ rawHeaders["Authorization"] = `Bearer ${tk.data.ghToken}`;
1248
+ } catch {}
1249
+
1250
+ const cloneBase = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`;
1251
+
1252
+ step(`Loading ${files.length} files...`);
1253
+ const loaded = [];
1254
+ for (const f of files) {
1255
+ try {
1256
+ const raw = await axios.get(`${cloneBase}/${f.path}`, {
1257
+ headers: rawHeaders,
1258
+ responseType: "arraybuffer",
1259
+ timeout: 10_000,
1260
+ });
1261
+ const buf = Buffer.from(raw.data);
1262
+ const content = buf.toString("utf-8");
1263
+ const isBinary = content.includes("\0");
1264
+ loaded.push({
1265
+ path: f.path,
1266
+ content: isBinary ? null : content,
1267
+ binary: isBinary,
1268
+ size: buf.length,
1269
+ modified: new Date().toISOString(),
1270
+ });
1271
+ } catch {
1272
+ loaded.push({
1273
+ path: f.path,
1274
+ content: "",
1275
+ binary: false,
1276
+ size: 0,
1277
+ modified: new Date().toISOString(),
1278
+ });
1279
+ }
1280
+ }
1281
+ process.stdout.write("\n");
1282
+ return loaded;
1283
+ } catch (e) {
1284
+ process.stdout.write("\n");
1285
+ if (e.response?.status === 404) fatal("Repo not found or no access.");
1286
+ fatal(e.response?.data?.error || e.message);
1287
+ }
1288
+ }
1289
+
1290
+ // ─── Shell commands ───────────────────────────────────────────────────────────
1291
+ function cmd_ls(args) {
1292
+ const target = args.filter((a) => !a.startsWith("-"))[0];
1293
+ const dirAbs = target ? resolvePath(target) : _cwd;
1294
+ const rel = relativePath(dirAbs);
1295
+ const longFmt = args.some((a) => a.startsWith("-") && a.includes("l"));
1296
+
1297
+ const seen = new Set();
1298
+ const entries = [];
1299
+
1300
+ const addEntry = (fp, forceFile = false) => {
1301
+ if (rel === "") {
1302
+ const top = fp.split("/")[0];
1303
+ if (seen.has(top)) return;
1304
+ seen.add(top);
1305
+ const isDir = !forceFile && fp.includes("/");
1306
+ entries.push({ name: top, isDir, size: 0, path: fp });
524
1307
  } else {
525
- rest.push(rawArgs[i]);
1308
+ if (!fp.startsWith(rel + "/")) return;
1309
+ const rest = fp.slice(rel.length + 1);
1310
+ if (!rest) return;
1311
+ const part = rest.split("/")[0];
1312
+ if (seen.has(part)) return;
1313
+ seen.add(part);
1314
+ const isDir = !forceFile && rest.includes("/");
1315
+ entries.push({ name: part, isDir, size: 0, path: fp });
526
1316
  }
1317
+ };
1318
+
1319
+ for (const f of _repoFiles) {
1320
+ if (_deleted.has(f.path)) continue;
1321
+ addEntry(f.path);
527
1322
  }
528
- return { flags, rest };
1323
+ for (const [p] of _dirty) {
1324
+ const fp = p.startsWith("/") ? p.slice(1) : p;
1325
+ addEntry(fp, true);
1326
+ }
1327
+
1328
+ if (!entries.length) {
1329
+ console.log(C.grey(" (empty)"));
1330
+ return;
1331
+ }
1332
+
1333
+ entries.sort((a, b) => {
1334
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
1335
+ return a.name.localeCompare(b.name);
1336
+ });
1337
+
1338
+ if (longFmt) {
1339
+ for (const e of entries) {
1340
+ const dirty = _dirty.has(rel ? rel + "/" + e.name : e.name)
1341
+ ? C.yel("*")
1342
+ : " ";
1343
+ const type = e.isDir ? C.cyan("d") : "-";
1344
+ const name = e.isDir ? C.cyan(e.name + "/") : e.name;
1345
+ console.log(` ${type}${dirty} ${String(e.size).padStart(8)} ${name}`);
1346
+ }
1347
+ } else {
1348
+ const row = entries.map((e) => (e.isDir ? C.cyan(e.name + "/") : e.name));
1349
+ console.log(" " + row.join(" "));
1350
+ }
1351
+ }
1352
+
1353
+ function cmd_cd(args) {
1354
+ if (!args[0] || args[0] === "~") {
1355
+ _cwd = "/";
1356
+ return;
1357
+ }
1358
+ const target = resolvePath(args[0]);
1359
+ const rel = relativePath(target);
1360
+ if (rel === "") {
1361
+ _cwd = "/";
1362
+ return;
1363
+ }
1364
+ const exists = _repoFiles.some(
1365
+ (f) => f.path === rel || f.path.startsWith(rel + "/"),
1366
+ );
1367
+ if (exists) _cwd = target;
1368
+ else console.log(C.red(` cd: no such directory: ${args[0]}`));
1369
+ }
1370
+
1371
+ function cmd_pwd() {
1372
+ console.log(" " + (_cwd === "/" ? `/${_repoName}` : `/${_repoName}${_cwd}`));
529
1373
  }
530
1374
 
1375
+ function getFileContent(relPath) {
1376
+ if (_dirty.has(relPath)) return _dirty.get(relPath);
1377
+ return _repoFiles.find((f) => f.path === relPath)?.content || null;
1378
+ }
1379
+
1380
+ function cmd_cat(args) {
1381
+ if (!args[0]) {
1382
+ console.log(C.red(" Usage: cat <file>"));
1383
+ return;
1384
+ }
1385
+ const rel = relativePath(resolvePath(args[0]));
1386
+ if (_dirty.has(rel)) {
1387
+ console.log(_dirty.get(rel));
1388
+ return;
1389
+ }
1390
+ const f = _repoFiles.find((x) => x.path === rel);
1391
+ if (!f) {
1392
+ console.log(C.red(` cat: ${args[0]}: No such file`));
1393
+ return;
1394
+ }
1395
+ if (f.binary) {
1396
+ console.log(C.grey(" [binary file]"));
1397
+ return;
1398
+ }
1399
+ console.log(f.content || "");
1400
+ }
1401
+
1402
+ function cmd_head(args) {
1403
+ const nIdx = args.indexOf("-n");
1404
+ const n = nIdx !== -1 ? parseInt(args[nIdx + 1]) || 10 : 10;
1405
+ const fileArg = args.filter((a) => !a.startsWith("-") && !/^\d+$/.test(a))[0];
1406
+ if (!fileArg) {
1407
+ console.log(C.red(" Usage: head [-n N] <file>"));
1408
+ return;
1409
+ }
1410
+ const rel = relativePath(resolvePath(fileArg));
1411
+ const content = getFileContent(rel);
1412
+ if (content === null) {
1413
+ console.log(C.red(` head: ${fileArg}: No such file`));
1414
+ return;
1415
+ }
1416
+ console.log(content.split("\n").slice(0, n).join("\n"));
1417
+ }
1418
+
1419
+ function cmd_tail(args) {
1420
+ const nIdx = args.indexOf("-n");
1421
+ const n = nIdx !== -1 ? parseInt(args[nIdx + 1]) || 10 : 10;
1422
+ const fileArg = args.filter((a) => !a.startsWith("-") && !/^\d+$/.test(a))[0];
1423
+ if (!fileArg) {
1424
+ console.log(C.red(" Usage: tail [-n N] <file>"));
1425
+ return;
1426
+ }
1427
+ const rel = relativePath(resolvePath(fileArg));
1428
+ const content = getFileContent(rel);
1429
+ if (content === null) {
1430
+ console.log(C.red(` tail: ${fileArg}: No such file`));
1431
+ return;
1432
+ }
1433
+ const lines = content.split("\n");
1434
+ console.log(lines.slice(-n).join("\n"));
1435
+ }
1436
+
1437
+ function cmd_stat(args) {
1438
+ if (!args[0]) {
1439
+ console.log(C.red(" Usage: stat <file>"));
1440
+ return;
1441
+ }
1442
+ const rel = relativePath(resolvePath(args[0]));
1443
+ const f = _repoFiles.find((x) => x.path === rel);
1444
+ if (!f) {
1445
+ console.log(C.red(` stat: ${args[0]}: No such file`));
1446
+ return;
1447
+ }
1448
+ console.log(` File : ${f.path}`);
1449
+ console.log(` Size : ${f.size} bytes`);
1450
+ console.log(` Type : ${f.binary ? "binary" : "text"}`);
1451
+ console.log(` Mod : ${f.modified}`);
1452
+ }
1453
+
1454
+ function cmd_find(args) {
1455
+ const nameFlag = args.indexOf("-name");
1456
+ const pattern = nameFlag !== -1 ? args[nameFlag + 1] : args[0];
1457
+ if (!pattern) {
1458
+ console.log(C.red(" Usage: find -name <pattern>"));
1459
+ return;
1460
+ }
1461
+ const re = new RegExp(pattern.replace(/\*/g, ".*"), "i");
1462
+ const matches = _repoFiles
1463
+ .filter((f) => !_deleted.has(f.path) && re.test(path.basename(f.path)))
1464
+ .map((f) => C.grey(" ./") + f.path);
1465
+ if (!matches.length) console.log(C.grey(" (no matches)"));
1466
+ else console.log(matches.join("\n"));
1467
+ }
1468
+
1469
+ function cmd_grep(args) {
1470
+ if (args.length < 2) {
1471
+ console.log(C.red(" Usage: grep <pattern> <file|*>"));
1472
+ return;
1473
+ }
1474
+ const pattern = args[0];
1475
+ const target = args[1];
1476
+ let re;
1477
+ try {
1478
+ re = new RegExp(pattern, "gi");
1479
+ } catch {
1480
+ console.log(C.red(" Invalid regex pattern"));
1481
+ return;
1482
+ }
1483
+
1484
+ const files =
1485
+ target === "*" || target === "."
1486
+ ? _repoFiles.filter((f) => !f.binary && !_deleted.has(f.path))
1487
+ : [
1488
+ _repoFiles.find((f) => f.path === relativePath(resolvePath(target))),
1489
+ ].filter(Boolean);
1490
+
1491
+ let found = false;
1492
+ for (const f of files) {
1493
+ if (!f || f.binary) continue;
1494
+ const content = getFileContent(f.path) || "";
1495
+ content.split("\n").forEach((line, i) => {
1496
+ if (re.test(line)) {
1497
+ re.lastIndex = 0;
1498
+ console.log(
1499
+ ` ${C.cyan(f.path)}:${C.grey(i + 1)}:${line.replace(re, (m) => C.yel(m))}`,
1500
+ );
1501
+ re.lastIndex = 0;
1502
+ found = true;
1503
+ }
1504
+ });
1505
+ }
1506
+ if (!found) console.log(C.grey(" (no matches)"));
1507
+ }
1508
+
1509
+ function cmd_mkdir(args) {
1510
+ if (!args[0]) {
1511
+ console.log(C.red(" Usage: mkdir <dirname>"));
1512
+ return;
1513
+ }
1514
+ console.log(
1515
+ C.grey(
1516
+ ` mkdir: ${args[0]} (will exist after first file is created inside)`,
1517
+ ),
1518
+ );
1519
+ }
1520
+
1521
+ function cmd_touch(args) {
1522
+ if (!args[0]) {
1523
+ console.log(C.red(" Usage: touch <file>"));
1524
+ return;
1525
+ }
1526
+ const rel = relativePath(resolvePath(args[0]));
1527
+ if (!_dirty.has(rel) && !_repoFiles.find((f) => f.path === rel)) {
1528
+ _dirty.set(rel, "");
1529
+ console.log(C.yel(` created: ${rel}`));
1530
+ } else {
1531
+ console.log(C.grey(` touched: ${rel}`));
1532
+ }
1533
+ }
1534
+
1535
+ function cmd_rm(args) {
1536
+ if (!args[0]) {
1537
+ console.log(C.red(" Usage: rm <file>"));
1538
+ return;
1539
+ }
1540
+ const rel = relativePath(resolvePath(args[0]));
1541
+ const f = _repoFiles.find((x) => x.path === rel);
1542
+ if (!f && !_dirty.has(rel)) {
1543
+ console.log(C.red(` rm: ${args[0]}: No such file`));
1544
+ return;
1545
+ }
1546
+ _deleted.add(rel);
1547
+ _dirty.delete(rel);
1548
+ console.log(C.grey(` removed: ${rel}`));
1549
+ }
1550
+
1551
+ function cmd_cp(args) {
1552
+ if (args.length < 2) {
1553
+ console.log(C.red(" Usage: cp <src> <dest>"));
1554
+ return;
1555
+ }
1556
+ const srcRel = relativePath(resolvePath(args[0]));
1557
+ const destRel = relativePath(resolvePath(args[1]));
1558
+ const content = getFileContent(srcRel);
1559
+ if (content === null) {
1560
+ console.log(C.red(` cp: ${args[0]}: No such file`));
1561
+ return;
1562
+ }
1563
+ _dirty.set(destRel, content);
1564
+ console.log(C.grey(` ${srcRel} → ${destRel}`));
1565
+ }
1566
+
1567
+ function cmd_mv(args) {
1568
+ if (args.length < 2) {
1569
+ console.log(C.red(" Usage: mv <src> <dest>"));
1570
+ return;
1571
+ }
1572
+ const srcRel = relativePath(resolvePath(args[0]));
1573
+ const destRel = relativePath(resolvePath(args[1]));
1574
+ const content = getFileContent(srcRel);
1575
+ if (content === null) {
1576
+ console.log(C.red(` mv: ${args[0]}: No such file`));
1577
+ return;
1578
+ }
1579
+ _dirty.set(destRel, content);
1580
+ _deleted.add(srcRel);
1581
+ _dirty.delete(srcRel);
1582
+ console.log(C.grey(` ${srcRel} → ${destRel}`));
1583
+ }
1584
+
1585
+ function cmd_tree(args) {
1586
+ const tArg = args.filter((a) => !a.startsWith("-"))[0];
1587
+ const target = tArg ? relativePath(resolvePath(tArg)) : relativePath(_cwd);
1588
+ const prefix = target === "" ? "" : target + "/";
1589
+ const relevant = _repoFiles.filter(
1590
+ (f) =>
1591
+ !_deleted.has(f.path) &&
1592
+ (target === "" || f.path.startsWith(prefix) || f.path === target),
1593
+ );
1594
+ const paths = relevant
1595
+ .map((f) => f.path.slice(prefix.length))
1596
+ .filter(Boolean);
1597
+
1598
+ function printTree(items, indent) {
1599
+ const dirs = new Set();
1600
+ const files = [];
1601
+ for (const p of items) {
1602
+ const parts = p.split("/");
1603
+ if (parts.length > 1) dirs.add(parts[0]);
1604
+ else files.push(p);
1605
+ }
1606
+ for (const d of [...dirs].sort()) {
1607
+ console.log(indent + C.cyan("├── " + d + "/"));
1608
+ const sub = items
1609
+ .filter((p) => p.startsWith(d + "/"))
1610
+ .map((p) => p.slice(d.length + 1));
1611
+ printTree(sub, indent + "│ ");
1612
+ }
1613
+ for (const f of files.sort()) {
1614
+ console.log(indent + "├── " + f);
1615
+ }
1616
+ }
1617
+
1618
+ console.log(C.cyan(" " + (target || _repoName)));
1619
+ printTree(paths, " ");
1620
+ }
1621
+
1622
+ function cmd_diff() {
1623
+ if (!_dirty.size && !_deleted.size) {
1624
+ console.log(C.grey(" No changes."));
1625
+ return;
1626
+ }
1627
+ console.log(C.bold("\n Pending changes:\n"));
1628
+ for (const [p] of _dirty) {
1629
+ const isNew = !_repoFiles.find((f) => f.path === p);
1630
+ console.log(C.yel(` M ${p}`) + (isNew ? C.green(" (new)") : ""));
1631
+ }
1632
+ for (const p of _deleted) {
1633
+ console.log(C.red(` D ${p}`));
1634
+ }
1635
+ console.log();
1636
+ }
1637
+
1638
+ function cmd_help() {
1639
+ console.log(`
1640
+ ${C.bold("NGX Repo Terminal — available commands:")}
1641
+
1642
+ ${C.cyan("Navigation:")}
1643
+ ls [-l] List files and directories
1644
+ cd <dir> Change directory
1645
+ pwd Show current path
1646
+ tree [dir] Show directory tree
1647
+
1648
+ ${C.cyan("File ops:")}
1649
+ cat <file> Print file contents
1650
+ head [-n N] <file> First N lines
1651
+ tail [-n N] <file> Last N lines
1652
+ stat <file> File info
1653
+ nano <file> Edit file (inline editor)
1654
+ touch <file> Create empty file
1655
+ rm <file> Mark file for deletion
1656
+ cp <src> <dest> Copy file
1657
+ mv <src> <dest> Move/rename file
1658
+ mkdir <dir> Create directory (virtual)
1659
+
1660
+ ${C.cyan("Search:")}
1661
+ find -name <pat> Find files matching pattern
1662
+ grep <pat> <file> Search text in file (use * for all)
1663
+
1664
+ ${C.cyan("Repo:")}
1665
+ diff Show pending changes
1666
+ push [message] Commit & push changes to GitHub
1667
+ reload Re-fetch repo files from GitHub
1668
+ clear Clear terminal screen
1669
+
1670
+ ${C.cyan("Session:")}
1671
+ help Show this help
1672
+ exit / quit Leave repo terminal
1673
+ `);
1674
+ }
1675
+
1676
+ // ─── nano: inline editor ──────────────────────────────────────────────────────
1677
+ async function cmd_nano(args, rl) {
1678
+ if (!args[0]) {
1679
+ console.log(C.red(" Usage: nano <file>"));
1680
+ return;
1681
+ }
1682
+ const rel = relativePath(resolvePath(args[0]));
1683
+ const existing = getFileContent(rel) || "";
1684
+
1685
+ console.log(`\n${C.bold("NGX nano")} — ${C.cyan(rel)}`);
1686
+ console.log(
1687
+ C.grey(
1688
+ " Paste/type your content. Type " +
1689
+ C.bold("::save") +
1690
+ " on a new line to save, " +
1691
+ C.bold("::quit") +
1692
+ " to cancel.\n",
1693
+ ),
1694
+ );
1695
+
1696
+ if (existing) {
1697
+ console.log(C.grey(" [current content — will be replaced]"));
1698
+ const preview = existing.split("\n").slice(0, 5).join("\n ");
1699
+ const more = existing.split("\n").length > 5 ? "\n ..." : "";
1700
+ console.log(C.grey(" " + preview + more));
1701
+ console.log();
1702
+ }
1703
+
1704
+ const lines = [];
1705
+ let lineNum = 1;
1706
+
1707
+ return new Promise((resolve) => {
1708
+ rl.setPrompt(C.grey(` ${lineNum}: `));
1709
+ rl.prompt();
1710
+
1711
+ const onLine = (line) => {
1712
+ if (line === "::save") {
1713
+ _dirty.set(rel, lines.join("\n"));
1714
+ console.log(
1715
+ C.green(
1716
+ `\n ✓ Saved ${rel} (${lines.length} lines, use 'push' to commit)\n`,
1717
+ ),
1718
+ );
1719
+ rl.removeListener("line", onLine);
1720
+ resolve();
1721
+ } else if (line === "::quit") {
1722
+ console.log(C.grey("\n Cancelled.\n"));
1723
+ rl.removeListener("line", onLine);
1724
+ resolve();
1725
+ } else {
1726
+ lines.push(line);
1727
+ lineNum++;
1728
+ rl.setPrompt(C.grey(` ${lineNum}: `));
1729
+ rl.prompt();
1730
+ }
1731
+ };
1732
+ rl.on("line", onLine);
1733
+ });
1734
+ }
1735
+
1736
+ // ─── push from within the session ────────────────────────────────────────────
1737
+ async function cmd_sessionPush(args) {
1738
+ if (!_dirty.size && !_deleted.size) {
1739
+ console.log(C.grey(" Nothing to push. No changes made."));
1740
+ return;
1741
+ }
1742
+
1743
+ const commitMsg =
1744
+ args.join(" ") || `ngx open — ${new Date().toLocaleString()}`;
1745
+ const files = [];
1746
+
1747
+ for (const f of _repoFiles) {
1748
+ if (_deleted.has(f.path)) continue;
1749
+ if (!f.binary) {
1750
+ const content = getFileContent(f.path) || "";
1751
+ const buf = Buffer.from(content);
1752
+ const compressed = zlib.gzipSync(buf);
1753
+ files.push({
1754
+ path: f.path,
1755
+ content_b64: compressed.toString("base64"),
1756
+ compressed: true,
1757
+ });
1758
+ }
1759
+ }
1760
+
1761
+ for (const [p, content] of _dirty) {
1762
+ if (!_repoFiles.find((f) => f.path === p)) {
1763
+ const buf = Buffer.from(content);
1764
+ const compressed = zlib.gzipSync(buf);
1765
+ files.push({
1766
+ path: p,
1767
+ content_b64: compressed.toString("base64"),
1768
+ compressed: true,
1769
+ });
1770
+ }
1771
+ }
1772
+
1773
+ step("Pushing changes...");
1774
+ try {
1775
+ const res = await axios.post(
1776
+ `${getServer()}/push`,
1777
+ {
1778
+ repoName: _repoName,
1779
+ files,
1780
+ message: commitMsg,
1781
+ branch: "main",
1782
+ createIfMissing: false,
1783
+ private: true,
1784
+ },
1785
+ { headers: authHdr(), maxBodyLength: Infinity, timeout: 120_000 },
1786
+ );
1787
+ process.stdout.write("\n");
1788
+ ok(`Pushed ${C.bold(res.data.files)} files — ${C.cyan(res.data.url)}`);
1789
+ _dirty.clear();
1790
+ _deleted.clear();
1791
+ } catch (e) {
1792
+ process.stdout.write("\n");
1793
+ fatal(e.response?.data?.error || e.message);
1794
+ }
1795
+ }
1796
+
1797
+ // ─── OPEN — Main entry point ──────────────────────────────────────────────────
1798
+ async function doOpen(repoArg) {
1799
+ if (!repoArg) fatal("Usage: ngx open <reponame> or ngx open owner/repo");
1800
+
1801
+ const username = getUsername();
1802
+ if (!username) fatal("Not logged in. Run: ngx login");
1803
+
1804
+ let owner, repoName;
1805
+ if (repoArg.includes("/")) {
1806
+ [owner, repoName] = repoArg.split("/");
1807
+ } else {
1808
+ owner = username;
1809
+ repoName = repoArg;
1810
+ }
1811
+
1812
+ _owner = owner;
1813
+ _repoName = repoName;
1814
+ _cwd = "/";
1815
+ _dirty = new Map();
1816
+ _deleted = new Set();
1817
+
1818
+ console.log(
1819
+ `\n${C.bold("NGX Repo Terminal")} — ${C.cyan(`${owner}/${repoName}`)}`,
1820
+ );
1821
+ console.log(C.grey(" Loading repo files from GitHub...\n"));
1822
+
1823
+ _repoFiles = await fetchRepo(owner, repoName);
1824
+
1825
+ console.log(C.green(` ✓ ${_repoFiles.length} files loaded`));
1826
+ console.log(
1827
+ C.grey(
1828
+ ` Type ${C.bold("help")} for available commands, ${C.bold("exit")} to leave.\n`,
1829
+ ),
1830
+ );
1831
+
1832
+ const rl = readline.createInterface({
1833
+ input: process.stdin,
1834
+ output: process.stdout,
1835
+ terminal: true,
1836
+ });
1837
+
1838
+ function prompt() {
1839
+ const rel = relativePath(_cwd);
1840
+ const loc = rel ? `${repoName}/${rel}` : repoName;
1841
+ const dirty = _dirty.size + _deleted.size > 0 ? C.yel("*") : "";
1842
+ rl.setPrompt(
1843
+ `${C.grey(owner + "/")}${C.bold(C.cyan(loc))}${dirty} ${C.grey("❯")} `,
1844
+ );
1845
+ rl.prompt();
1846
+ }
1847
+
1848
+ prompt();
1849
+
1850
+ rl.on("line", async (input) => {
1851
+ rl.pause();
1852
+ const trimmed = input.trim();
1853
+ if (!trimmed) {
1854
+ prompt();
1855
+ rl.resume();
1856
+ return;
1857
+ }
1858
+
1859
+ const [command, ...args] = trimmed.split(/\s+/);
1860
+
1861
+ switch (command) {
1862
+ case "ls":
1863
+ cmd_ls(args);
1864
+ break;
1865
+ case "cd":
1866
+ cmd_cd(args);
1867
+ break;
1868
+ case "pwd":
1869
+ cmd_pwd();
1870
+ break;
1871
+ case "cat":
1872
+ cmd_cat(args);
1873
+ break;
1874
+ case "head":
1875
+ cmd_head(args);
1876
+ break;
1877
+ case "tail":
1878
+ cmd_tail(args);
1879
+ break;
1880
+ case "stat":
1881
+ cmd_stat(args);
1882
+ break;
1883
+ case "find":
1884
+ cmd_find(args);
1885
+ break;
1886
+ case "grep":
1887
+ cmd_grep(args);
1888
+ break;
1889
+ case "mkdir":
1890
+ cmd_mkdir(args);
1891
+ break;
1892
+ case "touch":
1893
+ cmd_touch(args);
1894
+ break;
1895
+ case "rm":
1896
+ cmd_rm(args);
1897
+ break;
1898
+ case "cp":
1899
+ cmd_cp(args);
1900
+ break;
1901
+ case "mv":
1902
+ cmd_mv(args);
1903
+ break;
1904
+ case "tree":
1905
+ cmd_tree(args);
1906
+ break;
1907
+ case "diff":
1908
+ cmd_diff();
1909
+ break;
1910
+ case "help":
1911
+ cmd_help();
1912
+ break;
1913
+ case "clear":
1914
+ process.stdout.write("\x1Bc");
1915
+ break;
1916
+ case "nano":
1917
+ await cmd_nano(args, rl);
1918
+ break;
1919
+ case "push":
1920
+ await cmd_sessionPush(args);
1921
+ break;
1922
+ case "reload":
1923
+ console.log(C.grey(" Reloading..."));
1924
+ _repoFiles = await fetchRepo(owner, repoName);
1925
+ _dirty.clear();
1926
+ _deleted.clear();
1927
+ console.log(C.green(` ✓ Reloaded ${_repoFiles.length} files`));
1928
+ break;
1929
+ case "exit":
1930
+ case "quit":
1931
+ if (_dirty.size + _deleted.size > 0 && !args.includes("--force")) {
1932
+ console.log(
1933
+ C.yel(
1934
+ `\n ⚠ You have ${_dirty.size + _deleted.size} unsaved change(s). Run 'push' first or 'exit --force'\n`,
1935
+ ),
1936
+ );
1937
+ prompt();
1938
+ rl.resume();
1939
+ return;
1940
+ }
1941
+ console.log(C.grey(`\n Leaving ${repoName} repo terminal.\n`));
1942
+ rl.close();
1943
+ process.exit(0);
1944
+ default:
1945
+ console.log(
1946
+ C.red(` command not found: ${command}`) +
1947
+ C.grey(" (type 'help' for commands)"),
1948
+ );
1949
+ }
1950
+
1951
+ prompt();
1952
+ rl.resume();
1953
+ });
1954
+
1955
+ rl.on("close", () => process.exit(0));
1956
+ }
1957
+
1958
+ // ══════════════════════════════════════════════════════════════════════════════
531
1959
  // ─── MAIN ─────────────────────────────────────────────────────────────────────
1960
+ // ══════════════════════════════════════════════════════════════════════════════
1961
+
532
1962
  const [, , cmd, ...rawArgs] = process.argv;
533
1963
  const { flags, rest } = parseArgs(rawArgs || []);
534
1964
 
@@ -541,42 +1971,45 @@ const { flags, rest } = parseArgs(rawArgs || []);
541
1971
 
542
1972
  case "token":
543
1973
  if (!rest[0]) fatal("Usage: ngx token <JWT>");
544
-
545
1974
  try {
546
- const decoded = JSON.parse(
547
- Buffer.from(rest[0].split(".")[1], "base64").toString(),
548
- );
549
-
1975
+ const payload = Buffer.from(rest[0].split(".")[1], "base64").toString();
1976
+ const decoded = JSON.parse(payload);
550
1977
  const c = cfg();
551
1978
  c.token = rest[0];
552
- c.username = decoded.username; // 🔥 fix
1979
+ c.username = decoded.username || decoded.login || decoded.sub;
1980
+ if (!c.username) fatal("Could not extract username from token");
553
1981
  saveCfg(c);
554
-
555
- ok(`Token saved for ${decoded.username}`);
1982
+ ok(`Token saved for ${C.bold(c.username)}`);
556
1983
  } catch {
557
1984
  fatal("Invalid JWT token");
558
1985
  }
559
1986
  break;
560
1987
 
1988
+ case "open":
1989
+ if (!rest[0]) fatal("Usage: ngx open <reponame>");
1990
+ await doOpen(rest[0]);
1991
+ break;
1992
+
561
1993
  case "login":
562
1994
  await doLogin();
563
1995
  break;
564
1996
 
565
- case "logout":
566
- const c2 = cfg();
567
- delete c2.token;
568
- delete c2.username;
569
- saveCfg(c2);
1997
+ case "logout": {
1998
+ const c = cfg();
1999
+ delete c.token;
2000
+ delete c.username;
2001
+ saveCfg(c);
570
2002
  ok("Logged out");
571
2003
  break;
2004
+ }
572
2005
 
573
- case "whoami":
2006
+ case "whoami": {
574
2007
  const u = getUsername();
575
2008
  if (u) console.log(`Logged in as: ${C.bold(u)} "nginxSoft"`);
576
2009
  else console.log("Not logged in");
577
2010
  break;
2011
+ }
578
2012
 
579
- // ngx push [reponame] [--path <dir>] [--public] [--message "msg"] [--branch main]
580
2013
  case "push":
581
2014
  await doPush(rest[0], {
582
2015
  path: flags.path,
@@ -586,18 +2019,15 @@ const { flags, rest } = parseArgs(rawArgs || []);
586
2019
  });
587
2020
  break;
588
2021
 
589
- // ngx clone owner/repo [dest]
590
2022
  case "clone":
591
2023
  if (!rest[0]) fatal("Usage: ngx clone owner/repo [dest]");
592
2024
  await doClone(rest[0], rest[1], {});
593
2025
  break;
594
2026
 
595
- // ngx repos
596
2027
  case "repos":
597
2028
  await listRepos();
598
2029
  break;
599
2030
 
600
- // ngx create <name> [--public] [--desc "..."]
601
2031
  case "create":
602
2032
  await createRepo(rest[0], {
603
2033
  visibility: flags.public ? "public" : "private",
@@ -605,30 +2035,28 @@ const { flags, rest } = parseArgs(rawArgs || []);
605
2035
  });
606
2036
  break;
607
2037
 
608
- // ngx delete <name>
609
2038
  case "delete":
610
2039
  if (!rest[0]) fatal("Usage: ngx delete <reponame>");
611
2040
  await deleteRepo(rest[0]);
612
2041
  break;
613
2042
 
614
- // ngx visibility <repo> public|private
615
2043
  case "visibility":
616
2044
  await setVisibility(rest[0], rest[1]);
617
2045
  break;
618
2046
 
619
- // ngx logs
620
2047
  case "logs":
621
2048
  await showLogs();
622
2049
  break;
623
2050
 
624
- // ngx config server <url>
625
2051
  case "config":
626
2052
  if (rest[0] === "server" && rest[1]) {
627
- const c3 = cfg();
628
- c3.server = rest[1];
629
- saveCfg(c3);
2053
+ const c = cfg();
2054
+ c.server = rest[1];
2055
+ saveCfg(c);
630
2056
  ok(`Server set to ${rest[1]}`);
631
- } else console.log("Usage: ngx config server <url>");
2057
+ } else {
2058
+ console.log("Usage: ngx config server <url>");
2059
+ }
632
2060
  break;
633
2061
 
634
2062
  default:
@@ -639,6 +2067,7 @@ ${C.cyan("Auth:")}
639
2067
  ngx login GitHub OAuth (opens browser)
640
2068
  ngx logout Clear saved session
641
2069
  ngx whoami Show current user
2070
+ ngx token <JWT> Manually save a token
642
2071
 
643
2072
  ${C.cyan("Push / Clone:")}
644
2073
  ngx push Push current folder to GitHub
@@ -654,6 +2083,7 @@ ${C.cyan("Repos:")}
654
2083
  ngx create <name> [--public] Create a new GitHub repo
655
2084
  ngx delete <name> Delete a GitHub repo
656
2085
  ngx visibility <repo> public|private Change repo visibility
2086
+ ngx open <repo> Open interactive repo terminal
657
2087
 
658
2088
  ${C.cyan("Logs:")}
659
2089
  ngx logs Your NGX activity log