@within-7/jetr 0.4.2 → 0.5.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 (2) hide show
  1. package/dist/cli.js +182 -84
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { resolve as resolve2, basename } from "path";
4
+ import { resolve as resolve2, basename as basename2 } from "path";
5
5
  import { existsSync as existsSync4, statSync as statSync2 } from "fs";
6
6
  import { Command } from "commander";
7
7
  import chalk2 from "chalk";
@@ -252,6 +252,7 @@ function createJetrignore(dir) {
252
252
  }
253
253
 
254
254
  // src/deploy.ts
255
+ import { basename } from "path";
255
256
  function hashFile(filePath) {
256
257
  const content = readFileSync3(filePath);
257
258
  const hash = createHash("sha256").update(content).digest("hex");
@@ -323,6 +324,49 @@ async function deploy(siteName, directory, onProgress) {
323
324
  totalSize: result.total_size
324
325
  };
325
326
  }
327
+ async function deployFiles(siteName, filePaths, onProgress) {
328
+ onProgress?.(`Preparing ${filePaths.length} file(s)...`);
329
+ const manifest = {};
330
+ const absMap = {};
331
+ for (const fp of filePaths) {
332
+ const abs = resolve(fp);
333
+ const name = basename(abs);
334
+ const hash = hashFile(abs);
335
+ const size = statSync(abs).size;
336
+ manifest[name] = { hash, size };
337
+ absMap[name] = abs;
338
+ }
339
+ onProgress?.("Computing diff...");
340
+ const diff = await api.deployDiff(siteName, manifest);
341
+ onProgress?.(
342
+ `Upload: ${diff.upload.length}, Delete: ${diff.delete.length}, Unchanged: ${diff.unchanged.length}`
343
+ );
344
+ if (diff.upload.length > 0) {
345
+ const CONCURRENCY = 10;
346
+ let completed = 0;
347
+ const total = diff.upload.length;
348
+ const uploadOne = async (name) => {
349
+ const content = readFileSync3(absMap[name]);
350
+ await api.uploadFile(siteName, name, content, diff.deploy_id, manifest[name].hash);
351
+ completed++;
352
+ onProgress?.(`Uploading (${completed}/${total}) ${name}`);
353
+ };
354
+ for (let i = 0; i < total; i += CONCURRENCY) {
355
+ const batch = diff.upload.slice(i, i + CONCURRENCY);
356
+ await Promise.all(batch.map(uploadOne));
357
+ }
358
+ }
359
+ onProgress?.("Finalizing...");
360
+ const result = await api.finalize(siteName, diff.deploy_id);
361
+ return {
362
+ url: `https://${siteName}.jetr.within-7.com`,
363
+ filesUploaded: result.files_uploaded,
364
+ filesDeleted: result.files_deleted,
365
+ filesUnchanged: diff.unchanged.length,
366
+ filesIgnored: 0,
367
+ totalSize: result.total_size
368
+ };
369
+ }
326
370
 
327
371
  // src/rc.ts
328
372
  import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
