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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +87 -28
  3. 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 # paste a token, or sign in with email/password
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` | Store a personal access token in `~/.lecodes/config.json`. `--token <pat>` to use a token created in the web UI, `--api <url>` to set the server. |
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/commands/login.ts
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 login2 = async (args) => {
358
- const config = loadConfig();
359
- const apiUrl = normalizeApiUrl(flagStr(args, "api") ?? await prompt("Server URL", config.apiUrl ?? DEFAULT_API));
360
- let token = flagStr(args, "token");
361
- if (!token) {
362
- const email = flagStr(args, "email") ?? await prompt("Email");
363
- const password = await promptHidden("Password");
364
- info(c.dim("Authenticating…"));
365
- const { accessToken } = await login(apiUrl, email, password);
366
- const created = await createAccessToken(apiUrl, accessToken, `lecodes CLI (${hostname()})`);
367
- token = created.token;
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">&#10003; 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 Store a personal access token (prompts email/password)
590
- --token <pat> use an existing token instead
591
- --api <url> set the server 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: login2, clone, status, pull, push };
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];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lecodes-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Clone, edit and push LeCodes projects from your machine.",
5
5
  "type": "module",
6
6
  "license": "MIT",