@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.
Files changed (102) hide show
  1. package/dist/__tests__/credentials.test.d.ts +5 -0
  2. package/dist/__tests__/credentials.test.d.ts.map +1 -0
  3. package/dist/__tests__/credentials.test.js +169 -0
  4. package/dist/__tests__/credentials.test.js.map +1 -0
  5. package/dist/commands/add.d.ts +6 -0
  6. package/dist/commands/add.d.ts.map +1 -0
  7. package/dist/commands/add.js +60 -0
  8. package/dist/commands/add.js.map +1 -0
  9. package/dist/commands/auth.d.ts +17 -0
  10. package/dist/commands/auth.d.ts.map +1 -0
  11. package/dist/commands/auth.js +269 -0
  12. package/dist/commands/auth.js.map +1 -0
  13. package/dist/commands/cloud-setup.d.ts +19 -0
  14. package/dist/commands/cloud-setup.d.ts.map +1 -0
  15. package/dist/commands/cloud-setup.js +206 -0
  16. package/dist/commands/cloud-setup.js.map +1 -0
  17. package/dist/commands/cloud.d.ts +16 -0
  18. package/dist/commands/cloud.d.ts.map +1 -0
  19. package/dist/commands/cloud.js +263 -0
  20. package/dist/commands/cloud.js.map +1 -0
  21. package/dist/commands/initial-upload.d.ts +67 -0
  22. package/dist/commands/initial-upload.d.ts.map +1 -0
  23. package/dist/commands/initial-upload.js +205 -0
  24. package/dist/commands/initial-upload.js.map +1 -0
  25. package/dist/commands/list.d.ts +6 -0
  26. package/dist/commands/list.d.ts.map +1 -0
  27. package/dist/commands/list.js +55 -0
  28. package/dist/commands/list.js.map +1 -0
  29. package/dist/commands/sync.d.ts +6 -0
  30. package/dist/commands/sync.d.ts.map +1 -0
  31. package/dist/commands/sync.js +104 -0
  32. package/dist/commands/sync.js.map +1 -0
  33. package/dist/commands/update.d.ts +7 -0
  34. package/dist/commands/update.d.ts.map +1 -0
  35. package/dist/commands/update.js +60 -0
  36. package/dist/commands/update.js.map +1 -0
  37. package/dist/index.d.ts +6 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +36 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/strategies/link.d.ts +7 -0
  42. package/dist/strategies/link.d.ts.map +1 -0
  43. package/dist/strategies/link.js +51 -0
  44. package/dist/strategies/link.js.map +1 -0
  45. package/dist/strategies/merge.d.ts +7 -0
  46. package/dist/strategies/merge.d.ts.map +1 -0
  47. package/dist/strategies/merge.js +110 -0
  48. package/dist/strategies/merge.js.map +1 -0
  49. package/dist/sync-worker.d.ts +11 -0
  50. package/dist/sync-worker.d.ts.map +1 -0
  51. package/dist/sync-worker.js +77 -0
  52. package/dist/sync-worker.js.map +1 -0
  53. package/dist/types.d.ts +41 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +5 -0
  56. package/dist/types.js.map +1 -0
  57. package/dist/utils/api-client.d.ts +26 -0
  58. package/dist/utils/api-client.d.ts.map +1 -0
  59. package/dist/utils/api-client.js +87 -0
  60. package/dist/utils/api-client.js.map +1 -0
  61. package/dist/utils/credentials.d.ts +44 -0
  62. package/dist/utils/credentials.d.ts.map +1 -0
  63. package/dist/utils/credentials.js +101 -0
  64. package/dist/utils/credentials.js.map +1 -0
  65. package/dist/utils/git.d.ts +13 -0
  66. package/dist/utils/git.d.ts.map +1 -0
  67. package/dist/utils/git.js +70 -0
  68. package/dist/utils/git.js.map +1 -0
  69. package/dist/utils/manifest.d.ts +16 -0
  70. package/dist/utils/manifest.d.ts.map +1 -0
  71. package/dist/utils/manifest.js +95 -0
  72. package/dist/utils/manifest.js.map +1 -0
  73. package/dist/utils/sync.d.ts +125 -0
  74. package/dist/utils/sync.d.ts.map +1 -0
  75. package/dist/utils/sync.js +291 -0
  76. package/dist/utils/sync.js.map +1 -0
  77. package/package.json +36 -0
  78. package/src/__tests__/cloud-setup.test.ts +117 -0
  79. package/src/__tests__/credentials.test.ts +203 -0
  80. package/src/__tests__/initial-upload.test.ts +414 -0
  81. package/src/__tests__/sync.test.ts +627 -0
  82. package/src/commands/add.ts +74 -0
  83. package/src/commands/auth.ts +303 -0
  84. package/src/commands/cloud-setup.ts +251 -0
  85. package/src/commands/cloud.ts +300 -0
  86. package/src/commands/initial-upload.ts +263 -0
  87. package/src/commands/list.ts +66 -0
  88. package/src/commands/sync.ts +149 -0
  89. package/src/commands/update.ts +71 -0
  90. package/src/hq-cloud.d.ts +19 -0
  91. package/src/index.ts +46 -0
  92. package/src/strategies/link.ts +62 -0
  93. package/src/strategies/merge.ts +142 -0
  94. package/src/sync-worker.ts +82 -0
  95. package/src/types.ts +47 -0
  96. package/src/utils/api-client.ts +111 -0
  97. package/src/utils/credentials.ts +124 -0
  98. package/src/utils/git.ts +74 -0
  99. package/src/utils/manifest.ts +111 -0
  100. package/src/utils/sync.ts +381 -0
  101. package/tsconfig.json +9 -0
  102. 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
+ }