@indigoai-us/hq-cli 5.1.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/__tests__/credentials.test.d.ts +5 -0
- package/dist/__tests__/credentials.test.d.ts.map +1 -0
- package/dist/__tests__/credentials.test.js +169 -0
- package/dist/__tests__/credentials.test.js.map +1 -0
- package/dist/commands/add.d.ts +6 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +60 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/auth.d.ts +17 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +269 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/cloud-setup.d.ts +19 -0
- package/dist/commands/cloud-setup.d.ts.map +1 -0
- package/dist/commands/cloud-setup.js +206 -0
- package/dist/commands/cloud-setup.js.map +1 -0
- package/dist/commands/cloud.d.ts +16 -0
- package/dist/commands/cloud.d.ts.map +1 -0
- package/dist/commands/cloud.js +263 -0
- package/dist/commands/cloud.js.map +1 -0
- package/dist/commands/initial-upload.d.ts +67 -0
- package/dist/commands/initial-upload.d.ts.map +1 -0
- package/dist/commands/initial-upload.js +205 -0
- package/dist/commands/initial-upload.js.map +1 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +55 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/sync.d.ts +6 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +104 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +60 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/strategies/link.d.ts +7 -0
- package/dist/strategies/link.d.ts.map +1 -0
- package/dist/strategies/link.js +51 -0
- package/dist/strategies/link.js.map +1 -0
- package/dist/strategies/merge.d.ts +7 -0
- package/dist/strategies/merge.d.ts.map +1 -0
- package/dist/strategies/merge.js +110 -0
- package/dist/strategies/merge.js.map +1 -0
- package/dist/sync-worker.d.ts +11 -0
- package/dist/sync-worker.d.ts.map +1 -0
- package/dist/sync-worker.js +77 -0
- package/dist/sync-worker.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/api-client.d.ts +26 -0
- package/dist/utils/api-client.d.ts.map +1 -0
- package/dist/utils/api-client.js +87 -0
- package/dist/utils/api-client.js.map +1 -0
- package/dist/utils/credentials.d.ts +44 -0
- package/dist/utils/credentials.d.ts.map +1 -0
- package/dist/utils/credentials.js +101 -0
- package/dist/utils/credentials.js.map +1 -0
- package/dist/utils/git.d.ts +13 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +70 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/manifest.d.ts +16 -0
- package/dist/utils/manifest.d.ts.map +1 -0
- package/dist/utils/manifest.js +95 -0
- package/dist/utils/manifest.js.map +1 -0
- package/dist/utils/sync.d.ts +125 -0
- package/dist/utils/sync.d.ts.map +1 -0
- package/dist/utils/sync.js +291 -0
- package/dist/utils/sync.js.map +1 -0
- package/package.json +36 -0
- package/src/__tests__/cloud-setup.test.ts +117 -0
- package/src/__tests__/credentials.test.ts +203 -0
- package/src/__tests__/initial-upload.test.ts +414 -0
- package/src/__tests__/sync.test.ts +627 -0
- package/src/commands/add.ts +74 -0
- package/src/commands/auth.ts +303 -0
- package/src/commands/cloud-setup.ts +251 -0
- package/src/commands/cloud.ts +300 -0
- package/src/commands/initial-upload.ts +263 -0
- package/src/commands/list.ts +66 -0
- package/src/commands/sync.ts +149 -0
- package/src/commands/update.ts +71 -0
- package/src/hq-cloud.d.ts +19 -0
- package/src/index.ts +46 -0
- package/src/strategies/link.ts +62 -0
- package/src/strategies/merge.ts +142 -0
- package/src/sync-worker.ts +82 -0
- package/src/types.ts +47 -0
- package/src/utils/api-client.ts +111 -0
- package/src/utils/credentials.ts +124 -0
- package/src/utils/git.ts +74 -0
- package/src/utils/manifest.ts +111 -0
- package/src/utils/sync.ts +381 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq auth commands — login, logout, status
|
|
3
|
+
*
|
|
4
|
+
* Login flow:
|
|
5
|
+
* 1. CLI generates a unique device code
|
|
6
|
+
* 2. CLI starts a temporary localhost HTTP server to receive the callback
|
|
7
|
+
* 3. CLI opens the user's browser to the hq-cloud API auth page with the device code + callback port
|
|
8
|
+
* 4. User signs in with Clerk in the browser
|
|
9
|
+
* 5. After sign-in, the API redirects to the localhost callback with the token
|
|
10
|
+
* 6. CLI captures the token, stores it, and shuts down the server
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Command } from 'commander';
|
|
14
|
+
import * as http from 'http';
|
|
15
|
+
import * as crypto from 'crypto';
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
import {
|
|
18
|
+
readCredentials,
|
|
19
|
+
writeCredentials,
|
|
20
|
+
clearCredentials,
|
|
21
|
+
getCredentialsPath,
|
|
22
|
+
isExpired,
|
|
23
|
+
} from '../utils/credentials.js';
|
|
24
|
+
import { getApiUrl, apiRequest } from '../utils/api-client.js';
|
|
25
|
+
|
|
26
|
+
/** Port range for the localhost callback server */
|
|
27
|
+
const MIN_PORT = 19750;
|
|
28
|
+
const MAX_PORT = 19850;
|
|
29
|
+
|
|
30
|
+
/** Timeout for waiting for the browser callback (ms) */
|
|
31
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Open a URL in the user's default browser.
|
|
35
|
+
* Works on macOS, Linux, and Windows.
|
|
36
|
+
*/
|
|
37
|
+
async function openBrowser(url: string): Promise<void> {
|
|
38
|
+
const { exec } = await import('child_process');
|
|
39
|
+
const { promisify } = await import('util');
|
|
40
|
+
const execAsync = promisify(exec);
|
|
41
|
+
|
|
42
|
+
const platform = process.platform;
|
|
43
|
+
let command: string;
|
|
44
|
+
|
|
45
|
+
if (platform === 'darwin') {
|
|
46
|
+
command = `open "${url}"`;
|
|
47
|
+
} else if (platform === 'win32') {
|
|
48
|
+
command = `start "" "${url}"`;
|
|
49
|
+
} else {
|
|
50
|
+
// Linux — try xdg-open, then sensible-browser
|
|
51
|
+
command = `xdg-open "${url}" 2>/dev/null || sensible-browser "${url}" 2>/dev/null || echo "OPEN_FAILED"`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await execAsync(command);
|
|
56
|
+
} catch {
|
|
57
|
+
// Browser open failed silently — user will be shown the URL to open manually
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Find an available port in the callback port range.
|
|
63
|
+
*/
|
|
64
|
+
function findAvailablePort(): Promise<number> {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const port = MIN_PORT + Math.floor(Math.random() * (MAX_PORT - MIN_PORT));
|
|
67
|
+
const server = http.createServer();
|
|
68
|
+
server.listen(port, '127.0.0.1', () => {
|
|
69
|
+
server.close(() => resolve(port));
|
|
70
|
+
});
|
|
71
|
+
server.on('error', () => {
|
|
72
|
+
// Try another port
|
|
73
|
+
const fallback = MIN_PORT + Math.floor(Math.random() * (MAX_PORT - MIN_PORT));
|
|
74
|
+
const server2 = http.createServer();
|
|
75
|
+
server2.listen(fallback, '127.0.0.1', () => {
|
|
76
|
+
server2.close(() => resolve(fallback));
|
|
77
|
+
});
|
|
78
|
+
server2.on('error', () => {
|
|
79
|
+
reject(new Error('Could not find an available port for auth callback'));
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* HTML page shown to the user after successful login.
|
|
87
|
+
*/
|
|
88
|
+
function successHtml(): string {
|
|
89
|
+
return `<!DOCTYPE html>
|
|
90
|
+
<html>
|
|
91
|
+
<head><title>HQ CLI — Logged In</title></head>
|
|
92
|
+
<body style="font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #f8f9fa;">
|
|
93
|
+
<div style="text-align: center; max-width: 400px;">
|
|
94
|
+
<h1 style="color: #16a34a;">Logged In</h1>
|
|
95
|
+
<p style="color: #4b5563;">You have been authenticated. You can close this tab and return to the terminal.</p>
|
|
96
|
+
</div>
|
|
97
|
+
</body>
|
|
98
|
+
</html>`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* HTML page shown on error.
|
|
103
|
+
*/
|
|
104
|
+
function errorHtml(message: string): string {
|
|
105
|
+
return `<!DOCTYPE html>
|
|
106
|
+
<html>
|
|
107
|
+
<head><title>HQ CLI — Auth Error</title></head>
|
|
108
|
+
<body style="font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #f8f9fa;">
|
|
109
|
+
<div style="text-align: center; max-width: 400px;">
|
|
110
|
+
<h1 style="color: #dc2626;">Authentication Error</h1>
|
|
111
|
+
<p style="color: #4b5563;">${message}</p>
|
|
112
|
+
<p style="color: #6b7280;">Please return to the terminal and try again.</p>
|
|
113
|
+
</div>
|
|
114
|
+
</body>
|
|
115
|
+
</html>`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Start a temporary localhost server and wait for the auth callback.
|
|
120
|
+
* Returns the received token and user info.
|
|
121
|
+
*/
|
|
122
|
+
function waitForCallback(port: number): Promise<{ token: string; userId: string; email?: string; expiresAt?: string }> {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const server = http.createServer((req, res) => {
|
|
125
|
+
const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
|
|
126
|
+
|
|
127
|
+
if (url.pathname === '/callback') {
|
|
128
|
+
const token = url.searchParams.get('token');
|
|
129
|
+
const userId = url.searchParams.get('user_id');
|
|
130
|
+
const email = url.searchParams.get('email') ?? undefined;
|
|
131
|
+
const expiresAt = url.searchParams.get('expires_at') ?? undefined;
|
|
132
|
+
const error = url.searchParams.get('error');
|
|
133
|
+
|
|
134
|
+
if (error) {
|
|
135
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
136
|
+
res.end(errorHtml(error));
|
|
137
|
+
server.close();
|
|
138
|
+
reject(new Error(error));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!token || !userId) {
|
|
143
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
144
|
+
res.end(errorHtml('Missing token or user ID in callback'));
|
|
145
|
+
server.close();
|
|
146
|
+
reject(new Error('Invalid callback: missing token or user_id'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
151
|
+
res.end(successHtml());
|
|
152
|
+
|
|
153
|
+
// Close the server after sending the response
|
|
154
|
+
server.close();
|
|
155
|
+
resolve({ token, userId, email, expiresAt });
|
|
156
|
+
} else {
|
|
157
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
158
|
+
res.end('Not Found');
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
server.listen(port, '127.0.0.1', () => {
|
|
163
|
+
// Server is ready
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Timeout
|
|
167
|
+
const timeout = setTimeout(() => {
|
|
168
|
+
server.close();
|
|
169
|
+
reject(new Error('Login timed out. Please try again.'));
|
|
170
|
+
}, LOGIN_TIMEOUT_MS);
|
|
171
|
+
|
|
172
|
+
server.on('close', () => {
|
|
173
|
+
clearTimeout(timeout);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
server.on('error', (err) => {
|
|
177
|
+
clearTimeout(timeout);
|
|
178
|
+
reject(err);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Register the "hq auth" command group with login, logout, and status subcommands.
|
|
185
|
+
*/
|
|
186
|
+
export function registerAuthCommand(program: Command): void {
|
|
187
|
+
const authCmd = program
|
|
188
|
+
.command('auth')
|
|
189
|
+
.description('Authenticate with HQ Cloud');
|
|
190
|
+
|
|
191
|
+
// --- hq auth login ---
|
|
192
|
+
authCmd
|
|
193
|
+
.command('login')
|
|
194
|
+
.description('Log in to HQ Cloud via browser')
|
|
195
|
+
.action(async () => {
|
|
196
|
+
try {
|
|
197
|
+
// Check if already logged in
|
|
198
|
+
const existing = readCredentials();
|
|
199
|
+
if (existing && !isExpired(existing)) {
|
|
200
|
+
const label = existing.email ?? existing.userId;
|
|
201
|
+
console.log(chalk.yellow(`Already logged in as ${label}.`));
|
|
202
|
+
console.log('Run "hq auth logout" first to switch accounts.');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Generate a device code for the login session
|
|
207
|
+
const deviceCode = crypto.randomBytes(16).toString('hex');
|
|
208
|
+
|
|
209
|
+
// Find an available port for the callback
|
|
210
|
+
const port = await findAvailablePort();
|
|
211
|
+
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
212
|
+
|
|
213
|
+
// Build the login URL
|
|
214
|
+
const apiUrl = getApiUrl();
|
|
215
|
+
const loginUrl = `${apiUrl}/auth/cli-login?device_code=${deviceCode}&callback_url=${encodeURIComponent(callbackUrl)}`;
|
|
216
|
+
|
|
217
|
+
console.log(chalk.blue('Opening browser for authentication...'));
|
|
218
|
+
console.log();
|
|
219
|
+
console.log(`If the browser does not open, visit this URL:`);
|
|
220
|
+
console.log(chalk.underline(loginUrl));
|
|
221
|
+
console.log();
|
|
222
|
+
console.log(chalk.dim('Waiting for authentication (timeout: 5 minutes)...'));
|
|
223
|
+
|
|
224
|
+
// Open browser
|
|
225
|
+
await openBrowser(loginUrl);
|
|
226
|
+
|
|
227
|
+
// Wait for callback
|
|
228
|
+
const result = await waitForCallback(port);
|
|
229
|
+
|
|
230
|
+
// Store credentials
|
|
231
|
+
writeCredentials({
|
|
232
|
+
token: result.token,
|
|
233
|
+
userId: result.userId,
|
|
234
|
+
email: result.email,
|
|
235
|
+
storedAt: new Date().toISOString(),
|
|
236
|
+
expiresAt: result.expiresAt,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const label = result.email ?? result.userId;
|
|
240
|
+
console.log();
|
|
241
|
+
console.log(chalk.green(`Logged in as ${label}`));
|
|
242
|
+
console.log(chalk.dim(`Credentials saved to ${getCredentialsPath()}`));
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error(chalk.red('Login failed:'), error instanceof Error ? error.message : error);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// --- hq auth logout ---
|
|
250
|
+
authCmd
|
|
251
|
+
.command('logout')
|
|
252
|
+
.description('Log out and clear stored credentials')
|
|
253
|
+
.action(() => {
|
|
254
|
+
const removed = clearCredentials();
|
|
255
|
+
if (removed) {
|
|
256
|
+
console.log(chalk.green('Logged out. Credentials cleared.'));
|
|
257
|
+
} else {
|
|
258
|
+
console.log(chalk.yellow('Not logged in.'));
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// --- hq auth status ---
|
|
263
|
+
authCmd
|
|
264
|
+
.command('status')
|
|
265
|
+
.description('Show current authentication status')
|
|
266
|
+
.action(async () => {
|
|
267
|
+
const creds = readCredentials();
|
|
268
|
+
|
|
269
|
+
if (!creds) {
|
|
270
|
+
console.log(chalk.yellow('Not logged in.'));
|
|
271
|
+
console.log('Run "hq auth login" to authenticate.');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (isExpired(creds)) {
|
|
276
|
+
console.log(chalk.red('Session expired.'));
|
|
277
|
+
console.log('Run "hq auth login" to re-authenticate.');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const label = creds.email ?? creds.userId;
|
|
282
|
+
console.log(chalk.green(`Logged in as ${label}`));
|
|
283
|
+
console.log(` User ID: ${creds.userId}`);
|
|
284
|
+
console.log(` Stored at: ${creds.storedAt}`);
|
|
285
|
+
if (creds.expiresAt) {
|
|
286
|
+
console.log(` Expires at: ${creds.expiresAt}`);
|
|
287
|
+
}
|
|
288
|
+
console.log(` API URL: ${getApiUrl()}`);
|
|
289
|
+
console.log(` Creds file: ${getCredentialsPath()}`);
|
|
290
|
+
|
|
291
|
+
// Optionally verify with the API
|
|
292
|
+
try {
|
|
293
|
+
const resp = await apiRequest<{ userId: string; sessionId: string }>('GET', '/auth/me');
|
|
294
|
+
if (resp.ok && resp.data) {
|
|
295
|
+
console.log(chalk.dim(` Verified: API confirms session is valid`));
|
|
296
|
+
} else {
|
|
297
|
+
console.log(chalk.yellow(` Warning: API returned ${resp.status} — token may be invalid`));
|
|
298
|
+
}
|
|
299
|
+
} catch {
|
|
300
|
+
console.log(chalk.dim(` Note: Could not reach API to verify token`));
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq cloud commands — cloud session management
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* - setup-token: Walk user through generating and storing a Claude OAuth token
|
|
6
|
+
* - status: Show cloud readiness (auth state + Claude token state)
|
|
7
|
+
* - upload: Initial HQ file upload to cloud storage
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import * as readline from 'readline';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import {
|
|
14
|
+
readCredentials,
|
|
15
|
+
isExpired,
|
|
16
|
+
} from '../utils/credentials.js';
|
|
17
|
+
import { apiRequest, getApiUrl } from '../utils/api-client.js';
|
|
18
|
+
import { findHqRoot } from '../utils/manifest.js';
|
|
19
|
+
import { runInitialUpload } from './initial-upload.js';
|
|
20
|
+
|
|
21
|
+
/** Minimum token length for basic validation */
|
|
22
|
+
const MIN_TOKEN_LENGTH = 20;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate a Claude OAuth token string.
|
|
26
|
+
* Returns null if valid, or an error message string if invalid.
|
|
27
|
+
*/
|
|
28
|
+
export function validateClaudeToken(token: string): string | null {
|
|
29
|
+
if (!token || token.trim().length === 0) {
|
|
30
|
+
return 'Token cannot be empty.';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const trimmed = token.trim();
|
|
34
|
+
|
|
35
|
+
if (trimmed.length < MIN_TOKEN_LENGTH) {
|
|
36
|
+
return `Token is too short (${trimmed.length} chars). Claude tokens are typically much longer. Please check you copied the full token.`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Reject tokens that look like they contain whitespace in the middle (copy-paste artifacts)
|
|
40
|
+
if (/\s/.test(trimmed)) {
|
|
41
|
+
return 'Token contains whitespace. Please ensure you copied it correctly without line breaks.';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Prompt the user for input on stdin.
|
|
49
|
+
* Returns the entered string (trimmed).
|
|
50
|
+
*/
|
|
51
|
+
function promptUser(question: string): Promise<string> {
|
|
52
|
+
const rl = readline.createInterface({
|
|
53
|
+
input: process.stdin,
|
|
54
|
+
output: process.stdout,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
rl.question(question, (answer) => {
|
|
59
|
+
rl.close();
|
|
60
|
+
resolve(answer.trim());
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register the "hq cloud" command group with setup-token and status subcommands.
|
|
67
|
+
*/
|
|
68
|
+
export function registerCloudSetupCommand(program: Command): void {
|
|
69
|
+
const cloudCmd = program
|
|
70
|
+
.command('cloud')
|
|
71
|
+
.description('Cloud session management — token setup and status');
|
|
72
|
+
|
|
73
|
+
// --- hq cloud setup-token ---
|
|
74
|
+
cloudCmd
|
|
75
|
+
.command('setup-token')
|
|
76
|
+
.description('Set up your Claude OAuth token for cloud sessions')
|
|
77
|
+
.action(async () => {
|
|
78
|
+
try {
|
|
79
|
+
// Check auth first (AC #6)
|
|
80
|
+
const creds = readCredentials();
|
|
81
|
+
if (!creds || isExpired(creds)) {
|
|
82
|
+
console.log(chalk.red('Not logged in to HQ Cloud.'));
|
|
83
|
+
console.log('Run "hq auth login" first, then try again.');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(chalk.blue('Claude Token Setup'));
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log('To launch cloud sessions, HQ needs your Claude OAuth token.');
|
|
90
|
+
console.log('This token lets cloud containers run Claude on your behalf.');
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(chalk.yellow('Step 1:') + ' Open a terminal and run:');
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(chalk.cyan(' claude setup-token'));
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log(chalk.yellow('Step 2:') + ' Copy the token output and paste it below.');
|
|
97
|
+
console.log('');
|
|
98
|
+
|
|
99
|
+
// Prompt for the token
|
|
100
|
+
const token = await promptUser('Paste your Claude token: ');
|
|
101
|
+
|
|
102
|
+
// Validate format (AC #2)
|
|
103
|
+
const validationError = validateClaudeToken(token);
|
|
104
|
+
if (validationError) {
|
|
105
|
+
console.log('');
|
|
106
|
+
console.log(chalk.red('Invalid token: ') + validationError);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Send to API (AC #3)
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(chalk.dim('Storing token securely...'));
|
|
113
|
+
|
|
114
|
+
const resp = await apiRequest<{ ok: boolean; hasToken: boolean; setAt: string | null }>(
|
|
115
|
+
'POST',
|
|
116
|
+
'/api/settings/claude-token',
|
|
117
|
+
{ token: token.trim() },
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (!resp.ok) {
|
|
121
|
+
console.log(chalk.red('Failed to store token: ') + (resp.error ?? `HTTP ${resp.status}`));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Success (AC #4)
|
|
126
|
+
console.log('');
|
|
127
|
+
console.log(chalk.green('Claude token stored securely.'));
|
|
128
|
+
if (resp.data?.setAt) {
|
|
129
|
+
console.log(chalk.dim(` Set at: ${resp.data.setAt}`));
|
|
130
|
+
}
|
|
131
|
+
console.log('');
|
|
132
|
+
console.log('You can now launch cloud sessions with "hq cloud" commands.');
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(
|
|
135
|
+
chalk.red('Error:'),
|
|
136
|
+
error instanceof Error ? error.message : error,
|
|
137
|
+
);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// --- hq cloud status ---
|
|
143
|
+
cloudCmd
|
|
144
|
+
.command('status')
|
|
145
|
+
.description('Show cloud readiness — authentication and Claude token status')
|
|
146
|
+
.action(async () => {
|
|
147
|
+
try {
|
|
148
|
+
console.log(chalk.blue('HQ Cloud Status'));
|
|
149
|
+
console.log('');
|
|
150
|
+
|
|
151
|
+
// 1. Auth status
|
|
152
|
+
const creds = readCredentials();
|
|
153
|
+
if (!creds) {
|
|
154
|
+
console.log(` Auth: ${chalk.red('Not logged in')}`);
|
|
155
|
+
console.log(` Claude Token: ${chalk.dim('unknown (login first)')}`);
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log('Run "hq auth login" to get started.');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (isExpired(creds)) {
|
|
162
|
+
console.log(` Auth: ${chalk.red('Session expired')}`);
|
|
163
|
+
console.log(` Claude Token: ${chalk.dim('unknown (login first)')}`);
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log('Run "hq auth login" to re-authenticate.');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const label = creds.email ?? creds.userId;
|
|
170
|
+
console.log(` Auth: ${chalk.green('Logged in')} as ${label}`);
|
|
171
|
+
console.log(` API: ${getApiUrl()}`);
|
|
172
|
+
|
|
173
|
+
// 2. Claude token status (AC #5)
|
|
174
|
+
try {
|
|
175
|
+
const resp = await apiRequest<{ hasToken: boolean; setAt: string | null }>(
|
|
176
|
+
'GET',
|
|
177
|
+
'/api/settings/claude-token',
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (resp.ok && resp.data) {
|
|
181
|
+
if (resp.data.hasToken) {
|
|
182
|
+
console.log(` Claude Token: ${chalk.green('Configured')}`);
|
|
183
|
+
if (resp.data.setAt) {
|
|
184
|
+
console.log(` Token Set At: ${resp.data.setAt}`);
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
console.log(` Claude Token: ${chalk.yellow('Not configured')}`);
|
|
188
|
+
console.log('');
|
|
189
|
+
console.log('Run "hq cloud setup-token" to configure your Claude token.');
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
console.log(` Claude Token: ${chalk.yellow('Could not check')} (API returned ${resp.status})`);
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
console.log(` Claude Token: ${chalk.dim('Could not reach API')}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log('');
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error(
|
|
201
|
+
chalk.red('Error:'),
|
|
202
|
+
error instanceof Error ? error.message : error,
|
|
203
|
+
);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// --- hq cloud upload ---
|
|
209
|
+
cloudCmd
|
|
210
|
+
.command('upload')
|
|
211
|
+
.description('Upload local HQ files to cloud storage (initial setup)')
|
|
212
|
+
.option('--hq-root <path>', 'Path to HQ root directory (auto-detected if omitted)')
|
|
213
|
+
.option('--on-conflict <action>', 'Action when remote has files: merge, replace, or skip')
|
|
214
|
+
.action(async (opts: { hqRoot?: string; onConflict?: string }) => {
|
|
215
|
+
try {
|
|
216
|
+
// Require auth
|
|
217
|
+
const creds = readCredentials();
|
|
218
|
+
if (!creds || isExpired(creds)) {
|
|
219
|
+
console.log(chalk.red('Not logged in to HQ Cloud.'));
|
|
220
|
+
console.log('Run "hq auth login" first, then try again.');
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const hqRoot = opts.hqRoot ?? findHqRoot();
|
|
225
|
+
|
|
226
|
+
console.log(chalk.blue('HQ Cloud — Initial Upload'));
|
|
227
|
+
console.log(chalk.dim(` HQ root: ${hqRoot}`));
|
|
228
|
+
console.log('');
|
|
229
|
+
|
|
230
|
+
const onConflict = opts.onConflict as 'merge' | 'replace' | 'skip' | undefined;
|
|
231
|
+
|
|
232
|
+
const result = await runInitialUpload(hqRoot, {
|
|
233
|
+
onConflict,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (result.skipped) {
|
|
237
|
+
process.exit(0);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (result.failed > 0) {
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error(
|
|
245
|
+
chalk.red('Upload failed:'),
|
|
246
|
+
error instanceof Error ? error.message : error,
|
|
247
|
+
);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|