ngx-git 1.0.4 → 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 +1521 -72
  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.3";
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);
1322
+ }
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(" "));
527
1350
  }
528
- return { flags, rest };
529
1351
  }
530
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}`));
1373
+ }
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
 
@@ -539,25 +1969,47 @@ const { flags, rest } = parseArgs(rawArgs || []);
539
1969
  console.log(`ngx ${VERSION}`);
540
1970
  break;
541
1971
 
1972
+ case "token":
1973
+ if (!rest[0]) fatal("Usage: ngx token <JWT>");
1974
+ try {
1975
+ const payload = Buffer.from(rest[0].split(".")[1], "base64").toString();
1976
+ const decoded = JSON.parse(payload);
1977
+ const c = cfg();
1978
+ c.token = rest[0];
1979
+ c.username = decoded.username || decoded.login || decoded.sub;
1980
+ if (!c.username) fatal("Could not extract username from token");
1981
+ saveCfg(c);
1982
+ ok(`Token saved for ${C.bold(c.username)}`);
1983
+ } catch {
1984
+ fatal("Invalid JWT token");
1985
+ }
1986
+ break;
1987
+
1988
+ case "open":
1989
+ if (!rest[0]) fatal("Usage: ngx open <reponame>");
1990
+ await doOpen(rest[0]);
1991
+ break;
1992
+
542
1993
  case "login":
543
1994
  await doLogin();
544
1995
  break;
545
1996
 
546
- case "logout":
547
- const c2 = cfg();
548
- delete c2.token;
549
- delete c2.username;
550
- saveCfg(c2);
1997
+ case "logout": {
1998
+ const c = cfg();
1999
+ delete c.token;
2000
+ delete c.username;
2001
+ saveCfg(c);
551
2002
  ok("Logged out");
552
2003
  break;
2004
+ }
553
2005
 
554
- case "whoami":
2006
+ case "whoami": {
555
2007
  const u = getUsername();
556
2008
  if (u) console.log(`Logged in as: ${C.bold(u)} "nginxSoft"`);
557
2009
  else console.log("Not logged in");
558
2010
  break;
2011
+ }
559
2012
 
560
- // ngx push [reponame] [--path <dir>] [--public] [--message "msg"] [--branch main]
561
2013
  case "push":
562
2014
  await doPush(rest[0], {
563
2015
  path: flags.path,
@@ -567,18 +2019,15 @@ const { flags, rest } = parseArgs(rawArgs || []);
567
2019
  });
568
2020
  break;
569
2021
 
570
- // ngx clone owner/repo [dest]
571
2022
  case "clone":
572
2023
  if (!rest[0]) fatal("Usage: ngx clone owner/repo [dest]");
573
2024
  await doClone(rest[0], rest[1], {});
574
2025
  break;
575
2026
 
576
- // ngx repos
577
2027
  case "repos":
578
2028
  await listRepos();
579
2029
  break;
580
2030
 
581
- // ngx create <name> [--public] [--desc "..."]
582
2031
  case "create":
583
2032
  await createRepo(rest[0], {
584
2033
  visibility: flags.public ? "public" : "private",
@@ -586,30 +2035,28 @@ const { flags, rest } = parseArgs(rawArgs || []);
586
2035
  });
587
2036
  break;
588
2037
 
589
- // ngx delete <name>
590
2038
  case "delete":
591
2039
  if (!rest[0]) fatal("Usage: ngx delete <reponame>");
592
2040
  await deleteRepo(rest[0]);
593
2041
  break;
594
2042
 
595
- // ngx visibility <repo> public|private
596
2043
  case "visibility":
597
2044
  await setVisibility(rest[0], rest[1]);
598
2045
  break;
599
2046
 
600
- // ngx logs
601
2047
  case "logs":
602
2048
  await showLogs();
603
2049
  break;
604
2050
 
605
- // ngx config server <url>
606
2051
  case "config":
607
2052
  if (rest[0] === "server" && rest[1]) {
608
- const c3 = cfg();
609
- c3.server = rest[1];
610
- saveCfg(c3);
2053
+ const c = cfg();
2054
+ c.server = rest[1];
2055
+ saveCfg(c);
611
2056
  ok(`Server set to ${rest[1]}`);
612
- } else console.log("Usage: ngx config server <url>");
2057
+ } else {
2058
+ console.log("Usage: ngx config server <url>");
2059
+ }
613
2060
  break;
614
2061
 
615
2062
  default:
@@ -620,6 +2067,7 @@ ${C.cyan("Auth:")}
620
2067
  ngx login GitHub OAuth (opens browser)
621
2068
  ngx logout Clear saved session
622
2069
  ngx whoami Show current user
2070
+ ngx token <JWT> Manually save a token
623
2071
 
624
2072
  ${C.cyan("Push / Clone:")}
625
2073
  ngx push Push current folder to GitHub
@@ -635,6 +2083,7 @@ ${C.cyan("Repos:")}
635
2083
  ngx create <name> [--public] Create a new GitHub repo
636
2084
  ngx delete <name> Delete a GitHub repo
637
2085
  ngx visibility <repo> public|private Change repo visibility
2086
+ ngx open <repo> Open interactive repo terminal
638
2087
 
639
2088
  ${C.cyan("Logs:")}
640
2089
  ngx logs Your NGX activity log