@tongil_kim/clautunnel 1.1.0 → 1.3.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.
- package/README.md +9 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.js +424 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,15 +26,21 @@ clautunnel setup
|
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
You'll need:
|
|
29
|
-
- **Supabase Project
|
|
30
|
-
- **Supabase Anon Key**: Dashboard → Settings → API →
|
|
29
|
+
- **Supabase Project ID**: Dashboard → Settings → General → Project ID
|
|
30
|
+
- **Supabase Anon Key**: Dashboard → Settings → API Keys → Legacy anon Tab → Copy anon key
|
|
31
31
|
|
|
32
32
|
## Usage
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
#
|
|
35
|
+
# Create account (first time)
|
|
36
|
+
clautunnel signup
|
|
37
|
+
|
|
38
|
+
# Login (returning user)
|
|
36
39
|
clautunnel login
|
|
37
40
|
|
|
41
|
+
# Logout
|
|
42
|
+
clautunnel logout
|
|
43
|
+
|
|
38
44
|
# Start a session
|
|
39
45
|
clautunnel start
|
|
40
46
|
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ interface ConfigData {
|
|
|
11
11
|
};
|
|
12
12
|
supabaseUrl?: string;
|
|
13
13
|
supabaseAnonKey?: string;
|
|
14
|
+
mobileProjectPath?: string;
|
|
14
15
|
}
|
|
15
16
|
declare class Config {
|
|
16
17
|
private configDir;
|
|
@@ -37,6 +38,8 @@ declare class Config {
|
|
|
37
38
|
refreshToken: string;
|
|
38
39
|
}): void;
|
|
39
40
|
clearSessionTokens(): void;
|
|
41
|
+
getMobileProjectPath(): string | undefined;
|
|
42
|
+
setMobileProjectPath(path: string): void;
|
|
40
43
|
}
|
|
41
44
|
declare function getConfig(): Config;
|
|
42
45
|
|
package/dist/index.js
CHANGED
|
@@ -236,7 +236,7 @@ var Config = class {
|
|
|
236
236
|
requireConfiguration() {
|
|
237
237
|
if (!this.isConfigured()) {
|
|
238
238
|
throw new ConfigurationError(
|
|
239
|
-
'ClauTunnel is not configured.\n\
|
|
239
|
+
'ClauTunnel is not configured.\n\nRun "clautunnel setup" to configure your Supabase credentials.\n\nOr set environment variables in your shell profile (~/.zshrc or ~/.bashrc):\n export SUPABASE_URL=https://<project-id>.supabase.co\n export SUPABASE_ANON_KEY=<your-anon-key>'
|
|
240
240
|
);
|
|
241
241
|
}
|
|
242
242
|
}
|
|
@@ -262,6 +262,13 @@ var Config = class {
|
|
|
262
262
|
delete this.data.sessionTokens;
|
|
263
263
|
this.saveConfig();
|
|
264
264
|
}
|
|
265
|
+
getMobileProjectPath() {
|
|
266
|
+
return this.data.mobileProjectPath;
|
|
267
|
+
}
|
|
268
|
+
setMobileProjectPath(path4) {
|
|
269
|
+
this.data.mobileProjectPath = path4;
|
|
270
|
+
this.saveConfig();
|
|
271
|
+
}
|
|
265
272
|
};
|
|
266
273
|
var defaultConfig = null;
|
|
267
274
|
function getConfig() {
|
|
@@ -325,20 +332,22 @@ import { EventEmitter } from "events";
|
|
|
325
332
|
var DEFAULT_TIMEOUT = 1e4;
|
|
326
333
|
function subscribeWithTimeout(channel, channelName, timeout = DEFAULT_TIMEOUT) {
|
|
327
334
|
return new Promise((resolve3) => {
|
|
335
|
+
let lastStatus = "unknown";
|
|
328
336
|
const timer = setTimeout(() => {
|
|
329
337
|
console.warn(
|
|
330
|
-
`[WARN] Realtime subscription timeout for ${channelName}
|
|
338
|
+
`[WARN] Realtime subscription timeout for ${channelName} (last status: ${lastStatus}).`
|
|
331
339
|
);
|
|
332
340
|
resolve3(false);
|
|
333
341
|
}, timeout);
|
|
334
342
|
channel.subscribe((status, err) => {
|
|
343
|
+
lastStatus = status;
|
|
335
344
|
if (status === "SUBSCRIBED") {
|
|
336
345
|
clearTimeout(timer);
|
|
337
346
|
resolve3(true);
|
|
338
347
|
} else if (status === "CHANNEL_ERROR" || status === "CLOSED" || status === "TIMED_OUT") {
|
|
339
348
|
clearTimeout(timer);
|
|
340
349
|
console.warn(
|
|
341
|
-
`[WARN] Channel ${channelName} ${status.toLowerCase()}
|
|
350
|
+
`[WARN] Channel ${channelName} ${status.toLowerCase()}.`
|
|
342
351
|
);
|
|
343
352
|
if (err) {
|
|
344
353
|
console.warn(`[WARN] Error details: ${err.message || err}`);
|
|
@@ -2301,7 +2310,7 @@ ${confirmationMsg}
|
|
|
2301
2310
|
};
|
|
2302
2311
|
|
|
2303
2312
|
// src/index.ts
|
|
2304
|
-
import { Command as
|
|
2313
|
+
import { Command as Command9 } from "commander";
|
|
2305
2314
|
|
|
2306
2315
|
// src/commands/start.ts
|
|
2307
2316
|
import { Command } from "commander";
|
|
@@ -2604,13 +2613,344 @@ function getFullDiskAccessStatus(enabled, terminalApp) {
|
|
|
2604
2613
|
};
|
|
2605
2614
|
}
|
|
2606
2615
|
|
|
2616
|
+
// src/mobile/mobile-server.ts
|
|
2617
|
+
import { spawn as spawn2, execSync as execSync2 } from "child_process";
|
|
2618
|
+
import {
|
|
2619
|
+
existsSync as existsSync4,
|
|
2620
|
+
mkdirSync as mkdirSync3,
|
|
2621
|
+
writeFileSync as writeFileSync3,
|
|
2622
|
+
createWriteStream
|
|
2623
|
+
} from "fs";
|
|
2624
|
+
import { join as join5 } from "path";
|
|
2625
|
+
import { homedir as homedir5 } from "os";
|
|
2626
|
+
import { get } from "http";
|
|
2627
|
+
var REPO_URL = "https://github.com/TongilKim/ClauTunnel.git";
|
|
2628
|
+
var DEFAULT_MOBILE_DIR = join5(homedir5(), ".clautunnel", "mobile");
|
|
2629
|
+
var MobileServerManager = class {
|
|
2630
|
+
options;
|
|
2631
|
+
mobileProjectPath;
|
|
2632
|
+
logDir;
|
|
2633
|
+
expoPort;
|
|
2634
|
+
ngrokProcess = null;
|
|
2635
|
+
expoProcess = null;
|
|
2636
|
+
ngrokLogStream = null;
|
|
2637
|
+
expoLogStream = null;
|
|
2638
|
+
tunnelUrl = null;
|
|
2639
|
+
onProgress;
|
|
2640
|
+
constructor(options) {
|
|
2641
|
+
this.options = options;
|
|
2642
|
+
this.mobileProjectPath = options.mobileProjectPath ?? DEFAULT_MOBILE_DIR;
|
|
2643
|
+
this.expoPort = options.expoPort ?? 8081;
|
|
2644
|
+
this.logDir = options.logDir ?? join5(homedir5(), ".clautunnel", "logs");
|
|
2645
|
+
this.onProgress = options.onProgress ?? (() => {
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
getMobileProjectPath() {
|
|
2649
|
+
return this.mobileProjectPath;
|
|
2650
|
+
}
|
|
2651
|
+
checkPrerequisites() {
|
|
2652
|
+
const issues = [];
|
|
2653
|
+
let needsInstall = false;
|
|
2654
|
+
try {
|
|
2655
|
+
execSync2("which ngrok", { stdio: "pipe" });
|
|
2656
|
+
} catch {
|
|
2657
|
+
issues.push(
|
|
2658
|
+
"ngrok is not installed.\n Install: brew install ngrok\n Sign up: https://ngrok.com\n Auth: ngrok config add-authtoken <your-token>"
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
const nodeModulesPath = join5(this.mobileProjectPath, "node_modules");
|
|
2662
|
+
if (!existsSync4(nodeModulesPath)) {
|
|
2663
|
+
needsInstall = true;
|
|
2664
|
+
}
|
|
2665
|
+
return { ready: issues.length === 0, issues, needsInstall };
|
|
2666
|
+
}
|
|
2667
|
+
ensureMobileProject() {
|
|
2668
|
+
const packageJson2 = join5(this.mobileProjectPath, "package.json");
|
|
2669
|
+
if (existsSync4(packageJson2)) {
|
|
2670
|
+
return { ready: true, cloned: false };
|
|
2671
|
+
}
|
|
2672
|
+
if (this.mobileProjectPath && !existsSync4(this.mobileProjectPath)) {
|
|
2673
|
+
return {
|
|
2674
|
+
ready: false,
|
|
2675
|
+
cloned: false,
|
|
2676
|
+
error: `Mobile project path not found: ${this.mobileProjectPath}`
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
try {
|
|
2680
|
+
execSync2("which git", { stdio: "pipe" });
|
|
2681
|
+
} catch {
|
|
2682
|
+
return {
|
|
2683
|
+
ready: false,
|
|
2684
|
+
cloned: false,
|
|
2685
|
+
error: "git is required to download the mobile app.\n Install: https://git-scm.com/downloads\n macOS: xcode-select --install"
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
try {
|
|
2689
|
+
const clautunnelDir = join5(homedir5(), ".clautunnel");
|
|
2690
|
+
if (!existsSync4(clautunnelDir)) {
|
|
2691
|
+
mkdirSync3(clautunnelDir, { recursive: true });
|
|
2692
|
+
}
|
|
2693
|
+
const repoDir = join5(clautunnelDir, "repo");
|
|
2694
|
+
if (existsSync4(repoDir)) {
|
|
2695
|
+
execSync2("git pull --ff-only", {
|
|
2696
|
+
cwd: repoDir,
|
|
2697
|
+
stdio: "pipe",
|
|
2698
|
+
timeout: 6e4
|
|
2699
|
+
});
|
|
2700
|
+
} else {
|
|
2701
|
+
execSync2(
|
|
2702
|
+
`git clone --depth 1 --filter=blob:none --sparse "${REPO_URL}" repo`,
|
|
2703
|
+
{
|
|
2704
|
+
cwd: clautunnelDir,
|
|
2705
|
+
stdio: "pipe",
|
|
2706
|
+
timeout: 6e4
|
|
2707
|
+
}
|
|
2708
|
+
);
|
|
2709
|
+
execSync2("git sparse-checkout set apps/mobile", {
|
|
2710
|
+
cwd: repoDir,
|
|
2711
|
+
stdio: "pipe",
|
|
2712
|
+
timeout: 3e4
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
this.mobileProjectPath = join5(repoDir, "apps", "mobile");
|
|
2716
|
+
if (!existsSync4(join5(this.mobileProjectPath, "package.json"))) {
|
|
2717
|
+
return { ready: false, cloned: false, error: "Clone succeeded but apps/mobile not found" };
|
|
2718
|
+
}
|
|
2719
|
+
return { ready: true, cloned: true };
|
|
2720
|
+
} catch (error) {
|
|
2721
|
+
return {
|
|
2722
|
+
ready: false,
|
|
2723
|
+
cloned: false,
|
|
2724
|
+
error: `Failed to clone mobile project: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
ensureEnvFile() {
|
|
2729
|
+
const envPath = join5(this.mobileProjectPath, ".env");
|
|
2730
|
+
const envContent = [
|
|
2731
|
+
`EXPO_PUBLIC_SUPABASE_URL=${this.options.supabaseUrl}`,
|
|
2732
|
+
`EXPO_PUBLIC_SUPABASE_ANON_KEY=${this.options.supabaseAnonKey}`,
|
|
2733
|
+
""
|
|
2734
|
+
].join("\n");
|
|
2735
|
+
writeFileSync3(envPath, envContent);
|
|
2736
|
+
}
|
|
2737
|
+
installDependencies() {
|
|
2738
|
+
const nodeModulesPath = join5(this.mobileProjectPath, "node_modules");
|
|
2739
|
+
if (existsSync4(nodeModulesPath)) return true;
|
|
2740
|
+
try {
|
|
2741
|
+
execSync2("pnpm install", {
|
|
2742
|
+
cwd: this.mobileProjectPath,
|
|
2743
|
+
stdio: "pipe",
|
|
2744
|
+
timeout: 12e4
|
|
2745
|
+
// 2 minute timeout
|
|
2746
|
+
});
|
|
2747
|
+
return true;
|
|
2748
|
+
} catch {
|
|
2749
|
+
return false;
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
async startNgrok() {
|
|
2753
|
+
this.ensureLogDir();
|
|
2754
|
+
this.ngrokLogStream = createWriteStream(join5(this.logDir, "ngrok.log"));
|
|
2755
|
+
this.ngrokProcess = spawn2("ngrok", ["http", String(this.expoPort)], {
|
|
2756
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2757
|
+
detached: false
|
|
2758
|
+
});
|
|
2759
|
+
this.ngrokProcess.stdout?.pipe(this.ngrokLogStream);
|
|
2760
|
+
this.ngrokProcess.stderr?.pipe(this.ngrokLogStream);
|
|
2761
|
+
this.ngrokProcess.on("error", () => {
|
|
2762
|
+
});
|
|
2763
|
+
for (let i = 0; i < 10; i++) {
|
|
2764
|
+
await this.sleep(1e3);
|
|
2765
|
+
const url = await this.getNgrokTunnelUrl();
|
|
2766
|
+
if (url) {
|
|
2767
|
+
this.tunnelUrl = url;
|
|
2768
|
+
return url;
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
this.killProcess(this.ngrokProcess);
|
|
2772
|
+
this.ngrokProcess = null;
|
|
2773
|
+
return null;
|
|
2774
|
+
}
|
|
2775
|
+
async startExpo(tunnelUrl) {
|
|
2776
|
+
this.ensureLogDir();
|
|
2777
|
+
this.expoLogStream = createWriteStream(join5(this.logDir, "expo.log"));
|
|
2778
|
+
this.expoProcess = spawn2("npx", ["expo", "start", "--port", String(this.expoPort)], {
|
|
2779
|
+
cwd: this.mobileProjectPath,
|
|
2780
|
+
env: {
|
|
2781
|
+
...process.env,
|
|
2782
|
+
EXPO_PACKAGER_PROXY_URL: tunnelUrl
|
|
2783
|
+
},
|
|
2784
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2785
|
+
detached: false
|
|
2786
|
+
});
|
|
2787
|
+
this.expoProcess.on("error", () => {
|
|
2788
|
+
});
|
|
2789
|
+
return new Promise((resolve3) => {
|
|
2790
|
+
let qrDetected = false;
|
|
2791
|
+
let qrFinished = false;
|
|
2792
|
+
let resolved = false;
|
|
2793
|
+
const timeout = setTimeout(() => {
|
|
2794
|
+
if (!resolved) {
|
|
2795
|
+
resolved = true;
|
|
2796
|
+
this.expoProcess?.stdout?.pipe(this.expoLogStream);
|
|
2797
|
+
this.expoProcess?.stderr?.pipe(this.expoLogStream);
|
|
2798
|
+
resolve3(qrDetected);
|
|
2799
|
+
}
|
|
2800
|
+
}, 3e4);
|
|
2801
|
+
this.expoProcess?.stdout?.on("data", (data) => {
|
|
2802
|
+
const text = data.toString();
|
|
2803
|
+
const lines = text.split("\n");
|
|
2804
|
+
for (const line of lines) {
|
|
2805
|
+
if (qrFinished) {
|
|
2806
|
+
this.expoLogStream?.write(line + "\n");
|
|
2807
|
+
continue;
|
|
2808
|
+
}
|
|
2809
|
+
if (this.isQrCodeLine(line)) {
|
|
2810
|
+
qrDetected = true;
|
|
2811
|
+
process.stdout.write(line + "\n");
|
|
2812
|
+
} else if (qrDetected && this.isExpoReadyLine(line)) {
|
|
2813
|
+
process.stdout.write(line + "\n");
|
|
2814
|
+
qrFinished = true;
|
|
2815
|
+
if (!resolved) {
|
|
2816
|
+
resolved = true;
|
|
2817
|
+
clearTimeout(timeout);
|
|
2818
|
+
resolve3(true);
|
|
2819
|
+
}
|
|
2820
|
+
} else if (qrDetected && !qrFinished) {
|
|
2821
|
+
process.stdout.write(line + "\n");
|
|
2822
|
+
} else {
|
|
2823
|
+
this.expoLogStream?.write(line + "\n");
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
});
|
|
2827
|
+
this.expoProcess?.stderr?.pipe(this.expoLogStream);
|
|
2828
|
+
this.expoProcess?.on("exit", () => {
|
|
2829
|
+
if (!resolved) {
|
|
2830
|
+
resolved = true;
|
|
2831
|
+
clearTimeout(timeout);
|
|
2832
|
+
resolve3(false);
|
|
2833
|
+
}
|
|
2834
|
+
});
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
async start() {
|
|
2838
|
+
this.onProgress("Checking mobile project...");
|
|
2839
|
+
const projectResult = this.ensureMobileProject();
|
|
2840
|
+
if (!projectResult.ready) {
|
|
2841
|
+
return { started: false, error: projectResult.error };
|
|
2842
|
+
}
|
|
2843
|
+
if (projectResult.cloned) {
|
|
2844
|
+
this.onProgress("Mobile project cloned to ~/.clautunnel/repo/apps/mobile");
|
|
2845
|
+
}
|
|
2846
|
+
this.onProgress("Checking prerequisites...");
|
|
2847
|
+
const prereqs = this.checkPrerequisites();
|
|
2848
|
+
if (!prereqs.ready) {
|
|
2849
|
+
return { started: false, error: prereqs.issues.join("; ") };
|
|
2850
|
+
}
|
|
2851
|
+
if (prereqs.needsInstall) {
|
|
2852
|
+
this.onProgress("Installing dependencies (this may take a minute)...");
|
|
2853
|
+
const installed = this.installDependencies();
|
|
2854
|
+
if (!installed) {
|
|
2855
|
+
return { started: false, error: "Failed to install mobile dependencies" };
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
this.onProgress("Syncing credentials...");
|
|
2859
|
+
this.ensureEnvFile();
|
|
2860
|
+
this.onProgress("Starting ngrok tunnel...");
|
|
2861
|
+
const tunnelUrl = await this.startNgrok();
|
|
2862
|
+
if (!tunnelUrl) {
|
|
2863
|
+
return { started: false, error: "Failed to start ngrok tunnel" };
|
|
2864
|
+
}
|
|
2865
|
+
this.onProgress("Starting Expo server...");
|
|
2866
|
+
const expoStarted = await this.startExpo(tunnelUrl);
|
|
2867
|
+
if (!expoStarted) {
|
|
2868
|
+
await this.stop();
|
|
2869
|
+
return { started: false, error: "Failed to start Expo server" };
|
|
2870
|
+
}
|
|
2871
|
+
return { started: true, tunnelUrl };
|
|
2872
|
+
}
|
|
2873
|
+
async stop() {
|
|
2874
|
+
if (this.expoProcess) {
|
|
2875
|
+
this.killProcess(this.expoProcess);
|
|
2876
|
+
this.expoProcess = null;
|
|
2877
|
+
}
|
|
2878
|
+
if (this.ngrokProcess) {
|
|
2879
|
+
this.killProcess(this.ngrokProcess);
|
|
2880
|
+
this.ngrokProcess = null;
|
|
2881
|
+
}
|
|
2882
|
+
if (this.expoLogStream) {
|
|
2883
|
+
this.expoLogStream.end();
|
|
2884
|
+
this.expoLogStream = null;
|
|
2885
|
+
}
|
|
2886
|
+
if (this.ngrokLogStream) {
|
|
2887
|
+
this.ngrokLogStream.end();
|
|
2888
|
+
this.ngrokLogStream = null;
|
|
2889
|
+
}
|
|
2890
|
+
this.tunnelUrl = null;
|
|
2891
|
+
}
|
|
2892
|
+
getTunnelUrl() {
|
|
2893
|
+
return this.tunnelUrl;
|
|
2894
|
+
}
|
|
2895
|
+
isQrCodeLine(line) {
|
|
2896
|
+
return line.includes("\u2588") || line.includes("\u2584") || line.includes("\u2580");
|
|
2897
|
+
}
|
|
2898
|
+
isExpoReadyLine(line) {
|
|
2899
|
+
return line.includes("Metro waiting on") || line.includes("Logs for your project");
|
|
2900
|
+
}
|
|
2901
|
+
getNgrokTunnelUrl() {
|
|
2902
|
+
return new Promise((resolve3) => {
|
|
2903
|
+
const req = get("http://localhost:4040/api/tunnels", (res) => {
|
|
2904
|
+
let data = "";
|
|
2905
|
+
res.on("data", (chunk) => data += chunk);
|
|
2906
|
+
res.on("end", () => {
|
|
2907
|
+
try {
|
|
2908
|
+
const tunnels = JSON.parse(data).tunnels;
|
|
2909
|
+
const httpsTunnel = tunnels?.find(
|
|
2910
|
+
(t) => t.proto === "https"
|
|
2911
|
+
);
|
|
2912
|
+
resolve3(httpsTunnel?.public_url ?? null);
|
|
2913
|
+
} catch {
|
|
2914
|
+
resolve3(null);
|
|
2915
|
+
}
|
|
2916
|
+
});
|
|
2917
|
+
});
|
|
2918
|
+
req.on("error", () => resolve3(null));
|
|
2919
|
+
req.setTimeout(2e3, () => {
|
|
2920
|
+
req.destroy();
|
|
2921
|
+
resolve3(null);
|
|
2922
|
+
});
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
killProcess(proc) {
|
|
2926
|
+
try {
|
|
2927
|
+
proc.kill("SIGTERM");
|
|
2928
|
+
setTimeout(() => {
|
|
2929
|
+
try {
|
|
2930
|
+
proc.kill("SIGKILL");
|
|
2931
|
+
} catch {
|
|
2932
|
+
}
|
|
2933
|
+
}, 3e3);
|
|
2934
|
+
} catch {
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
ensureLogDir() {
|
|
2938
|
+
if (!existsSync4(this.logDir)) {
|
|
2939
|
+
mkdirSync3(this.logDir, { recursive: true });
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
sleep(ms) {
|
|
2943
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2944
|
+
}
|
|
2945
|
+
};
|
|
2946
|
+
|
|
2607
2947
|
// src/commands/start.ts
|
|
2608
2948
|
if (typeof globalThis.WebSocket === "undefined") {
|
|
2609
2949
|
globalThis.WebSocket = WebSocket;
|
|
2610
2950
|
}
|
|
2611
2951
|
function createStartCommand() {
|
|
2612
2952
|
const command = new Command("start");
|
|
2613
|
-
command.description("Start ClauTunnel and listen for session requests from mobile app").option("-n, --name <name>", "Machine name").option("--prevent-sleep", "Auto-enable sleep prevention (skip prompt)").action(async (options) => {
|
|
2953
|
+
command.description("Start ClauTunnel and listen for session requests from mobile app").option("-n, --name <name>", "Machine name").option("--prevent-sleep", "Auto-enable sleep prevention (skip prompt)").option("--no-mobile", "Skip mobile server startup").action(async (options) => {
|
|
2614
2954
|
const config2 = new Config();
|
|
2615
2955
|
const logger = new Logger();
|
|
2616
2956
|
const spinner = new Spinner("Starting ClauTunnel...");
|
|
@@ -2628,10 +2968,13 @@ function createStartCommand() {
|
|
|
2628
2968
|
const session = await restoreSession(supabase, config2);
|
|
2629
2969
|
if (!session) {
|
|
2630
2970
|
spinner.fail("Not authenticated");
|
|
2631
|
-
logger.error(
|
|
2971
|
+
logger.error(
|
|
2972
|
+
'Run "clautunnel login" or "clautunnel signup" first.'
|
|
2973
|
+
);
|
|
2632
2974
|
process.exit(1);
|
|
2633
2975
|
}
|
|
2634
2976
|
const { user } = session;
|
|
2977
|
+
spinner.update(`Authenticated as ${user.email}...`);
|
|
2635
2978
|
let fdaStatus = null;
|
|
2636
2979
|
if (isMacOS()) {
|
|
2637
2980
|
spinner.stop();
|
|
@@ -2698,7 +3041,34 @@ function createStartCommand() {
|
|
|
2698
3041
|
}
|
|
2699
3042
|
spinner.start();
|
|
2700
3043
|
}
|
|
2701
|
-
|
|
3044
|
+
let mobileServer = null;
|
|
3045
|
+
if (options.mobile !== false) {
|
|
3046
|
+
const mobileProjectPath = config2.getMobileProjectPath();
|
|
3047
|
+
mobileServer = new MobileServerManager({
|
|
3048
|
+
mobileProjectPath,
|
|
3049
|
+
supabaseUrl: config2.getSupabaseUrl(),
|
|
3050
|
+
supabaseAnonKey: config2.getSupabaseAnonKey(),
|
|
3051
|
+
onProgress: (msg) => spinner.update(msg)
|
|
3052
|
+
});
|
|
3053
|
+
const result = await mobileServer.start();
|
|
3054
|
+
spinner.stop();
|
|
3055
|
+
if (result.started) {
|
|
3056
|
+
logger.info("");
|
|
3057
|
+
logger.info(` Tunnel: ${result.tunnelUrl}`);
|
|
3058
|
+
logger.info("");
|
|
3059
|
+
} else {
|
|
3060
|
+
logger.error(`Mobile server failed: ${result.error}`);
|
|
3061
|
+
process.exit(1);
|
|
3062
|
+
}
|
|
3063
|
+
spinner.start();
|
|
3064
|
+
}
|
|
3065
|
+
const cleanup2 = async () => {
|
|
3066
|
+
if (mobileServer) {
|
|
3067
|
+
try {
|
|
3068
|
+
await mobileServer.stop();
|
|
3069
|
+
} catch {
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
2702
3072
|
if (sleepState.pmsetEnabled) {
|
|
2703
3073
|
console.log("Restoring sleep settings...");
|
|
2704
3074
|
}
|
|
@@ -2754,9 +3124,12 @@ function createStartCommand() {
|
|
|
2754
3124
|
const connected = await machineClient.connect();
|
|
2755
3125
|
spinner.stop();
|
|
2756
3126
|
if (!connected) {
|
|
2757
|
-
logger.error(
|
|
2758
|
-
|
|
2759
|
-
);
|
|
3127
|
+
logger.error("Failed to connect to Supabase Realtime.");
|
|
3128
|
+
logger.error("");
|
|
3129
|
+
logger.error("This may be a temporary issue. Try the following:");
|
|
3130
|
+
logger.error(' 1. Open a new terminal and run "clautunnel start" again');
|
|
3131
|
+
logger.error(" 2. Check your network connection");
|
|
3132
|
+
logger.error(' 3. Try "clautunnel login" to refresh your session');
|
|
2760
3133
|
process.exit(1);
|
|
2761
3134
|
}
|
|
2762
3135
|
logger.info("");
|
|
@@ -2774,6 +3147,9 @@ function createStartCommand() {
|
|
|
2774
3147
|
` Sleep prevention: ${sleepState.pmsetEnabled ? "Lid-closed mode" : "Basic mode"}`
|
|
2775
3148
|
);
|
|
2776
3149
|
}
|
|
3150
|
+
if (mobileServer) {
|
|
3151
|
+
logger.info(" Mobile server: Running");
|
|
3152
|
+
}
|
|
2777
3153
|
logger.info("");
|
|
2778
3154
|
logger.info("Open the mobile app to start a session.");
|
|
2779
3155
|
logger.info("Press Ctrl+C to stop.");
|
|
@@ -3083,10 +3459,28 @@ function createLoginCommand() {
|
|
|
3083
3459
|
return command;
|
|
3084
3460
|
}
|
|
3085
3461
|
|
|
3086
|
-
// src/commands/
|
|
3462
|
+
// src/commands/logout.ts
|
|
3087
3463
|
import { Command as Command5 } from "commander";
|
|
3464
|
+
function createLogoutCommand() {
|
|
3465
|
+
const command = new Command5("logout");
|
|
3466
|
+
command.description("Log out of ClauTunnel").action(async () => {
|
|
3467
|
+
const config2 = new Config();
|
|
3468
|
+
const logger = new Logger();
|
|
3469
|
+
const session = config2.getSessionTokens();
|
|
3470
|
+
if (!session) {
|
|
3471
|
+
logger.info("Not currently logged in.");
|
|
3472
|
+
return;
|
|
3473
|
+
}
|
|
3474
|
+
config2.clearSessionTokens();
|
|
3475
|
+
logger.info("Logged out successfully.");
|
|
3476
|
+
});
|
|
3477
|
+
return command;
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
// src/commands/signup.ts
|
|
3481
|
+
import { Command as Command6 } from "commander";
|
|
3088
3482
|
function createSignupCommand() {
|
|
3089
|
-
const command = new
|
|
3483
|
+
const command = new Command6("signup");
|
|
3090
3484
|
command.description("Create a new ClauTunnel account").action(async () => {
|
|
3091
3485
|
const config2 = new Config();
|
|
3092
3486
|
const logger = new Logger();
|
|
@@ -3156,9 +3550,9 @@ function createSignupCommand() {
|
|
|
3156
3550
|
}
|
|
3157
3551
|
|
|
3158
3552
|
// src/commands/setup.ts
|
|
3159
|
-
import { Command as
|
|
3553
|
+
import { Command as Command7 } from "commander";
|
|
3160
3554
|
function createSetupCommand() {
|
|
3161
|
-
const command = new
|
|
3555
|
+
const command = new Command7("setup");
|
|
3162
3556
|
command.description("Configure ClauTunnel with Supabase credentials").action(async () => {
|
|
3163
3557
|
const config2 = new Config();
|
|
3164
3558
|
const logger = new Logger();
|
|
@@ -3223,11 +3617,11 @@ function createSetupCommand() {
|
|
|
3223
3617
|
}
|
|
3224
3618
|
|
|
3225
3619
|
// src/commands/mobile-setup.ts
|
|
3226
|
-
import { Command as
|
|
3227
|
-
import { existsSync as
|
|
3228
|
-
import { join as
|
|
3620
|
+
import { Command as Command8 } from "commander";
|
|
3621
|
+
import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
3622
|
+
import { join as join7, resolve } from "path";
|
|
3229
3623
|
function createMobileSetupCommand() {
|
|
3230
|
-
const command = new
|
|
3624
|
+
const command = new Command8("mobile-setup");
|
|
3231
3625
|
command.description("Generate mobile app .env file from CLI credentials").action(async () => {
|
|
3232
3626
|
const config2 = new Config();
|
|
3233
3627
|
const logger = new Logger();
|
|
@@ -3236,7 +3630,7 @@ function createMobileSetupCommand() {
|
|
|
3236
3630
|
const supabaseUrl = config2.getSupabaseUrl();
|
|
3237
3631
|
const supabaseAnonKey = config2.getSupabaseAnonKey();
|
|
3238
3632
|
const mobileDir = resolve(process.cwd(), "apps", "mobile");
|
|
3239
|
-
if (!
|
|
3633
|
+
if (!existsSync6(mobileDir)) {
|
|
3240
3634
|
logger.error("Could not find apps/mobile directory.");
|
|
3241
3635
|
logger.error("");
|
|
3242
3636
|
logger.error("Make sure you run this command from the ClauTunnel project root:");
|
|
@@ -3244,8 +3638,8 @@ function createMobileSetupCommand() {
|
|
|
3244
3638
|
logger.error(" clautunnel mobile-setup");
|
|
3245
3639
|
process.exit(1);
|
|
3246
3640
|
}
|
|
3247
|
-
const envPath =
|
|
3248
|
-
if (
|
|
3641
|
+
const envPath = join7(mobileDir, ".env");
|
|
3642
|
+
if (existsSync6(envPath)) {
|
|
3249
3643
|
logger.warn("apps/mobile/.env already exists and will be overwritten.");
|
|
3250
3644
|
}
|
|
3251
3645
|
const envContent = [
|
|
@@ -3253,15 +3647,15 @@ function createMobileSetupCommand() {
|
|
|
3253
3647
|
`EXPO_PUBLIC_SUPABASE_ANON_KEY=${supabaseAnonKey}`,
|
|
3254
3648
|
""
|
|
3255
3649
|
].join("\n");
|
|
3256
|
-
|
|
3650
|
+
writeFileSync4(envPath, envContent);
|
|
3651
|
+
config2.setMobileProjectPath(mobileDir);
|
|
3257
3652
|
logger.info("");
|
|
3258
|
-
logger.info("Mobile app
|
|
3259
|
-
logger.info(` ${
|
|
3653
|
+
logger.info("Mobile app configured successfully!");
|
|
3654
|
+
logger.info(` Project: ${mobileDir}`);
|
|
3655
|
+
logger.info(` .env: ${envPath}`);
|
|
3260
3656
|
logger.info("");
|
|
3261
|
-
logger.info("
|
|
3262
|
-
logger.info("
|
|
3263
|
-
logger.info(" 2. pnpm start");
|
|
3264
|
-
logger.info(" 3. Scan the QR code with Expo Go");
|
|
3657
|
+
logger.info('The mobile server will start automatically with "clautunnel start".');
|
|
3658
|
+
logger.info('Use "clautunnel start --no-mobile" to skip mobile server.');
|
|
3265
3659
|
} catch (error) {
|
|
3266
3660
|
if (error instanceof ConfigurationError) {
|
|
3267
3661
|
logger.error(error.message);
|
|
@@ -3284,13 +3678,14 @@ var packageJson = JSON.parse(
|
|
|
3284
3678
|
readFileSync5(resolve2(__dirname, "../package.json"), "utf-8")
|
|
3285
3679
|
);
|
|
3286
3680
|
var version = packageJson.version || "0.0.0";
|
|
3287
|
-
var program = new
|
|
3681
|
+
var program = new Command9();
|
|
3288
3682
|
program.name("clautunnel").description("Remote control for Claude Code CLI").version(version);
|
|
3289
3683
|
program.addCommand(createSetupCommand());
|
|
3290
3684
|
program.addCommand(createStartCommand());
|
|
3291
3685
|
program.addCommand(createStopCommand());
|
|
3292
3686
|
program.addCommand(createStatusCommand());
|
|
3293
3687
|
program.addCommand(createLoginCommand());
|
|
3688
|
+
program.addCommand(createLogoutCommand());
|
|
3294
3689
|
program.addCommand(createSignupCommand());
|
|
3295
3690
|
program.addCommand(createMobileSetupCommand());
|
|
3296
3691
|
if (process.argv[1]?.includes("clautunnel") || process.argv[1]?.endsWith("/index.js") || process.argv[1]?.endsWith("/index.ts")) {
|