@exreve/exk 1.0.55 ā 1.0.57
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/bin/exk +5 -1
- package/dist/cli/index.js +2 -2
- package/dist/ttc-cli.tar.gz +0 -0
- package/package.json +1 -1
- package/dist/agentLogger.js +0 -143
- package/dist/app-child.js +0 -1038
- package/dist/appHandlers.js +0 -142
- package/dist/appManager.js +0 -212
- package/dist/appRunner.js +0 -383
- package/dist/benchmark-startup.js +0 -347
- package/dist/cloudflaredHandlers.js +0 -279
- package/dist/containerHandlers.js +0 -193
- package/dist/fsHandlers.js +0 -86
- package/dist/githubHandlers.js +0 -525
- package/dist/index.js +0 -1262
- package/dist/moduleMcpServer.js +0 -284
- package/dist/openaiAdapter.js +0 -181
- package/dist/projectAnalyzer.js +0 -330
- package/dist/projectManager.js +0 -69
- package/dist/runnerGenerator.js +0 -210
- package/dist/sessionHandlers.js +0 -271
- package/dist/skills/index.js +0 -117
- package/dist/transferService.js +0 -284
- package/dist/updateHandlers.js +0 -82
- package/dist/updater.js +0 -422
package/dist/app-child.js
DELETED
|
@@ -1,1038 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* TalkToCode CLI - Child Process (App Bundle)
|
|
4
|
-
*
|
|
5
|
-
* This is the child process that contains all application logic.
|
|
6
|
-
* It is spawned by the updater process (updater.ts).
|
|
7
|
-
*
|
|
8
|
-
* This file is what gets updated during updates - the updater stays constant.
|
|
9
|
-
*/
|
|
10
|
-
import { Command } from 'commander';
|
|
11
|
-
import { io } from 'socket.io-client';
|
|
12
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
13
|
-
import fs from 'fs/promises';
|
|
14
|
-
import fsSync from 'fs';
|
|
15
|
-
import path from 'path';
|
|
16
|
-
import os, { networkInterfaces } from 'os';
|
|
17
|
-
import { Buffer } from 'buffer';
|
|
18
|
-
import crypto from 'crypto';
|
|
19
|
-
import { createProject, deleteProject } from './projectManager.js';
|
|
20
|
-
import { agentSessionManager } from './agentSession.js';
|
|
21
|
-
import { registerGitHubHandlers } from './githubHandlers.js';
|
|
22
|
-
import { registerCloudflaredHandlers } from './cloudflaredHandlers.js';
|
|
23
|
-
import { registerContainerHandlers } from './containerHandlers.js';
|
|
24
|
-
import { registerSessionHandlers } from './sessionHandlers.js';
|
|
25
|
-
import { registerAppHandlers } from './appHandlers.js';
|
|
26
|
-
import { registerFsHandlers } from './fsHandlers.js';
|
|
27
|
-
import { registerUpdateHandlers } from './updateHandlers.js';
|
|
28
|
-
import { execSync } from 'child_process';
|
|
29
|
-
import readline from 'readline';
|
|
30
|
-
import { fileURLToPath } from 'url';
|
|
31
|
-
import { createHash } from 'crypto';
|
|
32
|
-
// ============ Update IPC (Child -> Updater) ============
|
|
33
|
-
/**
|
|
34
|
-
* Request update from parent updater process
|
|
35
|
-
*/
|
|
36
|
-
function requestUpdateFromParent() {
|
|
37
|
-
const updaterPid = process.env.TTC_UPDATER_PID;
|
|
38
|
-
if (updaterPid) {
|
|
39
|
-
try {
|
|
40
|
-
// Send SIGUSR1 to parent to request update
|
|
41
|
-
process.kill(parseInt(updaterPid, 10), 'SIGUSR1');
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
console.error('Failed to request update from parent:', error);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
// ============ Constants ============
|
|
49
|
-
const CONFIG_DIR = path.join(os.homedir(), '.talk-to-code');
|
|
50
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
51
|
-
const DEVICE_ID_FILE = path.join(CONFIG_DIR, 'device-id.json');
|
|
52
|
-
const DEVICE_CONFIG_FILE = path.join(CONFIG_DIR, 'device-config.json');
|
|
53
|
-
const AI_CONFIG_FILE = path.join(CONFIG_DIR, 'ai-config.json');
|
|
54
|
-
const DEFAULT_API_URL = 'https://api.talk-to-code.com';
|
|
55
|
-
// ============ Helpers ============
|
|
56
|
-
async function readConfig() {
|
|
57
|
-
try {
|
|
58
|
-
const data = await fs.readFile(CONFIG_FILE, 'utf-8');
|
|
59
|
-
return JSON.parse(data);
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return { apiUrl: DEFAULT_API_URL };
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
async function writeConfig(config) {
|
|
66
|
-
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
67
|
-
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Fetch AI config from server and save to ~/.talk-to-code/ai-config.json
|
|
71
|
-
*/
|
|
72
|
-
async function fetchAiConfig(authToken) {
|
|
73
|
-
try {
|
|
74
|
-
const config = await readConfig();
|
|
75
|
-
const res = await fetch(`${config.apiUrl}/config/ai`, {
|
|
76
|
-
headers: { Authorization: `Bearer ${authToken}` },
|
|
77
|
-
});
|
|
78
|
-
if (!res.ok) {
|
|
79
|
-
console.log(`[fetchAiConfig] Server returned ${res.status}`);
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
const data = await res.json();
|
|
83
|
-
if (!data.success || !data.aiConfig) {
|
|
84
|
-
console.log('[fetchAiConfig] No AI config available for this user');
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
await fs.writeFile(AI_CONFIG_FILE, JSON.stringify(data.aiConfig, null, 2));
|
|
88
|
-
console.log('[fetchAiConfig] AI config saved successfully');
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
console.log(`[fetchAiConfig] Failed: ${error.message}`);
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
/** TTL cache for ai-config.json reads */
|
|
97
|
-
let _aiCfgCache = null;
|
|
98
|
-
const _AI_CFG_TTL = 5_000;
|
|
99
|
-
function readAiConfigCached() {
|
|
100
|
-
const now = Date.now();
|
|
101
|
-
if (_aiCfgCache && (now - _aiCfgCache.ts) < _AI_CFG_TTL)
|
|
102
|
-
return _aiCfgCache.raw;
|
|
103
|
-
try {
|
|
104
|
-
const raw = fsSync.readFileSync(AI_CONFIG_FILE, 'utf-8');
|
|
105
|
-
_aiCfgCache = { raw, ts: now };
|
|
106
|
-
return raw;
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return '';
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function hasAiCredentials() {
|
|
113
|
-
try {
|
|
114
|
-
const raw = readAiConfigCached();
|
|
115
|
-
if (!raw)
|
|
116
|
-
return false;
|
|
117
|
-
const j = JSON.parse(raw);
|
|
118
|
-
return typeof j.authToken === 'string' && j.authToken.trim().length > 0;
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
async function readDeviceAuthToken() {
|
|
125
|
-
const env = process.env.TTC_AUTH_TOKEN?.trim();
|
|
126
|
-
if (env)
|
|
127
|
-
return env;
|
|
128
|
-
try {
|
|
129
|
-
const raw = await fs.readFile(DEVICE_CONFIG_FILE, 'utf-8');
|
|
130
|
-
const j = JSON.parse(raw);
|
|
131
|
-
return typeof j.authToken === 'string' ? j.authToken.trim() : undefined;
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
return undefined;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
async function readDiskDeviceConfig() {
|
|
138
|
-
try {
|
|
139
|
-
const raw = await fs.readFile(DEVICE_CONFIG_FILE, 'utf-8');
|
|
140
|
-
return JSON.parse(raw);
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return {};
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
async function writeDeviceConfigMerged(partial) {
|
|
147
|
-
const prev = await readDiskDeviceConfig();
|
|
148
|
-
const email = partial.email !== undefined ? partial.email : prev.email;
|
|
149
|
-
let token;
|
|
150
|
-
if (partial.authToken !== undefined) {
|
|
151
|
-
token = partial.authToken.trim() || undefined;
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
token = typeof prev.authToken === 'string' && prev.authToken.trim() ? prev.authToken.trim() : undefined;
|
|
155
|
-
}
|
|
156
|
-
const out = {};
|
|
157
|
-
if (email)
|
|
158
|
-
out.email = email;
|
|
159
|
-
if (token)
|
|
160
|
-
out.authToken = token;
|
|
161
|
-
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
162
|
-
await fs.writeFile(DEVICE_CONFIG_FILE, JSON.stringify(out, null, 2));
|
|
163
|
-
}
|
|
164
|
-
async function maybeFetchAiConfigIfMissing() {
|
|
165
|
-
if (hasAiCredentials())
|
|
166
|
-
return;
|
|
167
|
-
const jwt = await readDeviceAuthToken();
|
|
168
|
-
if (!jwt)
|
|
169
|
-
return;
|
|
170
|
-
const ok = await fetchAiConfig(jwt);
|
|
171
|
-
if (!ok && !hasAiCredentials()) {
|
|
172
|
-
const cfg = await readConfig();
|
|
173
|
-
console.warn(`[CLI] No AI key in ai-config.json yet. Could not sync from ${cfg.apiUrl}/config/ai ā agent prompts will fail until this succeeds.`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
function scheduleAiConfigSync(authToken) {
|
|
177
|
-
void fetchAiConfig(authToken).then((ok) => {
|
|
178
|
-
if (!ok && !hasAiCredentials()) {
|
|
179
|
-
console.warn('[CLI] AI config sync failed; check backend /config/ai and apiUrl.');
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
async function getDeviceId() {
|
|
184
|
-
try {
|
|
185
|
-
const data = await fs.readFile(DEVICE_ID_FILE, 'utf-8');
|
|
186
|
-
return JSON.parse(data).deviceId;
|
|
187
|
-
}
|
|
188
|
-
catch {
|
|
189
|
-
const deviceId = uuidv4();
|
|
190
|
-
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
191
|
-
await fs.writeFile(DEVICE_ID_FILE, JSON.stringify({ deviceId }, null, 2));
|
|
192
|
-
return deviceId;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
function getLocalIpAddress() {
|
|
196
|
-
const nets = networkInterfaces();
|
|
197
|
-
for (const name of Object.keys(nets)) {
|
|
198
|
-
for (const net of nets[name]) {
|
|
199
|
-
if (net.family === 'IPv4' && !net.internal) {
|
|
200
|
-
return net.address;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return 'Unknown';
|
|
205
|
-
}
|
|
206
|
-
function getHostname() {
|
|
207
|
-
return os.hostname() || 'Unknown';
|
|
208
|
-
}
|
|
209
|
-
async function connect() {
|
|
210
|
-
const config = await readConfig();
|
|
211
|
-
const deviceId = await getDeviceId();
|
|
212
|
-
return new Promise((resolve, reject) => {
|
|
213
|
-
const socket = io(config.apiUrl, {
|
|
214
|
-
transports: ['websocket', 'polling'],
|
|
215
|
-
});
|
|
216
|
-
socket.on('connect', () => {
|
|
217
|
-
socket.emit('register', { type: 'cli', deviceId });
|
|
218
|
-
resolve(socket);
|
|
219
|
-
});
|
|
220
|
-
socket.on('registered', ({ type }) => {
|
|
221
|
-
console.log(`Connected as ${type}`);
|
|
222
|
-
});
|
|
223
|
-
socket.on('connect_error', (err) => {
|
|
224
|
-
reject(err);
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
const CURRENT_FILE = fileURLToPath(import.meta.url);
|
|
229
|
-
const __dirname = path.dirname(CURRENT_FILE);
|
|
230
|
-
function getCliHash() {
|
|
231
|
-
return createHash('sha256').update(fsSync.readFileSync(CURRENT_FILE)).digest('hex');
|
|
232
|
-
}
|
|
233
|
-
async function checkForUpdate() {
|
|
234
|
-
try {
|
|
235
|
-
const config = await readConfig();
|
|
236
|
-
const res = await fetch(`${config.apiUrl}/update/check`, {
|
|
237
|
-
method: 'POST',
|
|
238
|
-
headers: { 'Content-Type': 'application/json' },
|
|
239
|
-
body: JSON.stringify({ hash: getCliHash(), platform: os.platform() })
|
|
240
|
-
});
|
|
241
|
-
if (!res.ok)
|
|
242
|
-
return null;
|
|
243
|
-
return await res.json();
|
|
244
|
-
}
|
|
245
|
-
catch {
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
async function replaceSelf(tarballBuffer) {
|
|
250
|
-
const extractDir = path.join(os.tmpdir(), `ttc-update-${Date.now()}`);
|
|
251
|
-
await fs.mkdir(extractDir, { recursive: true });
|
|
252
|
-
const tarPath = path.join(extractDir, 'ttc-cli.tar.gz');
|
|
253
|
-
await fs.writeFile(tarPath, tarballBuffer);
|
|
254
|
-
execSync(`tar -xzf "${tarPath}" -C "${extractDir}"`);
|
|
255
|
-
await fs.unlink(tarPath);
|
|
256
|
-
// Preserve user config/token files (never overwrite on update)
|
|
257
|
-
const preserveFiles = ['device-config.json', 'device-id.json', 'config.json'];
|
|
258
|
-
const preserved = {};
|
|
259
|
-
for (const f of preserveFiles) {
|
|
260
|
-
try {
|
|
261
|
-
preserved[f] = await fs.readFile(path.join(CONFIG_DIR, f));
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
/* file may not exist */
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
const cliDest = path.join(CONFIG_DIR, 'cli');
|
|
268
|
-
const sharedDest = path.join(CONFIG_DIR, 'shared');
|
|
269
|
-
await fs.rm(cliDest, { recursive: true, force: true });
|
|
270
|
-
await fs.rm(sharedDest, { recursive: true, force: true });
|
|
271
|
-
await fs.cp(path.join(extractDir, 'cli'), cliDest, { recursive: true });
|
|
272
|
-
await fs.cp(path.join(extractDir, 'shared'), sharedDest, { recursive: true });
|
|
273
|
-
await fs.copyFile(path.join(extractDir, 'package.json'), path.join(CONFIG_DIR, 'package.json'));
|
|
274
|
-
await fs.rm(extractDir, { recursive: true, force: true });
|
|
275
|
-
// Restore preserved config
|
|
276
|
-
for (const f of preserveFiles) {
|
|
277
|
-
if (preserved[f])
|
|
278
|
-
await fs.writeFile(path.join(CONFIG_DIR, f), preserved[f]);
|
|
279
|
-
}
|
|
280
|
-
console.log('ā CLI updated');
|
|
281
|
-
const npmPaths = [path.join(os.homedir(), '.nvm/versions/node/v22/bin/npm'), path.join(os.homedir(), '.nvm/versions/node/v20/bin/npm')];
|
|
282
|
-
const npmBin = npmPaths.find(p => fsSync.existsSync(p)) || 'npm';
|
|
283
|
-
try {
|
|
284
|
-
execSync(`"${npmBin}" install --omit=dev`, { cwd: CONFIG_DIR, stdio: 'inherit' });
|
|
285
|
-
console.log('ā Dependencies updated');
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
console.warn('ā npm install failed');
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
async function selfUpdate(force = false) {
|
|
292
|
-
const info = await checkForUpdate();
|
|
293
|
-
if (!info || !info.updateAvailable) {
|
|
294
|
-
console.log('ā Already up to date');
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
console.log('š¦ Update available!');
|
|
298
|
-
if (!force && process.stdin.isTTY) {
|
|
299
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
300
|
-
const answer = await new Promise(r => rl.question('Update now? [Y/n] ', a => { rl.close(); r(a.trim()); }));
|
|
301
|
-
if (answer.toLowerCase().startsWith('n'))
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
if (!info.downloadUrl || !info.hash)
|
|
305
|
-
return;
|
|
306
|
-
console.log('Downloading...');
|
|
307
|
-
const res = await fetch(info.downloadUrl);
|
|
308
|
-
if (!res.ok)
|
|
309
|
-
throw new Error('Download failed');
|
|
310
|
-
const bundle = Buffer.from(await res.arrayBuffer());
|
|
311
|
-
if (createHash('sha256').update(bundle).digest('hex') !== info.hash) {
|
|
312
|
-
throw new Error('Hash mismatch');
|
|
313
|
-
}
|
|
314
|
-
await replaceSelf(bundle);
|
|
315
|
-
process.exit(0);
|
|
316
|
-
}
|
|
317
|
-
// ============ Commands ============
|
|
318
|
-
async function registerDevice(name, email) {
|
|
319
|
-
try {
|
|
320
|
-
const deviceId = await getDeviceId();
|
|
321
|
-
let deviceEmail = email;
|
|
322
|
-
// If we already have a valid token, skip approval email and just register
|
|
323
|
-
try {
|
|
324
|
-
const deviceConfig = await readDiskDeviceConfig();
|
|
325
|
-
const existingToken = deviceConfig.authToken;
|
|
326
|
-
if (existingToken && typeof existingToken === 'string') {
|
|
327
|
-
const config = await readConfig();
|
|
328
|
-
const meRes = await fetch(`${config.apiUrl}/auth/me`, {
|
|
329
|
-
headers: { Authorization: `Bearer ${existingToken}` }
|
|
330
|
-
});
|
|
331
|
-
const meData = await meRes.json();
|
|
332
|
-
if (meRes.ok && meData.success && meData.user?.email) {
|
|
333
|
-
console.log(`ā Using existing auth token for ${meData.user.email}`);
|
|
334
|
-
deviceEmail = deviceEmail || meData.user.email;
|
|
335
|
-
const socket = await connect();
|
|
336
|
-
await new Promise((resolve, reject) => {
|
|
337
|
-
socket.emit('device:register', {
|
|
338
|
-
deviceId,
|
|
339
|
-
name: name || `CLI Device ${deviceId.slice(0, 8)}`,
|
|
340
|
-
ipAddress: getLocalIpAddress(),
|
|
341
|
-
hostname: getHostname(),
|
|
342
|
-
email: deviceEmail
|
|
343
|
-
}, ({ success, device, error }) => {
|
|
344
|
-
socket.disconnect();
|
|
345
|
-
if (success && device) {
|
|
346
|
-
console.log(`ā Device registered!`);
|
|
347
|
-
console.log(`Device ID: ${device.deviceId}`);
|
|
348
|
-
console.log(`Name: ${device.name}`);
|
|
349
|
-
console.log(`Email: ${device.email}`);
|
|
350
|
-
resolve();
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
reject(new Error(error || 'Registration failed'));
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
});
|
|
357
|
-
await fetchAiConfig(existingToken);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
catch {
|
|
363
|
-
// No config or invalid token - continue with approval flow
|
|
364
|
-
}
|
|
365
|
-
// Prompt for email if not provided
|
|
366
|
-
if (!deviceEmail) {
|
|
367
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
368
|
-
deviceEmail = await new Promise((resolve) => {
|
|
369
|
-
rl.question('Enter your email address: ', (answer) => {
|
|
370
|
-
rl.close();
|
|
371
|
-
resolve(answer.trim());
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
if (!deviceEmail?.includes('@')) {
|
|
376
|
-
console.error('ā Valid email address is required');
|
|
377
|
-
throw new Error('Valid email address is required');
|
|
378
|
-
}
|
|
379
|
-
// Generate 32-byte random token (base64url encoded)
|
|
380
|
-
const randomBytes = crypto.randomBytes(32);
|
|
381
|
-
const approvalToken = randomBytes.toString('base64url'); // base64url encoding (no padding, URL-safe)
|
|
382
|
-
console.log(`\nš§ Sending approval request to ${deviceEmail}...`);
|
|
383
|
-
// Send approval request
|
|
384
|
-
const config = await readConfig();
|
|
385
|
-
const response = await fetch(`${config.apiUrl}/auth/approval-request`, {
|
|
386
|
-
method: 'POST',
|
|
387
|
-
headers: {
|
|
388
|
-
'Content-Type': 'application/json'
|
|
389
|
-
},
|
|
390
|
-
body: JSON.stringify({
|
|
391
|
-
email: deviceEmail,
|
|
392
|
-
deviceId,
|
|
393
|
-
approvalToken
|
|
394
|
-
})
|
|
395
|
-
});
|
|
396
|
-
const result = await response.json();
|
|
397
|
-
if (!result.success) {
|
|
398
|
-
console.error(`ā Failed to send approval request: ${result.error || 'Unknown error'}`);
|
|
399
|
-
throw new Error(result.error || 'Failed to send approval request');
|
|
400
|
-
}
|
|
401
|
-
console.log(`ā Approval email sent! Check your inbox (${deviceEmail})`);
|
|
402
|
-
console.log(`ā³ Waiting for approval...`);
|
|
403
|
-
// Poll every 5 seconds for approval
|
|
404
|
-
const maxAttempts = 120; // 10 minutes max (120 * 5s)
|
|
405
|
-
let attempts = 0;
|
|
406
|
-
const checkApproval = async () => {
|
|
407
|
-
attempts++;
|
|
408
|
-
try {
|
|
409
|
-
const statusResponse = await fetch(`${config.apiUrl}/auth/approval-status?token=${approvalToken}`);
|
|
410
|
-
const statusResult = await statusResponse.json();
|
|
411
|
-
if (statusResult.success && statusResult.approved) {
|
|
412
|
-
return statusResult.token || null; // Return permanent auth token
|
|
413
|
-
}
|
|
414
|
-
if (statusResult.error && !statusResult.pending) {
|
|
415
|
-
console.error(`\nā ${statusResult.error}`);
|
|
416
|
-
return null;
|
|
417
|
-
}
|
|
418
|
-
return null; // Still pending
|
|
419
|
-
}
|
|
420
|
-
catch (error) {
|
|
421
|
-
console.error(`\nā Error checking approval status: ${error.message}`);
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
// Poll until approved or timeout
|
|
426
|
-
while (attempts < maxAttempts) {
|
|
427
|
-
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
|
|
428
|
-
const token = await checkApproval();
|
|
429
|
-
if (token) {
|
|
430
|
-
// Device approved! Store permanent token
|
|
431
|
-
await writeDeviceConfigMerged({ email: deviceEmail, authToken: token });
|
|
432
|
-
console.log(`\nā Device approved!`);
|
|
433
|
-
// Register device with backend and wait for completion
|
|
434
|
-
const socket = await connect();
|
|
435
|
-
await new Promise((resolve, reject) => {
|
|
436
|
-
socket.emit('device:register', {
|
|
437
|
-
deviceId,
|
|
438
|
-
name: name || `CLI Device ${deviceId.slice(0, 8)}`,
|
|
439
|
-
ipAddress: getLocalIpAddress(),
|
|
440
|
-
hostname: getHostname(),
|
|
441
|
-
email: deviceEmail
|
|
442
|
-
}, ({ success, device, error }) => {
|
|
443
|
-
socket.disconnect();
|
|
444
|
-
if (success && device) {
|
|
445
|
-
console.log(`ā Device registered!`);
|
|
446
|
-
console.log(`Device ID: ${device.deviceId}`);
|
|
447
|
-
console.log(`Name: ${device.name}`);
|
|
448
|
-
console.log(`Email: ${device.email}`);
|
|
449
|
-
console.log(`IP Address: ${device.ipAddress || 'Unknown'}`);
|
|
450
|
-
console.log(`Hostname: ${device.hostname || 'Unknown'}`);
|
|
451
|
-
resolve();
|
|
452
|
-
}
|
|
453
|
-
else {
|
|
454
|
-
reject(new Error(error || 'Registration failed'));
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
await fetchAiConfig(token);
|
|
459
|
-
return; // Successfully registered
|
|
460
|
-
}
|
|
461
|
-
// Show progress every 30 seconds
|
|
462
|
-
if (attempts % 6 === 0) {
|
|
463
|
-
process.stdout.write(`\rā³ Still waiting... (${attempts * 5}s)`);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
console.error(`\nā Approval timeout. Please check your email and try again.`);
|
|
467
|
-
throw new Error('Approval timeout');
|
|
468
|
-
}
|
|
469
|
-
catch (error) {
|
|
470
|
-
console.error('Failed to register device:', error.message);
|
|
471
|
-
throw error;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
// Active sessions map - persists across reconnections
|
|
475
|
-
const activeSessions = new Map();
|
|
476
|
-
async function runDaemon(foreground = false, email) {
|
|
477
|
-
const config = await readConfig();
|
|
478
|
-
const deviceId = await getDeviceId();
|
|
479
|
-
const ipAddress = getLocalIpAddress();
|
|
480
|
-
const hostname = getHostname();
|
|
481
|
-
// Silent update check on startup
|
|
482
|
-
checkForUpdate().then(info => {
|
|
483
|
-
if (info?.updateAvailable)
|
|
484
|
-
console.log('š¦ Update available: run "ttc update"');
|
|
485
|
-
}).catch(() => { });
|
|
486
|
-
if (foreground)
|
|
487
|
-
console.log('=== TalkToCode CLI (Foreground) ===');
|
|
488
|
-
else
|
|
489
|
-
console.log('Starting daemon...');
|
|
490
|
-
console.log(`API: ${config.apiUrl}`);
|
|
491
|
-
console.log(`Device: ${deviceId}`);
|
|
492
|
-
console.log(`Host: ${hostname}`);
|
|
493
|
-
// Test if server is reachable
|
|
494
|
-
if (foreground) {
|
|
495
|
-
try {
|
|
496
|
-
const url = new URL(config.apiUrl);
|
|
497
|
-
const testUrl = `${url.protocol}//${url.host}`;
|
|
498
|
-
console.log(`Testing connection to ${testUrl}...`);
|
|
499
|
-
const response = await fetch(testUrl, {
|
|
500
|
-
method: 'GET',
|
|
501
|
-
signal: AbortSignal.timeout(5000)
|
|
502
|
-
});
|
|
503
|
-
console.log(`ā Server is reachable (HTTP ${response.status})`);
|
|
504
|
-
}
|
|
505
|
-
catch (error) {
|
|
506
|
-
console.error(`ā Cannot reach server: ${error.message}`);
|
|
507
|
-
console.error(` Make sure the backend server is running at ${config.apiUrl}`);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
console.log('');
|
|
511
|
-
await maybeFetchAiConfigIfMissing();
|
|
512
|
-
let socket = null;
|
|
513
|
-
let reconnectTimeout = null;
|
|
514
|
-
let reconnectAttempts = 0;
|
|
515
|
-
const MAX_RECONNECT_ATTEMPTS = Infinity; // Keep trying forever
|
|
516
|
-
const connectAndRegister = async () => {
|
|
517
|
-
try {
|
|
518
|
-
socket = io(config.apiUrl, {
|
|
519
|
-
transports: ['websocket', 'polling'],
|
|
520
|
-
reconnection: true,
|
|
521
|
-
reconnectionDelay: 1000,
|
|
522
|
-
reconnectionDelayMax: 10000,
|
|
523
|
-
reconnectionAttempts: MAX_RECONNECT_ATTEMPTS,
|
|
524
|
-
timeout: 20000,
|
|
525
|
-
forceNew: true,
|
|
526
|
-
upgrade: true,
|
|
527
|
-
});
|
|
528
|
-
// Wire socket reference for agentSession to fetch history from backend
|
|
529
|
-
agentSessionManager.setSocketRef(socket);
|
|
530
|
-
if (foreground) {
|
|
531
|
-
console.log(`Attempting to connect to: ${config.apiUrl}`);
|
|
532
|
-
}
|
|
533
|
-
// Register event handlers once (outside registered callback to prevent duplicates)
|
|
534
|
-
// Project handlers
|
|
535
|
-
socket.on('project:create', async (data, callback) => {
|
|
536
|
-
try {
|
|
537
|
-
if (foreground) {
|
|
538
|
-
console.log(`š Creating project: ${data.name} at ${data.path}`);
|
|
539
|
-
}
|
|
540
|
-
const result = await createProject({
|
|
541
|
-
projectId: data.projectId || uuidv4(),
|
|
542
|
-
name: data.name,
|
|
543
|
-
path: data.path,
|
|
544
|
-
sourcePath: data.sourcePath
|
|
545
|
-
});
|
|
546
|
-
if (result.success) {
|
|
547
|
-
if (foreground) {
|
|
548
|
-
console.log(`ā Project created: ${data.name} at ${result.actualPath || data.path}`);
|
|
549
|
-
}
|
|
550
|
-
else {
|
|
551
|
-
console.log(`Project created: ${data.name} at ${result.actualPath || data.path}`);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
else {
|
|
555
|
-
if (foreground) {
|
|
556
|
-
console.error(`ā Failed to create project: ${result.error || 'Unknown error'}`);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
callback?.({
|
|
560
|
-
success: result.success,
|
|
561
|
-
error: result.error,
|
|
562
|
-
actualPath: result.actualPath
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
catch (error) {
|
|
566
|
-
if (foreground) {
|
|
567
|
-
console.error(`ā Error creating project: ${error.message}`);
|
|
568
|
-
}
|
|
569
|
-
callback?.({ success: false, error: error.message });
|
|
570
|
-
}
|
|
571
|
-
});
|
|
572
|
-
socket.on('project:delete', async (data, callback) => {
|
|
573
|
-
try {
|
|
574
|
-
const { projectId, path } = data;
|
|
575
|
-
if (!path) {
|
|
576
|
-
callback?.({ success: false, error: 'Project path required for deletion' });
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
if (foreground) {
|
|
580
|
-
console.log(`šļø Deleting project: ${projectId} at ${path}`);
|
|
581
|
-
}
|
|
582
|
-
const result = await deleteProject(path);
|
|
583
|
-
if (result.success) {
|
|
584
|
-
if (foreground) {
|
|
585
|
-
console.log(`ā Project deleted: ${projectId} at ${path}`);
|
|
586
|
-
}
|
|
587
|
-
else {
|
|
588
|
-
console.log(`Project deleted: ${projectId} at ${path}`);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
else {
|
|
592
|
-
if (foreground) {
|
|
593
|
-
console.error(`ā Failed to delete project: ${result.error || 'Unknown error'}`);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
callback?.(result);
|
|
597
|
-
}
|
|
598
|
-
catch (error) {
|
|
599
|
-
if (foreground) {
|
|
600
|
-
console.error(`ā Error deleting project: ${error.message}`);
|
|
601
|
-
}
|
|
602
|
-
callback?.({ success: false, error: error.message });
|
|
603
|
-
}
|
|
604
|
-
});
|
|
605
|
-
// GitHub handlers (extracted to cli/githubHandlers.ts)
|
|
606
|
-
registerGitHubHandlers(socket, foreground);
|
|
607
|
-
// FS handlers (extracted to cli/fsHandlers.ts)
|
|
608
|
-
registerFsHandlers(socket);
|
|
609
|
-
// Update handlers (extracted to cli/updateHandlers.ts)
|
|
610
|
-
registerUpdateHandlers(socket, foreground, readConfig, requestUpdateFromParent);
|
|
611
|
-
// Session handlers (extracted to cli/sessionHandlers.ts)
|
|
612
|
-
registerSessionHandlers(socket, foreground, activeSessions, () => socket);
|
|
613
|
-
// App control handlers (extracted to cli/appHandlers.ts)
|
|
614
|
-
registerAppHandlers(socket, foreground);
|
|
615
|
-
socket.on('connect', () => {
|
|
616
|
-
if (foreground) {
|
|
617
|
-
console.log(`ā Connected to backend at ${config.apiUrl}`);
|
|
618
|
-
if (socket) {
|
|
619
|
-
console.log(` Socket ID: ${socket.id}`);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
else {
|
|
623
|
-
console.log('Connected to backend');
|
|
624
|
-
}
|
|
625
|
-
reconnectAttempts = 0;
|
|
626
|
-
// Register as CLI device
|
|
627
|
-
if (socket) {
|
|
628
|
-
socket.emit('register', { type: 'cli', deviceId });
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
socket.on('registered', async ({ type }) => {
|
|
632
|
-
if (foreground) {
|
|
633
|
-
console.log(`ā Registered as ${type}`);
|
|
634
|
-
}
|
|
635
|
-
else {
|
|
636
|
-
console.log(`Registered as ${type}`);
|
|
637
|
-
}
|
|
638
|
-
// Check for auth token: env TTC_AUTH_TOKEN or device-config.json (from previous approval)
|
|
639
|
-
let authToken = process.env.TTC_AUTH_TOKEN;
|
|
640
|
-
let deviceEmail = email;
|
|
641
|
-
let skipEmailFlow = false;
|
|
642
|
-
// Load from device-config if no env token (reuse state from previous approval)
|
|
643
|
-
if (!authToken) {
|
|
644
|
-
try {
|
|
645
|
-
const deviceConfig = await readDiskDeviceConfig();
|
|
646
|
-
authToken = deviceConfig.authToken;
|
|
647
|
-
if (!deviceEmail && deviceConfig.email)
|
|
648
|
-
deviceEmail = deviceConfig.email;
|
|
649
|
-
}
|
|
650
|
-
catch {
|
|
651
|
-
// No device config yet
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
if (authToken) {
|
|
655
|
-
try {
|
|
656
|
-
const parts = authToken.split('.');
|
|
657
|
-
if (parts.length === 3) {
|
|
658
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
659
|
-
if (payload.type === 'device_auth' && payload.email) {
|
|
660
|
-
deviceEmail = payload.email;
|
|
661
|
-
skipEmailFlow = true;
|
|
662
|
-
if (foreground) {
|
|
663
|
-
console.log(`ā Using stored auth token for ${deviceEmail}`);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
catch (err) {
|
|
669
|
-
if (foreground) {
|
|
670
|
-
console.warn(`ā ļø Invalid auth token in config, falling back to normal auth`);
|
|
671
|
-
}
|
|
672
|
-
authToken = undefined;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
// Load device email from config if not using token and not provided
|
|
676
|
-
if (!deviceEmail && !skipEmailFlow) {
|
|
677
|
-
try {
|
|
678
|
-
const deviceConfig = await readDiskDeviceConfig();
|
|
679
|
-
deviceEmail = deviceConfig.email;
|
|
680
|
-
}
|
|
681
|
-
catch {
|
|
682
|
-
// No device config yet
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
// Register device with backend
|
|
686
|
-
let needsApprovalLoggedOnce = false;
|
|
687
|
-
const registerDevice = () => {
|
|
688
|
-
// Determine device name
|
|
689
|
-
const containerName = process.env.CONTAINER_NAME;
|
|
690
|
-
const deviceName = containerName ? `Container: ${containerName}` : `CLI Device ${hostname}`;
|
|
691
|
-
socket.emit('device:register', {
|
|
692
|
-
deviceId,
|
|
693
|
-
name: deviceName,
|
|
694
|
-
ipAddress,
|
|
695
|
-
hostname,
|
|
696
|
-
email: deviceEmail,
|
|
697
|
-
// When using auth token, include the token for verification
|
|
698
|
-
authToken: skipEmailFlow ? authToken : undefined
|
|
699
|
-
}, ({ success, needsApproval, message, device, error }) => {
|
|
700
|
-
if (success && device) {
|
|
701
|
-
needsApprovalLoggedOnce = false;
|
|
702
|
-
if (foreground) {
|
|
703
|
-
console.log(`ā Device registered: ${device.name}`);
|
|
704
|
-
if (device.approved) {
|
|
705
|
-
console.log(`ā Device approved and ready`);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
else {
|
|
709
|
-
console.log(`Device registered: ${device.name}${device.approved ? ' (approved)' : ' (pending approval)'}`);
|
|
710
|
-
}
|
|
711
|
-
// Save config: email + authToken (preserve token so next run can reuse)
|
|
712
|
-
if (device.email) {
|
|
713
|
-
void writeDeviceConfigMerged({
|
|
714
|
-
email: device.email,
|
|
715
|
-
...(authToken ? { authToken } : {}),
|
|
716
|
-
}).catch(() => { });
|
|
717
|
-
}
|
|
718
|
-
if (authToken) {
|
|
719
|
-
scheduleAiConfigSync(authToken);
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
else if (needsApproval) {
|
|
723
|
-
// Persist email when approval was requested so restarts don't prompt again
|
|
724
|
-
if (deviceEmail) {
|
|
725
|
-
void writeDeviceConfigMerged({ email: deviceEmail }).catch(() => { });
|
|
726
|
-
}
|
|
727
|
-
// Only log once to avoid flooding logs every 30s on heartbeat
|
|
728
|
-
if (!needsApprovalLoggedOnce) {
|
|
729
|
-
needsApprovalLoggedOnce = true;
|
|
730
|
-
if (foreground) {
|
|
731
|
-
console.log(`\nā ļø ${message || 'Device approval required'}`);
|
|
732
|
-
if (!deviceEmail) {
|
|
733
|
-
console.log(` Please run: npx tsx index.ts register --email your@email.com`);
|
|
734
|
-
}
|
|
735
|
-
else {
|
|
736
|
-
console.log(` Check your email (${deviceEmail}) and click the approval link.`);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
else {
|
|
740
|
-
console.log(`ā ļø Device approval required: ${message || 'Please approve device'}`);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
else if (error) {
|
|
745
|
-
console.error(`ā Registration error: ${error}`);
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
};
|
|
749
|
-
registerDevice();
|
|
750
|
-
// Update lastSeen every 30 seconds while connected
|
|
751
|
-
const heartbeatInterval = setInterval(() => {
|
|
752
|
-
if (socket?.connected) {
|
|
753
|
-
registerDevice();
|
|
754
|
-
}
|
|
755
|
-
else {
|
|
756
|
-
clearInterval(heartbeatInterval);
|
|
757
|
-
}
|
|
758
|
-
}, 30000);
|
|
759
|
-
socket.heartbeatInterval = heartbeatInterval;
|
|
760
|
-
});
|
|
761
|
-
// Cloudflared handlers (extracted to cli/cloudflaredHandlers.ts)
|
|
762
|
-
registerCloudflaredHandlers(socket, foreground);
|
|
763
|
-
// Container handlers (extracted to cli/containerHandlers.ts)
|
|
764
|
-
registerContainerHandlers(socket, foreground, path.dirname(CURRENT_FILE));
|
|
765
|
-
socket.on('disconnect', (reason) => {
|
|
766
|
-
if (foreground) {
|
|
767
|
-
console.log(`\nā ļø Disconnected: ${reason}`);
|
|
768
|
-
console.log(' Attempting to reconnect...');
|
|
769
|
-
}
|
|
770
|
-
else {
|
|
771
|
-
console.log(`Disconnected: ${reason}`);
|
|
772
|
-
}
|
|
773
|
-
// Clear heartbeat interval
|
|
774
|
-
if (socket.heartbeatInterval) {
|
|
775
|
-
clearInterval(socket.heartbeatInterval);
|
|
776
|
-
}
|
|
777
|
-
if (reason === 'io server disconnect') {
|
|
778
|
-
// Server disconnected, reconnect manually
|
|
779
|
-
socket.connect();
|
|
780
|
-
}
|
|
781
|
-
else {
|
|
782
|
-
// Client disconnect or transport close: schedule full reconnect so process stays alive
|
|
783
|
-
if (reconnectTimeout)
|
|
784
|
-
clearTimeout(reconnectTimeout);
|
|
785
|
-
const delay = 2000;
|
|
786
|
-
if (!foreground)
|
|
787
|
-
console.log(`Reconnecting in ${delay}ms...`);
|
|
788
|
-
reconnectTimeout = setTimeout(connectAndRegister, delay);
|
|
789
|
-
}
|
|
790
|
-
});
|
|
791
|
-
socket.on('connect_error', (err) => {
|
|
792
|
-
reconnectAttempts++;
|
|
793
|
-
if (foreground) {
|
|
794
|
-
console.error(`ā Connection error (attempt ${reconnectAttempts}): ${err.message}`);
|
|
795
|
-
if (err.type) {
|
|
796
|
-
console.error(` Error type: ${err.type}`);
|
|
797
|
-
}
|
|
798
|
-
if (err.description) {
|
|
799
|
-
console.error(` Error description: ${err.description}`);
|
|
800
|
-
}
|
|
801
|
-
console.error(` Connecting to: ${config.apiUrl}`);
|
|
802
|
-
if (err.cause) {
|
|
803
|
-
console.error(` Cause:`, err.cause);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
else {
|
|
807
|
-
console.error(`Connection error (attempt ${reconnectAttempts}):`, err.message);
|
|
808
|
-
}
|
|
809
|
-
});
|
|
810
|
-
// Keep process alive
|
|
811
|
-
socket.on('error', (err) => {
|
|
812
|
-
if (foreground) {
|
|
813
|
-
console.error(`ā Socket error: ${err}`);
|
|
814
|
-
}
|
|
815
|
-
else {
|
|
816
|
-
console.error('Socket error:', err);
|
|
817
|
-
}
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
|
-
catch (error) {
|
|
821
|
-
if (foreground) {
|
|
822
|
-
console.error(`ā Failed to connect: ${error.message}`);
|
|
823
|
-
}
|
|
824
|
-
else {
|
|
825
|
-
console.error('Failed to connect:', error.message);
|
|
826
|
-
}
|
|
827
|
-
reconnectAttempts++;
|
|
828
|
-
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
829
|
-
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
830
|
-
if (foreground) {
|
|
831
|
-
console.log(`ā³ Reconnecting in ${delay}ms...`);
|
|
832
|
-
}
|
|
833
|
-
else {
|
|
834
|
-
console.log(`Reconnecting in ${delay}ms...`);
|
|
835
|
-
}
|
|
836
|
-
reconnectTimeout = setTimeout(connectAndRegister, delay);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
};
|
|
840
|
-
// Handle graceful shutdown
|
|
841
|
-
const shutdown = () => {
|
|
842
|
-
if (foreground) {
|
|
843
|
-
console.log('\n\nš Shutting down gracefully...');
|
|
844
|
-
}
|
|
845
|
-
else {
|
|
846
|
-
console.log('\nShutting down...');
|
|
847
|
-
}
|
|
848
|
-
if (reconnectTimeout) {
|
|
849
|
-
clearTimeout(reconnectTimeout);
|
|
850
|
-
}
|
|
851
|
-
if (socket) {
|
|
852
|
-
// Clear heartbeat interval
|
|
853
|
-
if (socket.heartbeatInterval) {
|
|
854
|
-
clearInterval(socket.heartbeatInterval);
|
|
855
|
-
}
|
|
856
|
-
socket.disconnect();
|
|
857
|
-
}
|
|
858
|
-
process.exit(0);
|
|
859
|
-
};
|
|
860
|
-
process.on('SIGTERM', shutdown);
|
|
861
|
-
process.on('SIGINT', shutdown);
|
|
862
|
-
process.on('SIGHUP', shutdown);
|
|
863
|
-
// Start connection
|
|
864
|
-
await connectAndRegister();
|
|
865
|
-
// Keep process alive
|
|
866
|
-
process.stdin.resume();
|
|
867
|
-
}
|
|
868
|
-
// ============ CLI Program ============
|
|
869
|
-
const program = new Command();
|
|
870
|
-
program
|
|
871
|
-
.name('ttc')
|
|
872
|
-
.description('TalkToCode CLI - Simple install and manage')
|
|
873
|
-
.version('1.0.0');
|
|
874
|
-
// Config command
|
|
875
|
-
program
|
|
876
|
-
.command('config')
|
|
877
|
-
.description('Get or set config (api-url)')
|
|
878
|
-
.option('--api-url <url>', 'API base URL')
|
|
879
|
-
.action(async (opts) => {
|
|
880
|
-
const config = await readConfig();
|
|
881
|
-
if (opts.apiUrl !== undefined) {
|
|
882
|
-
config.apiUrl = opts.apiUrl;
|
|
883
|
-
await writeConfig(config);
|
|
884
|
-
console.log('Set api-url:', config.apiUrl);
|
|
885
|
-
}
|
|
886
|
-
else {
|
|
887
|
-
console.log('Config:', JSON.stringify(config, null, 2));
|
|
888
|
-
}
|
|
889
|
-
process.exit(0);
|
|
890
|
-
});
|
|
891
|
-
// Install command
|
|
892
|
-
program
|
|
893
|
-
.command('install')
|
|
894
|
-
.description('Install and register TTC CLI (installs as pm2 daemon)')
|
|
895
|
-
.argument('[email]', 'Email address for device approval')
|
|
896
|
-
.action(async (email) => {
|
|
897
|
-
try {
|
|
898
|
-
// First register the device
|
|
899
|
-
await registerDevice(undefined, email);
|
|
900
|
-
// After successful registration, install as service (Linux only)
|
|
901
|
-
if (process.platform === 'win32') {
|
|
902
|
-
console.log('\nā Device registered.');
|
|
903
|
-
console.log('On Windows, run the daemon manually: ttc daemon');
|
|
904
|
-
console.log('To run in background: start /B node path\\to\\ttc daemon');
|
|
905
|
-
}
|
|
906
|
-
else {
|
|
907
|
-
console.log('\nš¦ Installing TTC with pm2...');
|
|
908
|
-
// install-service.sh: bundled at __dirname, or in ~/.talk-to-code for curl install
|
|
909
|
-
let installScript = path.join(__dirname, 'install-service.sh');
|
|
910
|
-
if (!fsSync.existsSync(installScript)) {
|
|
911
|
-
installScript = path.join(CONFIG_DIR, 'install-service.sh');
|
|
912
|
-
}
|
|
913
|
-
if (!fsSync.existsSync(installScript)) {
|
|
914
|
-
installScript = path.join(__dirname, '..', 'install-service.sh');
|
|
915
|
-
}
|
|
916
|
-
const scriptDir = path.dirname(installScript);
|
|
917
|
-
try {
|
|
918
|
-
await fs.access(installScript);
|
|
919
|
-
const { execSync } = await import('child_process');
|
|
920
|
-
execSync(`bash "${installScript}"`, {
|
|
921
|
-
stdio: 'inherit',
|
|
922
|
-
cwd: scriptDir,
|
|
923
|
-
env: { ...process.env, PATH: process.env.PATH }
|
|
924
|
-
});
|
|
925
|
-
console.log('\nā TTC installation complete!');
|
|
926
|
-
console.log('The service is now running in the background.');
|
|
927
|
-
}
|
|
928
|
-
catch (scriptErr) {
|
|
929
|
-
const err = scriptErr;
|
|
930
|
-
console.log('\nā Service installation had issues, but device is registered.');
|
|
931
|
-
if (err.message)
|
|
932
|
-
console.error('Error:', err.message);
|
|
933
|
-
console.log('You can start the daemon manually with: ttc daemon');
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
catch (error) {
|
|
938
|
-
const errorMsg = error.message;
|
|
939
|
-
if (errorMsg.includes('approval') || errorMsg.includes('email')) {
|
|
940
|
-
// Registration failed - exit with error
|
|
941
|
-
process.exit(1);
|
|
942
|
-
}
|
|
943
|
-
// Service installation failed, but device is registered
|
|
944
|
-
console.log('\nā Service installation had issues, but device is registered.');
|
|
945
|
-
console.log('You can start the daemon manually with: ttc daemon');
|
|
946
|
-
}
|
|
947
|
-
// Exit after install is complete
|
|
948
|
-
process.exit(0);
|
|
949
|
-
});
|
|
950
|
-
// Uninstall command
|
|
951
|
-
program
|
|
952
|
-
.command('uninstall')
|
|
953
|
-
.description('Uninstall TTC CLI (removes service and config)')
|
|
954
|
-
.action(async () => {
|
|
955
|
-
console.log('šļø Uninstalling TTC CLI...');
|
|
956
|
-
if (process.platform === 'win32') {
|
|
957
|
-
console.log('On Windows there is no service to remove.');
|
|
958
|
-
console.log('To remove TTC completely:');
|
|
959
|
-
console.log(' 1. Delete the ttc executable or folder');
|
|
960
|
-
console.log(' 2. Remove config: rmdir /s /q "%USERPROFILE%\\.talk-to-code"');
|
|
961
|
-
}
|
|
962
|
-
else {
|
|
963
|
-
let installScript = path.join(__dirname, 'install-service.sh');
|
|
964
|
-
if (!fsSync.existsSync(installScript))
|
|
965
|
-
installScript = path.join(CONFIG_DIR, 'install-service.sh');
|
|
966
|
-
if (!fsSync.existsSync(installScript))
|
|
967
|
-
installScript = path.join(__dirname, '..', 'install-service.sh');
|
|
968
|
-
const scriptDir = path.dirname(installScript);
|
|
969
|
-
try {
|
|
970
|
-
await fs.access(installScript);
|
|
971
|
-
const { execSync } = await import('child_process');
|
|
972
|
-
execSync(`bash "${installScript}" --uninstall`, {
|
|
973
|
-
stdio: 'inherit',
|
|
974
|
-
cwd: scriptDir,
|
|
975
|
-
env: { ...process.env, PATH: process.env.PATH }
|
|
976
|
-
});
|
|
977
|
-
console.log('\nā Service uninstalled!');
|
|
978
|
-
console.log('To remove TTC completely, delete the binary:');
|
|
979
|
-
console.log(' rm ~/.local/bin/ttc');
|
|
980
|
-
}
|
|
981
|
-
catch (error) {
|
|
982
|
-
console.log('\nā Service uninstall script not found or failed.');
|
|
983
|
-
console.log('To remove TTC completely, delete:');
|
|
984
|
-
console.log(' rm ~/.local/bin/ttc');
|
|
985
|
-
console.log(' rm -rf ~/.talk-to-code');
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
// Exit after uninstall is complete
|
|
989
|
-
process.exit(0);
|
|
990
|
-
});
|
|
991
|
-
// Update command
|
|
992
|
-
program
|
|
993
|
-
.command('update')
|
|
994
|
-
.description('Check for and apply CLI updates')
|
|
995
|
-
.option('-f, --force', 'Update without confirmation')
|
|
996
|
-
.action(async (options) => {
|
|
997
|
-
await selfUpdate(options.force);
|
|
998
|
-
process.exit(0);
|
|
999
|
-
});
|
|
1000
|
-
program
|
|
1001
|
-
.command('check-update')
|
|
1002
|
-
.description('Check for updates')
|
|
1003
|
-
.action(async () => {
|
|
1004
|
-
const info = await checkForUpdate();
|
|
1005
|
-
if (!info)
|
|
1006
|
-
process.exit(1);
|
|
1007
|
-
if (!info.updateAvailable) {
|
|
1008
|
-
console.log('ā Up to date');
|
|
1009
|
-
process.exit(0);
|
|
1010
|
-
}
|
|
1011
|
-
console.log('š¦ Update available');
|
|
1012
|
-
console.log('Run "ttc update" to install');
|
|
1013
|
-
process.exit(0);
|
|
1014
|
-
});
|
|
1015
|
-
// Run (connect to backend and process prompts)
|
|
1016
|
-
program
|
|
1017
|
-
.command('run')
|
|
1018
|
-
.description('Run CLI and connect to backend')
|
|
1019
|
-
.option('--email <email>', 'Email for device approval')
|
|
1020
|
-
.action((options) => {
|
|
1021
|
-
runDaemon(false, options.email).catch((error) => {
|
|
1022
|
-
console.error('Run error:', error.message);
|
|
1023
|
-
process.exit(1);
|
|
1024
|
-
});
|
|
1025
|
-
});
|
|
1026
|
-
// Hidden daemon command (internal alias)
|
|
1027
|
-
program
|
|
1028
|
-
.command('daemon', { hidden: true })
|
|
1029
|
-
.description('Run daemon (internal)')
|
|
1030
|
-
.option('--foreground', 'Run in foreground', false)
|
|
1031
|
-
.option('--email <email>', 'Email address')
|
|
1032
|
-
.action((options) => {
|
|
1033
|
-
runDaemon(options.foreground, options.email).catch((error) => {
|
|
1034
|
-
console.error('Daemon error:', error.message);
|
|
1035
|
-
process.exit(1);
|
|
1036
|
-
});
|
|
1037
|
-
});
|
|
1038
|
-
program.parse();
|