coderemote 0.1.0 → 0.1.2

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/Tranthanh98/vscoderemote/main/apps/ios/Design/AppIcon/Icon-Default-1024x1024@1x.png" width="96" height="96" alt="Code Remote icon">
2
+ <img src="https://raw.githubusercontent.com/Tranthanh98/coderemote/main/apps/ios/Design/AppIcon/Icon-Default-1024x1024@1x.png" width="96" height="96" alt="Code Remote icon">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">coderemote</h1>
@@ -12,7 +12,7 @@
12
12
 
13
13
  <p align="center">
14
14
  <a href="https://www.npmjs.com/package/coderemote"><img src="https://img.shields.io/npm/v/coderemote" alt="npm version"></a>
15
- <a href="https://github.com/Tranthanh98/vscoderemote/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/coderemote" alt="license: AGPL-3.0-or-later"></a>
15
+ <a href="https://github.com/Tranthanh98/coderemote/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/coderemote" alt="license: AGPL-3.0-or-later"></a>
16
16
  <img src="https://img.shields.io/node/v/coderemote" alt="node version">
17
17
  </p>
18
18
 
@@ -33,21 +33,20 @@ in your pocket:
33
33
 
34
34
  ```sh
35
35
  npm install -g coderemote
36
- brew install --cask devtunnel # Microsoft Dev Tunnels CLI (Linux: see below)
37
- devtunnel user login -g # once, sign in with GitHub (-d for a Microsoft account)
38
-
39
- cd ~/projects/my-app
40
- coderemote up
36
+ cd ~/projects/my-app && coderemote up
41
37
  ```
42
38
 
43
- `coderemote up` prints a QR code. Scan it with the iPhone Camera (or the **+** button in the
44
- app) to pair. Done.
39
+ Scan the QR code it prints with your iPhone Camera (or the **+** button in the app). Done.
40
+
41
+ The first run sets itself up: it downloads Microsoft's Dev Tunnels CLI (once, verified) and
42
+ opens your browser to sign in with GitHub. Prefer a Microsoft account? Run
43
+ `coderemote login --microsoft` first.
45
44
 
46
45
  ## How it works
47
46
 
48
- ```
49
- iPhone app ──HTTPS / WSS──► Microsoft Dev Tunnels relay ◄── coderemote (your computer)
50
- ```
47
+ <p align="center">
48
+ <img src="https://raw.githubusercontent.com/Tranthanh98/coderemote/main/docs/images/how-it-works.png" width="760" alt="How it works: the Code Remote app on the iPhone connects over HTTPS/WSS to the Microsoft Dev Tunnels relay; coderemote on your computer (127.0.0.1:31580) connects outbound to the same relay and serves files, git and terminals from the shared folder. Security at each hop: token in the iPhone Keychain, only your account through the relay, token check and one folder on the computer.">
49
+ </p>
51
50
 
52
51
  `coderemote` starts a small agent that listens on `127.0.0.1` only, and publishes it through
53
52
  a [Microsoft Dev Tunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/overview).
@@ -70,7 +69,7 @@ Remote shell access is serious, so access is locked down by default:
70
69
 
71
70
  `coderemote up --allow-anonymous` skips the account sign-in when you want the pairing token
72
71
  alone. Found a vulnerability? See the