@@ -409,71 +453,62 @@ async function promptSelection(options) {
409
453
  // src/cli.ts
410
454
  var program = new Command();
411
455
  program.name("jetr").description("Deploy static sites instantly").version("0.2.0");
412
- program.argument("[directory]", "Directory to deploy (default: current directory)").argument("[name]", "Site name (reads from .jetrrc if omitted)").option("--no-ignore", "Skip .jetrignore rules").action(async (directory, name) => {
413
- const dir = resolve2(directory || ".");
414
- if (!existsSync4(dir)) {
415
- console.error(chalk2.red(`Path not found: ${dir}`));
416
- process.exit(1);
417
- }
418
- if (!statSync2(dir).isDirectory()) {
419
- console.error(chalk2.red(`Not a directory: ${dir}`));
420
- process.exit(1);
421
- }
456
+ program.argument("[paths...]", "Files or directory to deploy").action(async (paths) => {
422
457
  const config = loadConfig();
423
458
  if (!config.token) {
424
459
  console.error(chalk2.red("Not logged in."));
425
460
  console.error(`Run: ${chalk2.cyan("jetr login")}`);
426
461
  process.exit(1);
427
462
  }
428
- let siteName = await resolveSiteName(dir, name);
429
- if (!siteName) {
430
- const dirName = basename(resolve2(dir));
431
- const { createInterface: createInterface2 } = await import("readline");
432
- const rl = createInterface2({
433
- input: process.stdin,
434
- output: process.stdout
435
- });
436
- siteName = await new Promise((res) => {
437
- rl.question(
438
- chalk2.bold(`Site name [${dirName}]: `),
439
- (answer) => {
440
- rl.close();
441
- res(answer.trim() || dirName);
442
- }
443
- );
444
- });
445
- }
446
- siteName = siteName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
447
- if (!siteName) {
448
- console.error(chalk2.red("Invalid site name"));
449
- process.exit(1);
450
- }
451
- const spinner = ora("").start();
452
- try {
453
- spinner.text = "Checking site...";
463
+ const { mode, dir, files, explicitName } = parseDeployArgs(paths);
464
+ if (mode === "directory") {
465
+ const rcDir = dir;
466
+ let siteName = await resolveSiteName(rcDir, explicitName);
467
+ if (!siteName) {
468
+ const dirName = basename2(resolve2(rcDir));
469
+ siteName = await promptLine(`Site name [${dirName}]: `) || dirName;
470
+ }
471
+ siteName = sanitizeName(siteName);
472
+ if (!siteName) {
473
+ console.error(chalk2.red("Invalid site name"));
474
+ process.exit(1);
475
+ }
476
+ const spinner = ora("").start();
477
+ try {
478
+ await ensureSiteExists(spinner, siteName);
479
+ const result = await deploy(siteName, rcDir, (msg) => {
480
+ spinner.text = msg;
481
+ });
482
+ printDeployResult(spinner, siteName, result);
483
+ const rc = loadRc(rcDir);
484
+ saveRc(rcDir, updateRc(rc, siteName));
485
+ } catch (e) {
486
+ spinner.fail(e.message);
487
+ process.exit(1);
488
+ }
489
+ } else {
490
+ console.log(chalk2.dim(`File mode: ${files.map((f) => basename2(f)).join(", ")}`));
491
+ let siteName = explicitName;
492
+ if (!siteName) {
493
+ const defaultName = basename2(files[0], ".html").replace(/\.[^.]+$/, "");
494
+ siteName = await promptLine(`Site name [${defaultName}]: `) || defaultName;
495
+ }
496
+ siteName = sanitizeName(siteName);
497
+ if (!siteName) {
498
+ console.error(chalk2.red("Invalid site name"));
499
+ process.exit(1);
500
+ }
501
+ const spinner = ora("").start();
454
502
  try {
455
- await api.getSite(siteName);
456
- } catch {
457
- spinner.text = `Creating site ${siteName}...`;
458
- await api.createSite(siteName);
503
+ await ensureSiteExists(spinner, siteName);
504
+ const result = await deployFiles(siteName, files, (msg) => {
505
+ spinner.text = msg;
506
+ });
507
+ printDeployResult(spinner, siteName, result);
508
+ } catch (e) {
509
+ spinner.fail(e.message);
510
+ process.exit(1);
459
511
  }
460
- const result = await deploy(siteName, dir, (msg) => {
461
- spinner.text = msg;
462
- });
463
- spinner.succeed("Deployed!");
464
- console.log();
465
- console.log(` ${chalk2.bold("Site:")} ${chalk2.cyan(siteName)}`);
466
- console.log(` ${chalk2.bold("URL:")} ${chalk2.green(result.url)}`);
467
- console.log(
468
- ` ${chalk2.bold("Files:")} ${result.filesUploaded} uploaded` + (result.filesDeleted > 0 ? `, ${result.filesDeleted} deleted` : "") + (result.filesUnchanged > 0 ? `, ${result.filesUnchanged} unchanged` : "") + (result.filesIgnored > 0 ? chalk2.dim(` (${result.filesIgnored} ignored)`) : "")
469
- );
470
- console.log(` ${chalk2.bold("Size:")} ${formatBytes(result.totalSize)}`);
471
- console.log();
472
- const rc = loadRc(dir);
473
- saveRc(dir, updateRc(rc, siteName));
474
- } catch (e) {
475
- spinner.fail(e.message);
476
- process.exit(1);
477
512
  }
478
513
  });
479
514
  program.command("login").description("Login with username and password").argument("[user]", "Username").argument("[password]", "Password").option("--token <token>", "Login directly with a JWT token").action(async (user, password, opts) => {
@@ -493,32 +528,7 @@ program.command("login").description("Login with username and password").argumen
493
528
  });
494
529
  }
495
530
  if (!password) {
496
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
497
- process.stdout.write("Password: ");
498
- const stdin = process.stdin;
499
- const wasRaw = stdin.isRaw;
500
- if (stdin.isTTY) stdin.setRawMode(true);
501
- password = await new Promise((res) => {
502
- let buf = "";
503
- const onData = (ch) => {
504
- const c = ch.toString();
505
- if (c === "\n" || c === "\r" || c === "") {
506
- if (stdin.isTTY) stdin.setRawMode(wasRaw ?? false);
507
- stdin.removeListener("data", onData);
508
- process.stdout.write("\n");
509
- rl.close();
510
- res(buf);
511
- } else if (c === "") {
512
- if (stdin.isTTY) stdin.setRawMode(wasRaw ?? false);
513
- process.exit(1);
514
- } else if (c === "\x7F" || c === "\b") {
515
- buf = buf.slice(0, -1);
516
- } else {
517
- buf += c;
518
- }
519
- };
520
- stdin.on("data", onData);
521
- });
531
+ password = await readSecret("Password: ");
522
532
  }
523
533
  if (!user || !password) {
524
534
  console.error(chalk2.red("Username and password required"));
@@ -693,6 +703,94 @@ tokenCmd.command("revoke").description("Revoke a share token").argument("<site>"
693
703
  process.exit(1);
694
704
  }
695
705
  });
706
+ function parseDeployArgs(paths) {
707
+ if (paths.length === 0) {
708
+ return { mode: "directory", dir: "." };
709
+ }
710
+ const first = resolve2(paths[0]);
711
+ if (existsSync4(first) && statSync2(first).isDirectory()) {
712
+ return { mode: "directory", dir: first, explicitName: paths[1] };
713
+ }
714
+ const files = [];
715
+ let explicitName;
716
+ for (const p of paths) {
717
+ const abs = resolve2(p);
718
+ if (existsSync4(abs) && statSync2(abs).isFile()) {
719
+ files.push(abs);
720
+ } else if (files.length > 0) {
721
+ explicitName = p;
722
+ break;
723
+ } else {
724
+ console.error(chalk2.red(`Not found: ${p}`));
725
+ process.exit(1);
726
+ }
727
+ }
728
+ if (files.length === 0) {
729
+ return { mode: "directory", dir: "." };
730
+ }
731
+ return { mode: "files", files, explicitName };
732
+ }
733
+ function sanitizeName(name) {
734
+ return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
735
+ }
736
+ async function promptLine(prompt) {
737
+ const { createInterface: createInterface2 } = await import("readline");
738
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
739
+ return new Promise((res) => {
740
+ rl.question(chalk2.bold(prompt), (a) => {
741
+ rl.close();
742
+ res(a.trim());
743
+ });
744
+ });
745
+ }
746
+ async function ensureSiteExists(spinner, siteName) {
747
+ spinner.text = "Checking site...";
748
+ try {
749
+ await api.getSite(siteName);
750
+ } catch {
751
+ spinner.text = `Creating site ${siteName}...`;
752
+ await api.createSite(siteName);
753
+ }
754
+ }
755
+ function printDeployResult(spinner, siteName, result) {
756
+ spinner.succeed("Deployed!");
757
+ console.log();
758
+ console.log(` ${chalk2.bold("Site:")} ${chalk2.cyan(siteName)}`);
759
+ console.log(` ${chalk2.bold("URL:")} ${chalk2.green(result.url)}`);
760
+ console.log(
761
+ ` ${chalk2.bold("Files:")} ${result.filesUploaded} uploaded` + (result.filesDeleted > 0 ? `, ${result.filesDeleted} deleted` : "") + (result.filesUnchanged > 0 ? `, ${result.filesUnchanged} unchanged` : "") + (result.filesIgnored > 0 ? chalk2.dim(` (${result.filesIgnored} ignored)`) : "")
762
+ );
763
+ console.log(` ${chalk2.bold("Size:")} ${formatBytes(result.totalSize)}`);
764
+ console.log();
765
+ }
766
+ function readSecret(prompt) {
767
+ return new Promise((resolve3) => {
768
+ process.stdout.write(prompt);
769
+ const stdin = process.stdin;
770
+ stdin.resume();
771
+ stdin.setRawMode(true);
772
+ stdin.setEncoding("utf8");
773
+ let buf = "";
774
+ const onData = (ch) => {
775
+ if (ch === "\n" || ch === "\r" || ch === "") {
776
+ stdin.setRawMode(false);
777
+ stdin.pause();
778
+ stdin.removeListener("data", onData);
779
+ process.stdout.write("\n");
780
+ resolve3(buf);
781
+ } else if (ch === "") {
782
+ stdin.setRawMode(false);
783
+ process.stdout.write("\n");
784
+ process.exit(1);
785
+ } else if (ch === "\x7F" || ch === "\b") {
786
+ buf = buf.slice(0, -1);
787
+ } else {
788
+ buf += ch;
789
+ }
790
+ };
791
+ stdin.on("data", onData);
792
+ });
793
+ }
696
794
  function formatBytes(bytes) {
697
795
  if (bytes === 0) return "0 B";
698
796
  const units = ["B", "KB", "MB", "GB"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@within-7/jetr",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "CLI for Jetr static site hosting",
5
5
  "type": "module",
6
6
  "bin": {