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.
- package/cli.js +1510 -80
- 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.
|
|
702
|
+
const VERSION = "1.0.6";
|
|
23
703
|
const CONFIG_PATH = path.join(os.homedir(), ".ngxconfig");
|
|
24
|
-
const CB_PORT = 9988;
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
//
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
547
|
-
|
|
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
|
|
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
|
|
567
|
-
delete
|
|
568
|
-
delete
|
|
569
|
-
saveCfg(
|
|
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
|
|
628
|
-
|
|
629
|
-
saveCfg(
|
|
2053
|
+
const c = cfg();
|
|
2054
|
+
c.server = rest[1];
|
|
2055
|
+
saveCfg(c);
|
|
630
2056
|
ok(`Server set to ${rest[1]}`);
|
|
631
|
-
} else
|
|
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
|