73
- [security policy](https://github.com/Tranthanh98/vscoderemote/blob/main/SECURITY.md).
72
+ [security policy](https://github.com/Tranthanh98/coderemote/blob/main/SECURITY.md).
74
73
 
75
74
  ## Commands
76
75
 
@@ -83,6 +82,7 @@ coderemote kill --all # end every session
83
82
  coderemote stop # stop the agent for this folder (--all: every agent)
84
83
  coderemote serve # share on 127.0.0.1 only, no tunnel (local testing)
85
84
  coderemote rotate-token # revoke every paired device
85
+ coderemote login # sign in again (--microsoft, or --device-code without a browser)
86
86
  coderemote help # all options
87
87
  ```
88
88
 
@@ -92,15 +92,14 @@ Options: `--root DIR` shares another folder; `--port N` picks the local port (de
92
92
  ## Requirements
93
93
 
94
94
  - **Node.js 20** or newer on **macOS** (Linux is expected to work but not yet tested)
95
- - The **devtunnel CLI**:
96
- - macOS: `brew install --cask devtunnel`
97
- - Linux: `curl -sL https://aka.ms/DevTunnelCliInstall | bash`
98
- - more options in [Microsoft's guide](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started)
99
- - A GitHub or Microsoft account for `devtunnel user login`
100
- - On Linux, `node-pty` compiles on install: have `python3`, `make` and a C++ compiler
101
- (`build-essential` on Debian/Ubuntu)
95
+ - A **GitHub or Microsoft account** to sign in to Dev Tunnels
96
+ - The **Dev Tunnels CLI** is downloaded on first run into `~/.coderemote/bin` (the macOS build
97
+ must carry Microsoft's code signature). A `devtunnel` already on your `PATH` is used instead,
98
+ or point `CODEREMOTE_DEVTUNNEL` at one.
99
+ - On Linux, `node-pty` compiles on install (`python3`, `make`, a C++ compiler:
100
+ `build-essential` on Debian/Ubuntu), and signing in needs `libsecret` (`libsecret-1-0`)
102
101
  - The **Code Remote** iOS app on your iPhone (App Store release coming; meanwhile build it
103
- from [source](https://github.com/Tranthanh98/vscoderemote#build-the-ios-app))
102
+ from [source](https://github.com/Tranthanh98/coderemote#build-the-ios-app))
104
103
 
105
104
  ## FAQ
106
105
 
@@ -121,6 +120,6 @@ repository inside it.
121
120
 
122
121
  ## License
123
122
 
124
- [AGPL-3.0-or-later](https://github.com/Tranthanh98/vscoderemote/blob/main/LICENSE).
123
+ [AGPL-3.0-or-later](https://github.com/Tranthanh98/coderemote/blob/main/LICENSE).
125
124
  "Code Remote" and its icon are trademarks of the project; see
126
- [TRADEMARKS.md](https://github.com/Tranthanh98/vscoderemote/blob/main/TRADEMARKS.md).
125
+ [TRADEMARKS.md](https://github.com/Tranthanh98/coderemote/blob/main/TRADEMARKS.md).
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/coderemote.js
4
- import { execFile as execFile2, spawn } from "node:child_process";
4
+ import { execFile as execFile3, spawn } from "node:child_process";
5
5
  import { randomBytes } from "node:crypto";
6
- import fs5 from "node:fs";
6
+ import fs6 from "node:fs";
7
7
  import os3 from "node:os";
8
- import path5 from "node:path";
9
- import { promisify } from "node:util";
8
+ import path6 from "node:path";
9
+ import { promisify as promisify2 } from "node:util";
10
10
  import qrcode from "qrcode-terminal";
11
11
 
12
12
  // ../../packages/agent-core/src/server.js
@@ -713,20 +713,129 @@ function createAgentServer({ token, root: root2, tunnelToken = async () => null
713
713
  return server;
714
714
  }
715
715
 
716
- // src/registry.js
716
+ // src/devtunnel.js
717
+ import { execFile as execFile2 } from "node:child_process";
718
+ import { createHash } from "node:crypto";
717
719
  import fs4 from "node:fs";
718
720
  import path4 from "node:path";
721
+ import { promisify } from "node:util";
722
+ var run = promisify(execFile2);
723
+ var DOWNLOAD_BASE = "https://tunnelsassetsprod.blob.core.windows.net/cli";
724
+ var BUILDS = {
725
+ "darwin-arm64": "osx-arm64-devtunnel",
726
+ "darwin-x64": "osx-x64-devtunnel",
727
+ "linux-arm64": "linux-arm64-devtunnel",
728
+ "linux-x64": "linux-x64-devtunnel"
729
+ };
730
+ var MICROSOFT_TEAM_ID = "UBF8T346G9";
731
+ var REFRESH_INTERVAL_MS = 24 * 36e5;
732
+ function executable(file) {
733
+ try {
734
+ fs4.accessSync(file, fs4.constants.X_OK);
735
+ return fs4.statSync(file).isFile();
736
+ } catch {
737
+ return false;
738
+ }
739
+ }
740
+ function onPath(name) {
741
+ for (const dir of (process.env.PATH || "").split(path4.delimiter)) {
742
+ if (dir && executable(path4.join(dir, name))) return path4.join(dir, name);
743
+ }
744
+ return null;
745
+ }
746
+ async function ensureDevtunnel({ dir, progress }) {
747
+ const override = process.env.CODEREMOTE_DEVTUNNEL;
748
+ if (override) {
749
+ if (executable(override)) return override;
750
+ throw new Error(`CODEREMOTE_DEVTUNNEL=${override} isn't an executable file.`);
751
+ }
752
+ const fromPath = onPath("devtunnel");
753
+ if (fromPath) return fromPath;
754
+ const build = BUILDS[`${process.platform}-${process.arch}`];
755
+ if (!build) {
756
+ throw new Error(
757
+ `No devtunnel build for ${process.platform}-${process.arch}. Install it yourself: https://learn.microsoft.com/azure/developer/dev-tunnels/get-started`
758
+ );
759
+ }
760
+ const bin = path4.join(dir, "devtunnel");
761
+ const metaFile = path4.join(dir, "devtunnel.json");
762
+ let meta = {};
763
+ try {
764
+ meta = JSON.parse(fs4.readFileSync(metaFile, "utf8"));
765
+ } catch {
766
+ }
767
+ const installed = executable(bin);
768
+ if (installed && Date.now() - (meta.checkedAt ?? 0) < REFRESH_INTERVAL_MS) return bin;
769
+ const url = `${DOWNLOAD_BASE}/${build}`;
770
+ if (installed) {
771
+ try {
772
+ const head = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(3e3) });
773
+ const lastModified = head.headers.get("last-modified");
774
+ if (head.ok && lastModified && lastModified === meta.lastModified) {
775
+ fs4.writeFileSync(metaFile, JSON.stringify({ ...meta, checkedAt: Date.now() }));
776
+ return bin;
777
+ }
778
+ } catch {
779
+ return bin;
780
+ }
781
+ }
782
+ await download(url, bin, metaFile, progress);
783
+ return bin;
784
+ }
785
+ async function download(url, bin, metaFile, progress) {
786
+ fs4.mkdirSync(path4.dirname(bin), { recursive: true, mode: 448 });
787
+ const res = await fetch(url, { signal: AbortSignal.timeout(12e4) });
788
+ if (!res.ok || !res.body) throw new Error(`Downloading devtunnel failed: HTTP ${res.status} from ${url}`);
789
+ const total = Number(res.headers.get("content-length")) || 0;
790
+ const tmp = `${bin}.${process.pid}.download`;
791
+ const out = fs4.createWriteStream(tmp, { mode: 493 });
792
+ const md5 = createHash("md5");
793
+ let received = 0;
794
+ try {
795
+ for await (const chunk of res.body) {
796
+ md5.update(chunk);
797
+ received += chunk.length;
798
+ if (!out.write(chunk)) await new Promise((r) => out.once("drain", r));
799
+ if (total) progress(`${Math.floor(received / total * 100)}%`);
800
+ }
801
+ await new Promise((resolve, reject) => out.end((e) => e ? reject(e) : resolve()));
802
+ if (total && received !== total) throw new Error(`devtunnel download was cut short (${received} of ${total} bytes)`);
803
+ await verify(tmp, md5.digest("base64"), res.headers.get("content-md5"));
804
+ fs4.chmodSync(tmp, 493);
805
+ fs4.renameSync(tmp, bin);
806
+ } catch (e) {
807
+ out.destroy();
808
+ fs4.rmSync(tmp, { force: true });
809
+ throw e;
810
+ }
811
+ fs4.writeFileSync(metaFile, JSON.stringify({ lastModified: res.headers.get("last-modified"), checkedAt: Date.now() }));
812
+ }
813
+ async function verify(file, md5, expectedMd5) {
814
+ if (expectedMd5 && md5 !== expectedMd5) throw new Error("devtunnel download is corrupt (MD5 mismatch)");
815
+ if (process.platform !== "darwin") return;
816
+ try {
817
+ await run("codesign", ["--verify", "--strict", file]);
818
+ const { stderr } = await run("codesign", ["-dv", file]);
819
+ if (!stderr.includes(`TeamIdentifier=${MICROSOFT_TEAM_ID}`)) throw new Error("not signed by Microsoft");
820
+ } catch (e) {
821
+ throw new Error(`devtunnel download failed its signature check: ${e.message}`);
822
+ }
823
+ }
824
+
825
+ // src/registry.js
826
+ import fs5 from "node:fs";
827
+ import path5 from "node:path";
719
828
  var AgentRegistry = class {
720
829
  constructor(dir) {
721
830
  this.dir = dir;
722
831
  }
723
832
  register({ port: port2, root: root2, url }) {
724
- fs4.mkdirSync(this.dir, { recursive: true, mode: 448 });
833
+ fs5.mkdirSync(this.dir, { recursive: true, mode: 448 });
725
834
  const file = this.#file(port2);
726
- fs4.writeFileSync(file, JSON.stringify({ pid: process.pid, port: port2, root: root2, url, startedAt: Date.now() }), { mode: 384 });
835
+ fs5.writeFileSync(file, JSON.stringify({ pid: process.pid, port: port2, root: root2, url, startedAt: Date.now() }), { mode: 384 });
727
836
  const cleanup = () => {
728
837
  try {
729
- if (JSON.parse(fs4.readFileSync(file, "utf8")).pid === process.pid) fs4.rmSync(file);
838
+ if (JSON.parse(fs5.readFileSync(file, "utf8")).pid === process.pid) fs5.rmSync(file);
730
839
  } catch {
731
840
  }
732
841
  };
@@ -737,25 +846,25 @@ var AgentRegistry = class {
737
846
  list() {
738
847
  let names = [];
739
848
  try {
740
- names = fs4.readdirSync(this.dir).filter((n) => n.endsWith(".json"));
849
+ names = fs5.readdirSync(this.dir).filter((n) => n.endsWith(".json"));
741
850
  } catch {
742
851
  return [];
743
852
  }
744
853
  const agents = [];
745
854
  for (const name of names) {
746
- const file = path4.join(this.dir, name);
855
+ const file = path5.join(this.dir, name);
747
856
  try {
748
- const agent = JSON.parse(fs4.readFileSync(file, "utf8"));
857
+ const agent = JSON.parse(fs5.readFileSync(file, "utf8"));
749
858
  process.kill(agent.pid, 0);
750
859
  agents.push(agent);
751
860
  } catch {
752
- fs4.rmSync(file, { force: true });
861
+ fs5.rmSync(file, { force: true });
753
862
  }
754
863
  }
755
864
  return agents.sort((a, b) => a.port - b.port);
756
865
  }
757
866
  #file(port2) {
758
- return path4.join(this.dir, `${port2}.json`);
867
+ return path5.join(this.dir, `${port2}.json`);
759
868
  }
760
869
  };
761
870
 
@@ -775,6 +884,7 @@ var Spinner = class {
775
884
  start(text) {
776
885
  this.stop();
777
886
  this.text = text;
887
+ this.detail = "";
778
888
  this.startedAt = Date.now();
779
889
  if (!this.tty) {
780
890
  this.stream.write(` ${text}...
@@ -791,6 +901,10 @@ var Spinner = class {
791
901
  this.render(FRAMES[0]);
792
902
  this.timer = setInterval(() => this.render(FRAMES[++frame % FRAMES.length]), 80);
793
903
  }
904
+ /** Extra status shown after the step's text while it runs, e.g. a download percentage. */
905
+ update(detail) {
906
+ this.detail = detail;
907
+ }
794
908
  succeed() {
795
909
  this.finish(green("\u2714"));
796
910
  }
@@ -807,7 +921,8 @@ var Spinner = class {
807
921
  this.text = null;
808
922
  }
809
923
  render(frame) {
810
- this.stream.write(`\r\x1B[2K ${cyan(frame)} ${this.text} ${dim(this.elapsed())}`);
924
+ const detail = this.detail ? ` ${this.detail}` : "";
925
+ this.stream.write(`\r\x1B[2K ${cyan(frame)} ${this.text}${detail} ${dim(this.elapsed())}`);
811
926
  }
812
927
  finish(mark) {
813
928
  if (!this.text) return;
@@ -823,17 +938,17 @@ var Spinner = class {
823
938
  };
824
939
 
825
940
  // bin/coderemote.js
826
- var run = promisify(execFile2);
827
- var CONFIG_DIR = path5.join(os3.homedir(), ".coderemote");
828
- var LEGACY_CONFIG_DIR = path5.join(os3.homedir(), ".vsremote");
829
- if (!fs5.existsSync(CONFIG_DIR) && fs5.existsSync(LEGACY_CONFIG_DIR)) fs5.renameSync(LEGACY_CONFIG_DIR, CONFIG_DIR);
830
- var CONFIG_FILE = path5.join(CONFIG_DIR, "config.json");
941
+ var run2 = promisify2(execFile3);
942
+ var CONFIG_DIR = path6.join(os3.homedir(), ".coderemote");
943
+ var LEGACY_CONFIG_DIR = path6.join(os3.homedir(), ".vsremote");
944
+ if (!fs6.existsSync(CONFIG_DIR) && fs6.existsSync(LEGACY_CONFIG_DIR)) fs6.renameSync(LEGACY_CONFIG_DIR, CONFIG_DIR);
945
+ var CONFIG_FILE = path6.join(CONFIG_DIR, "config.json");
831
946
  var DEFAULT_PORT = 31580;
832
947
  var spinner = new Spinner();
833
948
  function loadConfig() {
834
949
  let cfg2 = {};
835
950
  try {
836
- cfg2 = JSON.parse(fs5.readFileSync(CONFIG_FILE, "utf8"));
951
+ cfg2 = JSON.parse(fs6.readFileSync(CONFIG_FILE, "utf8"));
837
952
  } catch {
838
953
  }
839
954
  if (!cfg2.token) {
@@ -843,8 +958,8 @@ function loadConfig() {
843
958
  return cfg2;
844
959
  }
845
960
  function saveConfig(cfg2) {
846
- fs5.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
847
- fs5.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
961
+ fs6.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
962
+ fs6.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
848
963
  }
849
964
  function listen(token, port2, root2, tunnelToken) {
850
965
  const server = createAgentServer({ token, root: root2, tunnelToken });
@@ -853,33 +968,51 @@ function listen(token, port2, root2, tunnelToken) {
853
968
  server.listen(port2, "127.0.0.1", () => resolve(server));
854
969
  });
855
970
  }
971
+ var DEVTUNNEL = "devtunnel";
972
+ async function resolveDevtunnel() {
973
+ let installing = false;
974
+ DEVTUNNEL = await ensureDevtunnel({
975
+ dir: path6.join(CONFIG_DIR, "bin"),
976
+ progress: (detail) => {
977
+ if (!installing) {
978
+ installing = true;
979
+ spinner.start("Installing devtunnel (Microsoft Dev Tunnels CLI, one time)");
980
+ }
981
+ spinner.update(detail);
982
+ }
983
+ });
984
+ if (installing) spinner.succeed();
985
+ }
856
986
  async function devtunnel(...args) {
857
987
  try {
858
- const { stdout } = await run("devtunnel", args);
988
+ const { stdout } = await run2(DEVTUNNEL, args);
859
989
  return { ok: true, out: stdout };
860
990
  } catch (e) {
861
- if (e.code === "ENOENT") {
862
- spinner.stop();
863
- console.error("devtunnel CLI not found. Install it: brew install --cask devtunnel");
864
- process.exit(1);
865
- }
866
- return { ok: false, out: `${e.stdout || ""}${e.stderr || ""}` };
991
+ return { ok: false, out: `${e.stdout || ""}${e.stderr || ""}` || e.message };
867
992
  }
868
993
  }
994
+ function loginArgs(opts = rest) {
995
+ const args = ["user", "login"];
996
+ if (!opts.includes("--microsoft")) args.push("-g");
997
+ if (opts.includes("--device-code")) args.push("-d");
998
+ return args;
999
+ }
1000
+ function interactiveLogin(args) {
1001
+ return new Promise((resolve) => {
1002
+ spawn(DEVTUNNEL, args, { stdio: "inherit" }).on("exit", resolve);
1003
+ });
1004
+ }
869
1005
  var LOGIN_NEEDED = /not logged in|login token expired|login required|please log ?in/i;
870
1006
  async function login(reason) {
871
1007
  spinner.stop();
872
1008
  console.error(`Dev tunnels: ${reason.trim()}`);
873
1009
  if (!process.stdin.isTTY) {
874
- console.error("Run: devtunnel user login -g (or -d for a Microsoft account), then try again.");
1010
+ console.error("Run `coderemote login` (--microsoft for a Microsoft account), then try again.");
875
1011
  process.exit(1);
876
1012
  }
877
- console.error("Signing in again with GitHub (devtunnel user login -g)...\n");
878
- const code = await new Promise((resolve) => {
879
- spawn("devtunnel", ["user", "login", "-g"], { stdio: "inherit" }).on("exit", resolve);
880
- });
881
- if (code !== 0) {
882
- console.error("\nLogin failed. Run `devtunnel user login -g` (or -d) yourself, then try again.");
1013
+ console.error("Signing in with GitHub (use `coderemote login --microsoft` for a Microsoft account)...\n");
1014
+ if (await interactiveLogin(loginArgs([])) !== 0) {
1015
+ console.error("\nLogin failed. Run `coderemote login` (--microsoft, --device-code), then try again.");
883
1016
  process.exit(1);
884
1017
  }
885
1018
  }
@@ -935,7 +1068,7 @@ ${res.out}`);
935
1068
  }
936
1069
  function hostTunnel(tunnelId, port2) {
937
1070
  return new Promise((resolve, reject) => {
938
- const child = spawn("devtunnel", ["host", tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
1071
+ const child = spawn(DEVTUNNEL, ["host", tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
939
1072
  let log2 = "";
940
1073
  const onData = (d) => {
941
1074
  log2 += d;
@@ -971,6 +1104,8 @@ var USAGE = `Usage: coderemote <command> [options]
971
1104
  kill <id>... | --all kill terminal sessions (an id prefix is enough)
972
1105
  stop [--all] stop the agent for this folder (or every agent)
973
1106
  rotate-token revoke every paired device
1107
+ login sign in to Microsoft Dev Tunnels with GitHub (up does this when needed);
1108
+ --microsoft for a Microsoft account, --device-code without a browser
974
1109
 
975
1110
  Options:
976
1111
  --root DIR folder to share (default: current folder)
@@ -986,9 +1121,9 @@ var flag = (name) => {
986
1121
  };
987
1122
  var anonymous = rest.includes("--allow-anonymous") || rest.includes("--no-auth");
988
1123
  var port = flag("--port") ? parseInt(flag("--port"), 10) : DEFAULT_PORT;
989
- var root = path5.resolve(flag("--root") ?? process.cwd());
1124
+ var root = path6.resolve(flag("--root") ?? process.cwd());
990
1125
  var cfg = loadConfig();
991
- var registry = new AgentRegistry(path5.join(CONFIG_DIR, "agents"));
1126
+ var registry = new AgentRegistry(path6.join(CONFIG_DIR, "agents"));
992
1127
  async function start({ tunnel }) {
993
1128
  const running = registry.list().find((a) => a.port === port);
994
1129
  if (running) {
@@ -1015,6 +1150,7 @@ async function start({ tunnel }) {
1015
1150
  let url = `http://127.0.0.1:${port}`;
1016
1151
  if (tunnel) {
1017
1152
  spinner.succeed();
1153
+ await resolveDevtunnel();
1018
1154
  spinner.start("Checking dev tunnels sign-in");
1019
1155
  const user = await tunnelUser();
1020
1156
  spinner.succeed();
@@ -1042,6 +1178,8 @@ async function start({ tunnel }) {
1042
1178
  };
1043
1179
  process.on("SIGINT", shutdown);
1044
1180
  process.on("SIGTERM", shutdown);
1181
+ process.on("SIGHUP", shutdown);
1182
+ process.on("exit", () => child?.kill());
1045
1183
  return { url, access };
1046
1184
  }
1047
1185
  async function agentApi(agent, method, route, query = {}) {
@@ -1151,6 +1289,10 @@ Token: ${cfg.token}`);
1151
1289
  console.log("Token rotated. Re-pair your devices, and restart running agents.");
1152
1290
  break;
1153
1291
  }
1292
+ case "login": {
1293
+ await resolveDevtunnel();
1294
+ process.exit(await interactiveLogin(loginArgs()));
1295
+ }
1154
1296
  case "help":
1155
1297
  console.log(USAGE);
1156
1298
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderemote",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Use your computer from your iPhone: files, git and a real terminal over a secure Microsoft Dev Tunnel. No SSH, no port forwarding, no VPN.",
5
5
  "keywords": [
6
6
  "remote",
@@ -21,13 +21,13 @@
21
21
  "vscode",
22
22
  "code-remote"
23
23
  ],
24
- "homepage": "https://github.com/Tranthanh98/vscoderemote#readme",
24
+ "homepage": "https://github.com/Tranthanh98/coderemote#readme",
25
25
  "bugs": {
26
- "url": "https://github.com/Tranthanh98/vscoderemote/issues"
26
+ "url": "https://github.com/Tranthanh98/coderemote/issues"
27
27
  },
28
28
  "repository": {
29
29
  "type": "git",
30
- "url": "git+https://github.com/Tranthanh98/vscoderemote.git",
30
+ "url": "git+https://github.com/Tranthanh98/coderemote.git",
31
31
  "directory": "apps/agent"
32
32
  },
33
33
  "license": "AGPL-3.0-or-later",