lecodes-cli 0.1.0 → 0.1.1
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/README.md +2 -2
- package/dist/index.js +87 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npx lecodes-cli <command>
|
|
|
16
16
|
## Quick start
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
|
-
lecodes login #
|
|
19
|
+
lecodes login # opens the browser to authorize this machine
|
|
20
20
|
lecodes clone <project-uuid> myapp # download files + types into ./myapp
|
|
21
21
|
cd myapp
|
|
22
22
|
# …edit files in VS Code (IntelliSense works out of the box)…
|
|
@@ -28,7 +28,7 @@ lecodes push -m "Tweak the menu" # push as a new checkpoint
|
|
|
28
28
|
|
|
29
29
|
| Command | What it does |
|
|
30
30
|
| --- | --- |
|
|
31
|
-
| `login` |
|
|
31
|
+
| `login` | Authorize in the browser; stores an access token in `~/.lecodes/config.json`. `--token <pat>` to use a token created in the web UI instead, `--api <url>` for the API origin (default `https://le.codes`), `--web <url>` for the app origin. |
|
|
32
32
|
| `clone <uuid\|url> [dir]` | Download a project's file tree + binary resources, plus `.d.ts` types and a `tsconfig.json` for IntelliSense. `--no-types` to skip types. |
|
|
33
33
|
| `status` | Show added / modified / moved / deleted files vs the last sync. |
|
|
34
34
|
| `pull` | Overwrite local files with the project's current remote state. `--force` to discard local edits. |
|
package/dist/index.js
CHANGED
|
@@ -38,16 +38,6 @@ var prompt = (question, fallback) => new Promise((resolve) => {
|
|
|
38
38
|
resolve(answer.trim() || fallback || "");
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
|
-
var promptHidden = (question) => new Promise((resolve) => {
|
|
42
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
43
|
-
rl.question(`${question}: `, (answer) => {
|
|
44
|
-
rl.close();
|
|
45
|
-
process.stdout.write(`
|
|
46
|
-
`);
|
|
47
|
-
resolve(answer.trim());
|
|
48
|
-
});
|
|
49
|
-
rl._writeToOutput = () => {};
|
|
50
|
-
});
|
|
51
41
|
var parseArgs = (argv) => {
|
|
52
42
|
const out = { _: [], flags: {} };
|
|
53
43
|
for (let i = 0;i < argv.length; i++) {
|
|
@@ -133,8 +123,6 @@ var apiRequest = async (apiUrl, path, opts = {}) => {
|
|
|
133
123
|
throw new CliError(errorMessage(parsed, resp.status));
|
|
134
124
|
return parsed;
|
|
135
125
|
};
|
|
136
|
-
var login = (apiUrl, login2, password) => apiRequest(apiUrl, "/account/login", { body: { login: login2, password } });
|
|
137
|
-
var createAccessToken = (apiUrl, jwt, name) => apiRequest(apiUrl, "/account/tokens", { token: jwt, body: { name } });
|
|
138
126
|
var getAccount = (apiUrl, token) => apiRequest(apiUrl, "/account", { token });
|
|
139
127
|
var getProject = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}`, { token });
|
|
140
128
|
var getCommits = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/commits`, { token });
|
|
@@ -352,20 +340,90 @@ var clone = async (args) => {
|
|
|
352
340
|
info(c.dim(` cd ${dir} — edit, then \`lecodes push -m "your message"\``));
|
|
353
341
|
};
|
|
354
342
|
|
|
355
|
-
// src/
|
|
343
|
+
// src/browserAuth.ts
|
|
344
|
+
import { createServer } from "node:http";
|
|
345
|
+
import { spawn } from "node:child_process";
|
|
346
|
+
import { randomBytes } from "node:crypto";
|
|
356
347
|
import { hostname } from "node:os";
|
|
357
|
-
var
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
348
|
+
var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
|
|
349
|
+
<body style="font-family:system-ui,sans-serif;background:#0f1115;color:#e6e6e6;display:grid;place-items:center;height:100vh;margin:0">
|
|
350
|
+
<div style="text-align:center;max-width:420px;padding:32px">
|
|
351
|
+
<h1 style="font-size:20px;margin:0 0 8px">✓ Authorized</h1>
|
|
352
|
+
<p style="color:#9aa0aa;margin:0;font-size:14px">You can close this tab and return to your terminal.</p></div>`;
|
|
353
|
+
var errorHtml = (msg) => `<!doctype html><meta charset="utf-8"><title>lecodes</title>
|
|
354
|
+
<body style="font-family:system-ui,sans-serif;background:#0f1115;color:#e6e6e6;display:grid;place-items:center;height:100vh;margin:0">
|
|
355
|
+
<div style="text-align:center;max-width:420px;padding:32px">
|
|
356
|
+
<h1 style="font-size:20px;margin:0 0 8px">Authorization failed</h1>
|
|
357
|
+
<p style="color:#9aa0aa;margin:0;font-size:14px">${msg}</p></div>`;
|
|
358
|
+
var openBrowser = (url) => {
|
|
359
|
+
let cmd;
|
|
360
|
+
let args;
|
|
361
|
+
if (process.platform === "win32") {
|
|
362
|
+
cmd = "rundll32";
|
|
363
|
+
args = ["url.dll,FileProtocolHandler", url];
|
|
364
|
+
} else if (process.platform === "darwin") {
|
|
365
|
+
cmd = "open";
|
|
366
|
+
args = [url];
|
|
367
|
+
} else {
|
|
368
|
+
cmd = "xdg-open";
|
|
369
|
+
args = [url];
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
373
|
+
child.on("error", () => {});
|
|
374
|
+
child.unref();
|
|
375
|
+
} catch {}
|
|
376
|
+
};
|
|
377
|
+
var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser) => {
|
|
378
|
+
const state = randomBytes(16).toString("hex");
|
|
379
|
+
let resolve2;
|
|
380
|
+
let reject;
|
|
381
|
+
const tokenPromise = new Promise((res, rej) => {
|
|
382
|
+
resolve2 = res;
|
|
383
|
+
reject = rej;
|
|
384
|
+
});
|
|
385
|
+
const server = createServer((req, res) => {
|
|
386
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
387
|
+
if (url.pathname !== "/") {
|
|
388
|
+
res.writeHead(404);
|
|
389
|
+
res.end();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const token = url.searchParams.get("token");
|
|
393
|
+
if (!token || url.searchParams.get("state") !== state) {
|
|
394
|
+
res.writeHead(400, { "content-type": "text/html" });
|
|
395
|
+
res.end(errorHtml("Invalid or expired authorization."));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
399
|
+
res.end(SUCCESS_HTML);
|
|
400
|
+
resolve2(token);
|
|
401
|
+
});
|
|
402
|
+
await new Promise((res, rej) => {
|
|
403
|
+
server.once("error", rej);
|
|
404
|
+
server.listen(0, "127.0.0.1", res);
|
|
405
|
+
});
|
|
406
|
+
const port = server.address().port;
|
|
407
|
+
const timer = setTimeout(() => reject(new CliError("Timed out waiting for browser authorization.")), timeoutMs);
|
|
408
|
+
const authUrl = `${webBase}/app/cli-auth?port=${port}&state=${state}&name=${encodeURIComponent(hostname())}`;
|
|
409
|
+
info("Opening your browser to authorize the CLI…");
|
|
410
|
+
log(c.dim(" " + authUrl));
|
|
411
|
+
log(c.dim(" (waiting — finish in the browser, or press Ctrl+C to cancel)"));
|
|
412
|
+
open(authUrl);
|
|
413
|
+
try {
|
|
414
|
+
return await tokenPromise;
|
|
415
|
+
} finally {
|
|
416
|
+
clearTimeout(timer);
|
|
417
|
+
server.close();
|
|
368
418
|
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// src/commands/login.ts
|
|
422
|
+
var login = async (args) => {
|
|
423
|
+
const config = loadConfig();
|
|
424
|
+
const apiUrl = normalizeApiUrl(flagStr(args, "api") ?? config.apiUrl ?? DEFAULT_API);
|
|
425
|
+
const webBase = normalizeApiUrl(flagStr(args, "web") ?? apiUrl);
|
|
426
|
+
const token = flagStr(args, "token") ?? await browserLogin(webBase);
|
|
369
427
|
const account = await getAccount(apiUrl, token);
|
|
370
428
|
saveConfig({ apiUrl, token });
|
|
371
429
|
success(`Logged in as ${c.bold(account.email)} on ${apiUrl}`);
|
|
@@ -586,9 +644,10 @@ var HELP = `${c.bold("lecodes")} — clone, edit and push LeCodes projects from
|
|
|
586
644
|
${c.bold("Usage:")} lecodes <command> [options]
|
|
587
645
|
|
|
588
646
|
${c.bold("Commands:")}
|
|
589
|
-
login
|
|
590
|
-
--token <pat>
|
|
591
|
-
--api <url>
|
|
647
|
+
login Authorize in the browser and store an access token
|
|
648
|
+
--token <pat> store a token from the web UI instead
|
|
649
|
+
--api <url> API origin (default https://le.codes)
|
|
650
|
+
--web <url> app origin for the browser flow
|
|
592
651
|
clone <uuid|url> [dir] Download a project's files + types into a new folder
|
|
593
652
|
--no-types skip the .d.ts / tsconfig generation
|
|
594
653
|
status Show local changes vs the last sync
|
|
@@ -599,7 +658,7 @@ ${c.bold("Commands:")}
|
|
|
599
658
|
--force push even if the server moved on
|
|
600
659
|
|
|
601
660
|
Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
|
|
602
|
-
var commands = { login
|
|
661
|
+
var commands = { login, clone, status, pull, push };
|
|
603
662
|
var main = async () => {
|
|
604
663
|
const argv = process.argv.slice(2);
|
|
605
664
|
const command = argv[0];
|