@tongil_kim/clautunnel 1.6.2 → 1.7.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/dist/index.d.ts +1 -0
- package/dist/index.js +342 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ declare class Config {
|
|
|
31
31
|
requireConfiguration(): void;
|
|
32
32
|
getMachineId(): string | undefined;
|
|
33
33
|
setMachineId(machineId: string): void;
|
|
34
|
+
clearMachineId(): void;
|
|
34
35
|
getSessionTokens(): ConfigData['sessionTokens'] | undefined;
|
|
35
36
|
setSessionTokens(tokens: ConfigData['sessionTokens']): void;
|
|
36
37
|
setSession(tokens: {
|
package/dist/index.js
CHANGED
|
@@ -202,7 +202,7 @@ var require_dist = __commonJS({
|
|
|
202
202
|
// src/index.ts
|
|
203
203
|
import { config } from "dotenv";
|
|
204
204
|
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
205
|
-
import { readFileSync as
|
|
205
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
206
206
|
import { fileURLToPath } from "url";
|
|
207
207
|
|
|
208
208
|
// src/utils/config.ts
|
|
@@ -293,6 +293,10 @@ var Config = class {
|
|
|
293
293
|
this.data.machineId = machineId;
|
|
294
294
|
this.saveConfig();
|
|
295
295
|
}
|
|
296
|
+
clearMachineId() {
|
|
297
|
+
delete this.data.machineId;
|
|
298
|
+
this.saveConfig();
|
|
299
|
+
}
|
|
296
300
|
getSessionTokens() {
|
|
297
301
|
return this.data.sessionTokens;
|
|
298
302
|
}
|
|
@@ -311,8 +315,8 @@ var Config = class {
|
|
|
311
315
|
getMobileProjectPath() {
|
|
312
316
|
return this.data.mobileProjectPath;
|
|
313
317
|
}
|
|
314
|
-
setMobileProjectPath(
|
|
315
|
-
this.data.mobileProjectPath =
|
|
318
|
+
setMobileProjectPath(path5) {
|
|
319
|
+
this.data.mobileProjectPath = path5;
|
|
316
320
|
this.saveConfig();
|
|
317
321
|
}
|
|
318
322
|
};
|
|
@@ -419,10 +423,11 @@ var RealtimeClient = class extends EventEmitter {
|
|
|
419
423
|
this.sessionId = options.sessionId;
|
|
420
424
|
}
|
|
421
425
|
async connect() {
|
|
426
|
+
const privateConfig = { config: { private: true } };
|
|
422
427
|
const outputChannelName = import_clautunnel_shared.REALTIME_CHANNELS.sessionOutput(this.sessionId);
|
|
423
|
-
this.outputChannel = this.supabase.channel(outputChannelName);
|
|
428
|
+
this.outputChannel = this.supabase.channel(outputChannelName, privateConfig);
|
|
424
429
|
const inputChannelName = import_clautunnel_shared.REALTIME_CHANNELS.sessionInput(this.sessionId);
|
|
425
|
-
this.inputChannel = this.supabase.channel(inputChannelName);
|
|
430
|
+
this.inputChannel = this.supabase.channel(inputChannelName, privateConfig);
|
|
426
431
|
this.inputChannel.on("broadcast", { event: "input" }, (payload) => {
|
|
427
432
|
this.emit("input", payload.payload);
|
|
428
433
|
});
|
|
@@ -433,7 +438,7 @@ var RealtimeClient = class extends EventEmitter {
|
|
|
433
438
|
this.realtimeEnabled = results.every((success) => success);
|
|
434
439
|
if (this.realtimeEnabled) {
|
|
435
440
|
const presenceChannelName = import_clautunnel_shared.REALTIME_CHANNELS.sessionPresence(this.sessionId);
|
|
436
|
-
this.presenceChannel = this.supabase.channel(presenceChannelName);
|
|
441
|
+
this.presenceChannel = this.supabase.channel(presenceChannelName, privateConfig);
|
|
437
442
|
this.presenceChannel.subscribe(async (status, err) => {
|
|
438
443
|
if (status === "SUBSCRIBED" && this.presenceChannel) {
|
|
439
444
|
try {
|
|
@@ -1884,23 +1889,23 @@ var ConfigManager = class {
|
|
|
1884
1889
|
getProjectSettingsPath() {
|
|
1885
1890
|
return join3(this.cwd, ".claude", "settings.json");
|
|
1886
1891
|
}
|
|
1887
|
-
readSettingsFile(
|
|
1892
|
+
readSettingsFile(path5) {
|
|
1888
1893
|
try {
|
|
1889
|
-
if (existsSync3(
|
|
1890
|
-
const content = readFileSync3(
|
|
1894
|
+
if (existsSync3(path5)) {
|
|
1895
|
+
const content = readFileSync3(path5, "utf-8");
|
|
1891
1896
|
return JSON.parse(content);
|
|
1892
1897
|
}
|
|
1893
1898
|
} catch {
|
|
1894
1899
|
}
|
|
1895
1900
|
return null;
|
|
1896
1901
|
}
|
|
1897
|
-
writeSettingsFile(
|
|
1902
|
+
writeSettingsFile(path5, settings) {
|
|
1898
1903
|
try {
|
|
1899
|
-
const dir = dirname(
|
|
1904
|
+
const dir = dirname(path5);
|
|
1900
1905
|
if (!existsSync3(dir)) {
|
|
1901
1906
|
mkdirSync2(dir, { recursive: true });
|
|
1902
1907
|
}
|
|
1903
|
-
writeFileSync2(
|
|
1908
|
+
writeFileSync2(path5, JSON.stringify(settings, null, 2));
|
|
1904
1909
|
return true;
|
|
1905
1910
|
} catch {
|
|
1906
1911
|
return false;
|
|
@@ -2099,7 +2104,7 @@ var ConfigManager = class {
|
|
|
2099
2104
|
};
|
|
2100
2105
|
}
|
|
2101
2106
|
}
|
|
2102
|
-
applyConfigChange(settings,
|
|
2107
|
+
applyConfigChange(settings, path5, payload) {
|
|
2103
2108
|
if (!settings.preferences) {
|
|
2104
2109
|
settings.preferences = {};
|
|
2105
2110
|
}
|
|
@@ -2119,33 +2124,33 @@ var ConfigManager = class {
|
|
|
2119
2124
|
default:
|
|
2120
2125
|
return { success: false, message: `Unknown config key: ${payload.key}` };
|
|
2121
2126
|
}
|
|
2122
|
-
if (this.writeSettingsFile(
|
|
2127
|
+
if (this.writeSettingsFile(path5, settings)) {
|
|
2123
2128
|
return { success: true, message: `Config ${payload.key} updated` };
|
|
2124
2129
|
}
|
|
2125
2130
|
return { success: false, message: "Failed to write settings file" };
|
|
2126
2131
|
}
|
|
2127
|
-
applyPermissionsChange(settings,
|
|
2132
|
+
applyPermissionsChange(settings, path5, payload) {
|
|
2128
2133
|
if (!settings.permissions) {
|
|
2129
2134
|
settings.permissions = {};
|
|
2130
2135
|
}
|
|
2131
2136
|
settings.permissions.mode = payload.value;
|
|
2132
|
-
if (this.writeSettingsFile(
|
|
2137
|
+
if (this.writeSettingsFile(path5, settings)) {
|
|
2133
2138
|
return { success: true, message: `Permission mode set to ${payload.value}` };
|
|
2134
2139
|
}
|
|
2135
2140
|
return { success: false, message: "Failed to write settings file" };
|
|
2136
2141
|
}
|
|
2137
|
-
applyVimChange(settings,
|
|
2142
|
+
applyVimChange(settings, path5, payload) {
|
|
2138
2143
|
if (payload.action === "toggle") {
|
|
2139
2144
|
settings.vim = !settings.vim;
|
|
2140
2145
|
} else {
|
|
2141
2146
|
settings.vim = payload.value;
|
|
2142
2147
|
}
|
|
2143
|
-
if (this.writeSettingsFile(
|
|
2148
|
+
if (this.writeSettingsFile(path5, settings)) {
|
|
2144
2149
|
return { success: true, message: `Vim mode ${settings.vim ? "enabled" : "disabled"}` };
|
|
2145
2150
|
}
|
|
2146
2151
|
return { success: false, message: "Failed to write settings file" };
|
|
2147
2152
|
}
|
|
2148
|
-
applyAllowedToolsChange(settings,
|
|
2153
|
+
applyAllowedToolsChange(settings, path5, payload) {
|
|
2149
2154
|
if (!settings.permissions) {
|
|
2150
2155
|
settings.permissions = {};
|
|
2151
2156
|
}
|
|
@@ -2177,7 +2182,7 @@ var ConfigManager = class {
|
|
|
2177
2182
|
}
|
|
2178
2183
|
break;
|
|
2179
2184
|
}
|
|
2180
|
-
if (this.writeSettingsFile(
|
|
2185
|
+
if (this.writeSettingsFile(path5, settings)) {
|
|
2181
2186
|
return { success: true, message: "Allowed tools updated" };
|
|
2182
2187
|
}
|
|
2183
2188
|
return { success: false, message: "Failed to write settings file" };
|
|
@@ -2644,7 +2649,7 @@ ${confirmationMsg}
|
|
|
2644
2649
|
};
|
|
2645
2650
|
|
|
2646
2651
|
// src/index.ts
|
|
2647
|
-
import { Command as
|
|
2652
|
+
import { Command as Command10 } from "commander";
|
|
2648
2653
|
|
|
2649
2654
|
// src/commands/start.ts
|
|
2650
2655
|
import { Command } from "commander";
|
|
@@ -2665,13 +2670,14 @@ var MachineRealtimeClient = class extends EventEmitter4 {
|
|
|
2665
2670
|
this.machineId = options.machineId;
|
|
2666
2671
|
}
|
|
2667
2672
|
async connect() {
|
|
2673
|
+
const privateConfig = { config: { private: true } };
|
|
2668
2674
|
const inputChannelName = import_clautunnel_shared4.REALTIME_CHANNELS.machineInput(this.machineId);
|
|
2669
|
-
this.inputChannel = this.supabase.channel(inputChannelName);
|
|
2675
|
+
this.inputChannel = this.supabase.channel(inputChannelName, privateConfig);
|
|
2670
2676
|
this.inputChannel.on("broadcast", { event: "machine-command" }, (payload) => {
|
|
2671
2677
|
this.emit("command", payload.payload);
|
|
2672
2678
|
});
|
|
2673
2679
|
const outputChannelName = import_clautunnel_shared4.REALTIME_CHANNELS.machineOutput(this.machineId);
|
|
2674
|
-
this.outputChannel = this.supabase.channel(outputChannelName);
|
|
2680
|
+
this.outputChannel = this.supabase.channel(outputChannelName, privateConfig);
|
|
2675
2681
|
const results = await Promise.all([
|
|
2676
2682
|
subscribeWithTimeout(this.inputChannel, "machine-input"),
|
|
2677
2683
|
subscribeWithTimeout(this.outputChannel, "machine-output")
|
|
@@ -2679,7 +2685,7 @@ var MachineRealtimeClient = class extends EventEmitter4 {
|
|
|
2679
2685
|
const connected = results.every((success) => success);
|
|
2680
2686
|
if (connected) {
|
|
2681
2687
|
const presenceChannelName = import_clautunnel_shared4.REALTIME_CHANNELS.machinePresence(this.machineId);
|
|
2682
|
-
this.presenceChannel = this.supabase.channel(presenceChannelName);
|
|
2688
|
+
this.presenceChannel = this.supabase.channel(presenceChannelName, privateConfig);
|
|
2683
2689
|
this.presenceChannel.subscribe(async (status) => {
|
|
2684
2690
|
if (status === "SUBSCRIBED" && this.presenceChannel) {
|
|
2685
2691
|
try {
|
|
@@ -3089,12 +3095,12 @@ var MobileServerManager = class {
|
|
|
3089
3095
|
}
|
|
3090
3096
|
ensureEnvFile() {
|
|
3091
3097
|
const envPath = join5(this.mobileProjectPath, ".env");
|
|
3092
|
-
const
|
|
3098
|
+
const lines = [
|
|
3093
3099
|
`EXPO_PUBLIC_SUPABASE_URL=${this.options.supabaseUrl}`,
|
|
3094
3100
|
`EXPO_PUBLIC_SUPABASE_ANON_KEY=${this.options.supabaseAnonKey}`,
|
|
3095
3101
|
""
|
|
3096
|
-
]
|
|
3097
|
-
writeFileSync3(envPath,
|
|
3102
|
+
];
|
|
3103
|
+
writeFileSync3(envPath, lines.join("\n"));
|
|
3098
3104
|
}
|
|
3099
3105
|
installDependencies() {
|
|
3100
3106
|
const nodeModulesPath = join5(this.mobileProjectPath, "node_modules");
|
|
@@ -3153,7 +3159,7 @@ var MobileServerManager = class {
|
|
|
3153
3159
|
}
|
|
3154
3160
|
}
|
|
3155
3161
|
this.ngrokError = this.diagnoseNgrokFailure(stderrData);
|
|
3156
|
-
this.
|
|
3162
|
+
this.killProcessTree(this.ngrokProcess);
|
|
3157
3163
|
this.ngrokProcess = null;
|
|
3158
3164
|
return null;
|
|
3159
3165
|
}
|
|
@@ -3176,12 +3182,14 @@ var MobileServerManager = class {
|
|
|
3176
3182
|
}
|
|
3177
3183
|
async startExpo(tunnelUrl) {
|
|
3178
3184
|
this.ensureLogDir();
|
|
3185
|
+
this.killProcessOnPort(this.expoPort);
|
|
3179
3186
|
this.expoLogStream = createWriteStream(join5(this.logDir, "expo.log"));
|
|
3180
|
-
this.expoProcess = spawn2("npx", ["expo", "start", "--port", String(this.expoPort)], {
|
|
3187
|
+
this.expoProcess = spawn2("npx", ["expo", "start", "--clear", "--port", String(this.expoPort)], {
|
|
3181
3188
|
cwd: this.mobileProjectPath,
|
|
3182
3189
|
env: {
|
|
3183
3190
|
...process.env,
|
|
3184
|
-
EXPO_PACKAGER_PROXY_URL: tunnelUrl
|
|
3191
|
+
EXPO_PACKAGER_PROXY_URL: tunnelUrl,
|
|
3192
|
+
EXPO_PUBLIC_TUNNEL_URL: tunnelUrl
|
|
3185
3193
|
},
|
|
3186
3194
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3187
3195
|
detached: false
|
|
@@ -3259,13 +3267,13 @@ var MobileServerManager = class {
|
|
|
3259
3267
|
return { started: false, error: "Failed to install mobile dependencies" };
|
|
3260
3268
|
}
|
|
3261
3269
|
}
|
|
3262
|
-
this.onProgress("Syncing credentials...");
|
|
3263
|
-
this.ensureEnvFile();
|
|
3264
3270
|
this.onProgress("Starting ngrok tunnel...");
|
|
3265
3271
|
const tunnelUrl = await this.startNgrok();
|
|
3266
3272
|
if (!tunnelUrl) {
|
|
3267
3273
|
return { started: false, error: this.ngrokError ?? "Failed to start ngrok tunnel" };
|
|
3268
3274
|
}
|
|
3275
|
+
this.onProgress("Syncing credentials...");
|
|
3276
|
+
this.ensureEnvFile();
|
|
3269
3277
|
this.onProgress("Starting Expo server...");
|
|
3270
3278
|
const expoStarted = await this.startExpo(tunnelUrl);
|
|
3271
3279
|
if (!expoStarted) {
|
|
@@ -3273,14 +3281,8 @@ var MobileServerManager = class {
|
|
|
3273
3281
|
return { started: false, error: "Failed to start Expo server" };
|
|
3274
3282
|
}
|
|
3275
3283
|
const host = tunnelUrl.replace(/^https?:\/\//, "");
|
|
3276
|
-
const
|
|
3277
|
-
|
|
3278
|
-
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
3279
|
-
console.log(" \u2502 Expo Go is required to open this QR code. \u2502");
|
|
3280
|
-
console.log(" \u2502 iOS: https://apps.apple.com/app/id982107779\u2502");
|
|
3281
|
-
console.log(" \u2502 Android: https://play.google.com/store/apps/ \u2502");
|
|
3282
|
-
console.log(" \u2502 details?id=host.exp.exponent \u2502");
|
|
3283
|
-
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
3284
|
+
const pairingParam = this.options.pairingCode ? `/--/pair?code=${this.options.pairingCode}` : "";
|
|
3285
|
+
const expoUrl = `exp://${host}:443${pairingParam}`;
|
|
3284
3286
|
console.log("");
|
|
3285
3287
|
console.log(" Scan with Expo Go:");
|
|
3286
3288
|
qrcode.generate(expoUrl, { small: true }, (code) => {
|
|
@@ -3290,17 +3292,25 @@ var MobileServerManager = class {
|
|
|
3290
3292
|
});
|
|
3291
3293
|
console.log(` ${expoUrl}`);
|
|
3292
3294
|
console.log("");
|
|
3295
|
+
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
3296
|
+
console.log(" \u2502 Expo Go is required to open this QR code. \u2502");
|
|
3297
|
+
console.log(" \u2502 iOS: https://apps.apple.com/app/id982107779\u2502");
|
|
3298
|
+
console.log(" \u2502 Android: https://play.google.com/store/apps/ \u2502");
|
|
3299
|
+
console.log(" \u2502 details?id=host.exp.exponent \u2502");
|
|
3300
|
+
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
3301
|
+
console.log("");
|
|
3293
3302
|
return { started: true, tunnelUrl };
|
|
3294
3303
|
}
|
|
3295
3304
|
async stop() {
|
|
3296
3305
|
if (this.expoProcess) {
|
|
3297
|
-
this.
|
|
3306
|
+
this.killProcessTree(this.expoProcess);
|
|
3298
3307
|
this.expoProcess = null;
|
|
3299
3308
|
}
|
|
3300
3309
|
if (this.ngrokProcess) {
|
|
3301
|
-
this.
|
|
3310
|
+
this.killProcessTree(this.ngrokProcess);
|
|
3302
3311
|
this.ngrokProcess = null;
|
|
3303
3312
|
}
|
|
3313
|
+
this.killProcessOnPort(this.expoPort);
|
|
3304
3314
|
if (this.expoLogStream) {
|
|
3305
3315
|
this.expoLogStream.end();
|
|
3306
3316
|
this.expoLogStream = null;
|
|
@@ -3344,15 +3354,21 @@ var MobileServerManager = class {
|
|
|
3344
3354
|
});
|
|
3345
3355
|
});
|
|
3346
3356
|
}
|
|
3347
|
-
|
|
3357
|
+
killProcessTree(proc) {
|
|
3358
|
+
const pid = proc.pid;
|
|
3359
|
+
if (!pid) return;
|
|
3348
3360
|
try {
|
|
3349
3361
|
proc.kill("SIGTERM");
|
|
3350
|
-
|
|
3362
|
+
} catch {
|
|
3363
|
+
}
|
|
3364
|
+
try {
|
|
3365
|
+
const children = execSync2(`pgrep -P ${pid}`, { stdio: "pipe" }).toString().trim();
|
|
3366
|
+
for (const childPid of children.split("\n").filter(Boolean)) {
|
|
3351
3367
|
try {
|
|
3352
|
-
|
|
3368
|
+
process.kill(parseInt(childPid, 10), "SIGTERM");
|
|
3353
3369
|
} catch {
|
|
3354
3370
|
}
|
|
3355
|
-
}
|
|
3371
|
+
}
|
|
3356
3372
|
} catch {
|
|
3357
3373
|
}
|
|
3358
3374
|
}
|
|
@@ -3361,6 +3377,20 @@ var MobileServerManager = class {
|
|
|
3361
3377
|
mkdirSync3(this.logDir, { recursive: true });
|
|
3362
3378
|
}
|
|
3363
3379
|
}
|
|
3380
|
+
killProcessOnPort(port) {
|
|
3381
|
+
try {
|
|
3382
|
+
const output = execSync2(`lsof -ti tcp:${port}`, { stdio: "pipe" }).toString().trim();
|
|
3383
|
+
if (output) {
|
|
3384
|
+
for (const pid of output.split("\n")) {
|
|
3385
|
+
try {
|
|
3386
|
+
process.kill(parseInt(pid, 10), "SIGTERM");
|
|
3387
|
+
} catch {
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
} catch {
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3364
3394
|
sleep(ms) {
|
|
3365
3395
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
3366
3396
|
}
|
|
@@ -3631,11 +3661,23 @@ function createStartCommand() {
|
|
|
3631
3661
|
}
|
|
3632
3662
|
let mobileServer = null;
|
|
3633
3663
|
if (options.mobile !== false) {
|
|
3664
|
+
const { data: { session: currentSession } } = await supabase.auth.getSession();
|
|
3665
|
+
const { data: pairingData, error: pairingError } = await supabase.functions.invoke("create-mobile-pairing", {
|
|
3666
|
+
headers: { Authorization: `Bearer ${currentSession?.access_token}` }
|
|
3667
|
+
});
|
|
3668
|
+
if (pairingError || !pairingData?.code) {
|
|
3669
|
+
spinner.fail("Failed to create mobile pairing code");
|
|
3670
|
+
logger.error(pairingError?.message ?? "No pairing code returned");
|
|
3671
|
+
removePidFile();
|
|
3672
|
+
process.exit(1);
|
|
3673
|
+
}
|
|
3674
|
+
const pairingCode = pairingData.code;
|
|
3634
3675
|
const mobileProjectPath = config2.getMobileProjectPath();
|
|
3635
3676
|
mobileServer = new MobileServerManager({
|
|
3636
3677
|
mobileProjectPath,
|
|
3637
3678
|
supabaseUrl: config2.getSupabaseUrl(),
|
|
3638
3679
|
supabaseAnonKey: config2.getSupabaseAnonKey(),
|
|
3680
|
+
pairingCode,
|
|
3639
3681
|
onProgress: (msg) => spinner.update(msg)
|
|
3640
3682
|
});
|
|
3641
3683
|
const result = await mobileServer.start();
|
|
@@ -4069,7 +4111,7 @@ function createLoginCommand() {
|
|
|
4069
4111
|
import { Command as Command5 } from "commander";
|
|
4070
4112
|
function createLogoutCommand() {
|
|
4071
4113
|
const command = new Command5("logout");
|
|
4072
|
-
command.description("Log out of ClauTunnel").action(async () => {
|
|
4114
|
+
command.description("Log out of ClauTunnel and revoke all device sessions").action(async () => {
|
|
4073
4115
|
const config2 = new Config();
|
|
4074
4116
|
const logger = new Logger();
|
|
4075
4117
|
const session = config2.getSessionTokens();
|
|
@@ -4077,7 +4119,26 @@ function createLogoutCommand() {
|
|
|
4077
4119
|
logger.info("Not currently logged in.");
|
|
4078
4120
|
return;
|
|
4079
4121
|
}
|
|
4122
|
+
try {
|
|
4123
|
+
const supabase = createSupabaseClient(
|
|
4124
|
+
config2.getSupabaseUrl(),
|
|
4125
|
+
config2.getSupabaseAnonKey()
|
|
4126
|
+
);
|
|
4127
|
+
const restored = await restoreSession(supabase, config2);
|
|
4128
|
+
if (restored) {
|
|
4129
|
+
const { error } = await supabase.auth.signOut({ scope: "global" });
|
|
4130
|
+
if (error) {
|
|
4131
|
+
logger.warn(`Warning: failed to revoke remote sessions: ${error.message}`);
|
|
4132
|
+
logger.warn("Local credentials cleared, but mobile devices may remain active until their tokens expire.");
|
|
4133
|
+
} else {
|
|
4134
|
+
logger.info("All device sessions revoked.");
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
} catch {
|
|
4138
|
+
logger.warn("Warning: could not reach server to revoke sessions.");
|
|
4139
|
+
}
|
|
4080
4140
|
config2.clearSessionTokens();
|
|
4141
|
+
config2.clearMachineId();
|
|
4081
4142
|
logger.info("Logged out successfully.");
|
|
4082
4143
|
});
|
|
4083
4144
|
return command;
|
|
@@ -4276,15 +4337,246 @@ function createMobileSetupCommand() {
|
|
|
4276
4337
|
return command;
|
|
4277
4338
|
}
|
|
4278
4339
|
|
|
4340
|
+
// src/commands/reset.ts
|
|
4341
|
+
import { Command as Command9 } from "commander";
|
|
4342
|
+
import * as fs4 from "fs";
|
|
4343
|
+
import * as path4 from "path";
|
|
4344
|
+
import * as os5 from "os";
|
|
4345
|
+
import { execSync as execSync4 } from "child_process";
|
|
4346
|
+
function createResetCommand() {
|
|
4347
|
+
const command = new Command9("reset");
|
|
4348
|
+
command.description("Reset to fresh user state (uninstall CLI, clean config & DB)").option("--skip-db", "Skip Supabase DB cleanup").option("--skip-ngrok", "Keep ngrok installed and configured").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
|
|
4349
|
+
const logger = new Logger();
|
|
4350
|
+
if (!options.yes) {
|
|
4351
|
+
logger.info("This will:");
|
|
4352
|
+
logger.info(" - Stop any running clautunnel processes");
|
|
4353
|
+
logger.info(" - Restore macOS sleep settings");
|
|
4354
|
+
if (!options.skipDb) {
|
|
4355
|
+
logger.info(" - Delete all your data from Supabase (machines, sessions, messages)");
|
|
4356
|
+
}
|
|
4357
|
+
if (!options.skipNgrok) {
|
|
4358
|
+
logger.info(" - Uninstall ngrok and remove its config");
|
|
4359
|
+
}
|
|
4360
|
+
logger.info(" - Delete ~/.clautunnel/ config directory");
|
|
4361
|
+
logger.info(" - Uninstall clautunnel (npm & Homebrew)");
|
|
4362
|
+
logger.info("");
|
|
4363
|
+
const confirmed = await promptYesNo("Are you sure? [y/N]: ");
|
|
4364
|
+
if (!confirmed) {
|
|
4365
|
+
logger.info("Aborted.");
|
|
4366
|
+
return;
|
|
4367
|
+
}
|
|
4368
|
+
logger.info("");
|
|
4369
|
+
}
|
|
4370
|
+
logger.info("[1/7] Stopping running processes...");
|
|
4371
|
+
const pid = readPidFile();
|
|
4372
|
+
if (pid !== null && isProcessAlive(pid)) {
|
|
4373
|
+
try {
|
|
4374
|
+
process.kill(pid, "SIGTERM");
|
|
4375
|
+
logger.info(` - clautunnel daemon stopped (PID ${pid})`);
|
|
4376
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
4377
|
+
} catch {
|
|
4378
|
+
logger.info(" - could not stop daemon");
|
|
4379
|
+
}
|
|
4380
|
+
} else {
|
|
4381
|
+
logger.info(" - no running daemon");
|
|
4382
|
+
}
|
|
4383
|
+
try {
|
|
4384
|
+
fs4.unlinkSync(PID_FILE);
|
|
4385
|
+
} catch {
|
|
4386
|
+
}
|
|
4387
|
+
try {
|
|
4388
|
+
execSync4('pkill -f "ngrok.*tunnel" 2>/dev/null', { stdio: "ignore" });
|
|
4389
|
+
logger.info(" - ngrok tunnel process killed");
|
|
4390
|
+
} catch {
|
|
4391
|
+
}
|
|
4392
|
+
try {
|
|
4393
|
+
execSync4('pkill -f "expo start" 2>/dev/null', { stdio: "ignore" });
|
|
4394
|
+
logger.info(" - expo process killed");
|
|
4395
|
+
} catch {
|
|
4396
|
+
}
|
|
4397
|
+
try {
|
|
4398
|
+
const pids = execSync4("lsof -ti tcp:8081", { stdio: "pipe" }).toString().trim();
|
|
4399
|
+
if (pids) {
|
|
4400
|
+
for (const pid2 of pids.split("\n")) {
|
|
4401
|
+
try {
|
|
4402
|
+
process.kill(parseInt(pid2, 10), "SIGTERM");
|
|
4403
|
+
} catch {
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
logger.info(" - port 8081 freed");
|
|
4407
|
+
}
|
|
4408
|
+
} catch {
|
|
4409
|
+
}
|
|
4410
|
+
logger.info("[2/7] Restoring macOS sleep settings...");
|
|
4411
|
+
if (isMacOS()) {
|
|
4412
|
+
try {
|
|
4413
|
+
const pmsetOutput = execSync4("sudo pmset -g 2>/dev/null", { encoding: "utf-8" });
|
|
4414
|
+
if (pmsetOutput.includes("disablesleep") && pmsetOutput.includes("1")) {
|
|
4415
|
+
execSync4("sudo pmset -a disablesleep 0", { stdio: "inherit" });
|
|
4416
|
+
logger.info(" - lid-close sleep restored");
|
|
4417
|
+
} else {
|
|
4418
|
+
logger.info(" - already normal");
|
|
4419
|
+
}
|
|
4420
|
+
} catch {
|
|
4421
|
+
logger.info(" - already normal");
|
|
4422
|
+
}
|
|
4423
|
+
try {
|
|
4424
|
+
execSync4("pkill -f caffeinate 2>/dev/null", { stdio: "ignore" });
|
|
4425
|
+
logger.info(" - caffeinate stopped");
|
|
4426
|
+
} catch {
|
|
4427
|
+
}
|
|
4428
|
+
} else {
|
|
4429
|
+
logger.info(" - not macOS, skipped");
|
|
4430
|
+
}
|
|
4431
|
+
if (options.skipDb) {
|
|
4432
|
+
logger.info("[3/7] Skipping DB cleanup (--skip-db)");
|
|
4433
|
+
} else {
|
|
4434
|
+
await cleanSupabaseDb(logger);
|
|
4435
|
+
}
|
|
4436
|
+
if (options.skipNgrok) {
|
|
4437
|
+
logger.info("[4/7] Skipping ngrok cleanup (--skip-ngrok)");
|
|
4438
|
+
} else {
|
|
4439
|
+
logger.info("[4/7] Uninstalling ngrok...");
|
|
4440
|
+
try {
|
|
4441
|
+
execSync4("brew list ngrok", { stdio: "ignore" });
|
|
4442
|
+
execSync4("brew uninstall ngrok", { stdio: "inherit" });
|
|
4443
|
+
logger.info(" - ngrok removed");
|
|
4444
|
+
} catch {
|
|
4445
|
+
try {
|
|
4446
|
+
execSync4("which ngrok", { stdio: "ignore" });
|
|
4447
|
+
logger.info(" - ngrok found but not installed via Homebrew, remove manually");
|
|
4448
|
+
} catch {
|
|
4449
|
+
logger.info(" - not installed, skipped");
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
const ngrokConfigDir = path4.join(os5.homedir(), ".config", "ngrok");
|
|
4453
|
+
const ngrokLegacyDir = path4.join(os5.homedir(), ".ngrok2");
|
|
4454
|
+
if (fs4.existsSync(ngrokConfigDir)) {
|
|
4455
|
+
fs4.rmSync(ngrokConfigDir, { recursive: true, force: true });
|
|
4456
|
+
logger.info(" - ngrok config removed (~/.config/ngrok)");
|
|
4457
|
+
}
|
|
4458
|
+
if (fs4.existsSync(ngrokLegacyDir)) {
|
|
4459
|
+
fs4.rmSync(ngrokLegacyDir, { recursive: true, force: true });
|
|
4460
|
+
logger.info(" - ngrok legacy config removed (~/.ngrok2)");
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
logger.info("[5/7] Removing local data...");
|
|
4464
|
+
const configDir = path4.join(os5.homedir(), ".clautunnel");
|
|
4465
|
+
const legacyDir = path4.join(os5.homedir(), ".termbridge");
|
|
4466
|
+
if (fs4.existsSync(configDir)) {
|
|
4467
|
+
fs4.rmSync(configDir, { recursive: true, force: true });
|
|
4468
|
+
logger.info(" - ~/.clautunnel removed (config, logs, repo)");
|
|
4469
|
+
} else {
|
|
4470
|
+
logger.info(" - ~/.clautunnel already clean");
|
|
4471
|
+
}
|
|
4472
|
+
if (fs4.existsSync(legacyDir)) {
|
|
4473
|
+
fs4.rmSync(legacyDir, { recursive: true, force: true });
|
|
4474
|
+
logger.info(" - ~/.termbridge removed (legacy)");
|
|
4475
|
+
}
|
|
4476
|
+
logger.info("[6/7] Uninstalling CLI (npm)...");
|
|
4477
|
+
try {
|
|
4478
|
+
execSync4("npm list -g @tongil_kim/clautunnel", { stdio: "ignore" });
|
|
4479
|
+
execSync4("npm uninstall -g @tongil_kim/clautunnel", { stdio: "inherit" });
|
|
4480
|
+
logger.info(" - npm package removed");
|
|
4481
|
+
} catch {
|
|
4482
|
+
logger.info(" - not installed via npm, skipped");
|
|
4483
|
+
}
|
|
4484
|
+
logger.info("[7/7] Uninstalling CLI (Homebrew)...");
|
|
4485
|
+
try {
|
|
4486
|
+
execSync4("brew list clautunnel", { stdio: "ignore" });
|
|
4487
|
+
execSync4("brew uninstall clautunnel", { stdio: "inherit" });
|
|
4488
|
+
logger.info(" - Homebrew package removed");
|
|
4489
|
+
} catch {
|
|
4490
|
+
logger.info(" - not installed via Homebrew, skipped");
|
|
4491
|
+
}
|
|
4492
|
+
logger.info("");
|
|
4493
|
+
logger.info("Done! Fresh user state restored.");
|
|
4494
|
+
logger.info("");
|
|
4495
|
+
logger.info("Next steps:");
|
|
4496
|
+
logger.info(" 1. npm install -g @tongil_kim/clautunnel");
|
|
4497
|
+
logger.info(" 2. clautunnel setup");
|
|
4498
|
+
logger.info(" 3. clautunnel login");
|
|
4499
|
+
logger.info(" 4. clautunnel start");
|
|
4500
|
+
});
|
|
4501
|
+
return command;
|
|
4502
|
+
}
|
|
4503
|
+
async function cleanSupabaseDb(logger) {
|
|
4504
|
+
const configDir = path4.join(os5.homedir(), ".clautunnel");
|
|
4505
|
+
const configFile = path4.join(configDir, "config.json");
|
|
4506
|
+
if (!fs4.existsSync(configFile)) {
|
|
4507
|
+
logger.info("[3/7] Skipping DB cleanup (no config file found)");
|
|
4508
|
+
return;
|
|
4509
|
+
}
|
|
4510
|
+
let configData;
|
|
4511
|
+
try {
|
|
4512
|
+
configData = JSON.parse(fs4.readFileSync(configFile, "utf-8"));
|
|
4513
|
+
} catch {
|
|
4514
|
+
logger.info("[3/7] Skipping DB cleanup (invalid config file)");
|
|
4515
|
+
return;
|
|
4516
|
+
}
|
|
4517
|
+
const supabaseUrl = configData.supabaseUrl;
|
|
4518
|
+
const anonKey = configData.supabaseAnonKey;
|
|
4519
|
+
const accessToken = configData.sessionTokens?.accessToken;
|
|
4520
|
+
const refreshToken = configData.sessionTokens?.refreshToken;
|
|
4521
|
+
if (!supabaseUrl || !anonKey || !accessToken) {
|
|
4522
|
+
logger.info("[3/7] Skipping DB cleanup (missing credentials)");
|
|
4523
|
+
return;
|
|
4524
|
+
}
|
|
4525
|
+
logger.info("[3/7] Cleaning Supabase DB data...");
|
|
4526
|
+
const supabase = createSupabaseClient(supabaseUrl, anonKey);
|
|
4527
|
+
let token = accessToken;
|
|
4528
|
+
if (refreshToken) {
|
|
4529
|
+
const { data } = await supabase.auth.setSession({
|
|
4530
|
+
access_token: accessToken,
|
|
4531
|
+
refresh_token: refreshToken
|
|
4532
|
+
});
|
|
4533
|
+
if (data?.session) {
|
|
4534
|
+
token = data.session.access_token;
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
const headers = {
|
|
4538
|
+
apikey: anonKey,
|
|
4539
|
+
Authorization: `Bearer ${token}`,
|
|
4540
|
+
Prefer: "return=minimal"
|
|
4541
|
+
};
|
|
4542
|
+
try {
|
|
4543
|
+
const res = await fetch(`${supabaseUrl}/rest/v1/push_tokens?select=*`, {
|
|
4544
|
+
method: "DELETE",
|
|
4545
|
+
headers
|
|
4546
|
+
});
|
|
4547
|
+
logger.info(res.ok ? " - push_tokens cleared" : " - push_tokens: skipped");
|
|
4548
|
+
} catch {
|
|
4549
|
+
logger.info(" - push_tokens: skipped");
|
|
4550
|
+
}
|
|
4551
|
+
try {
|
|
4552
|
+
const res = await fetch(`${supabaseUrl}/rest/v1/machines?select=*`, {
|
|
4553
|
+
method: "DELETE",
|
|
4554
|
+
headers
|
|
4555
|
+
});
|
|
4556
|
+
logger.info(res.ok ? " - machines cleared (sessions + messages cascade)" : " - machines: skipped");
|
|
4557
|
+
} catch {
|
|
4558
|
+
logger.info(" - machines: skipped");
|
|
4559
|
+
}
|
|
4560
|
+
try {
|
|
4561
|
+
const res = await fetch(`${supabaseUrl}/rest/v1/mobile_pairings?select=*`, {
|
|
4562
|
+
method: "DELETE",
|
|
4563
|
+
headers
|
|
4564
|
+
});
|
|
4565
|
+
logger.info(res.ok ? " - mobile_pairings cleared" : " - mobile_pairings: skipped");
|
|
4566
|
+
} catch {
|
|
4567
|
+
logger.info(" - mobile_pairings: skipped");
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4279
4571
|
// src/index.ts
|
|
4280
4572
|
var __filename = fileURLToPath(import.meta.url);
|
|
4281
4573
|
var __dirname = dirname3(__filename);
|
|
4282
4574
|
config({ path: resolve2(__dirname, "../.env"), quiet: true });
|
|
4283
4575
|
var packageJson = JSON.parse(
|
|
4284
|
-
|
|
4576
|
+
readFileSync6(resolve2(__dirname, "../package.json"), "utf-8")
|
|
4285
4577
|
);
|
|
4286
4578
|
var version = packageJson.version || "0.0.0";
|
|
4287
|
-
var program = new
|
|
4579
|
+
var program = new Command10();
|
|
4288
4580
|
program.name("clautunnel").description("Remote control for Claude Code CLI").version(version);
|
|
4289
4581
|
program.addCommand(createSetupCommand());
|
|
4290
4582
|
program.addCommand(createStartCommand());
|
|
@@ -4294,6 +4586,7 @@ program.addCommand(createLoginCommand());
|
|
|
4294
4586
|
program.addCommand(createLogoutCommand());
|
|
4295
4587
|
program.addCommand(createSignupCommand());
|
|
4296
4588
|
program.addCommand(createMobileSetupCommand());
|
|
4589
|
+
program.addCommand(createResetCommand());
|
|
4297
4590
|
if (process.argv[1]?.includes("clautunnel") || process.argv[1]?.endsWith("/index.js") || process.argv[1]?.endsWith("/index.ts")) {
|
|
4298
4591
|
program.parse();
|
|
4299
4592
|
}
|