@masslessai/push-todo 3.0.0 → 3.2.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/lib/connect.js CHANGED
@@ -1,62 +1,611 @@
1
1
  /**
2
- * Connect and authentication module for Push CLI.
2
+ * Connect and authentication module for Push CLI (Doctor Mode).
3
3
  *
4
- * Handles the "doctor" flow for setting up and validating
5
- * the CLI connection to Push backend.
4
+ * Comprehensive health check and connect tool:
5
+ * - Version check: Compare local vs remote plugin version
6
+ * - API validation: Verify API key is still valid
7
+ * - Project registration: Register current project with keywords
8
+ * - Authentication: Handle initial auth or re-auth when needed
9
+ * - E2EE setup: Compile Swift helper, import encryption key
10
+ * - Machine validation: Multi-Mac coordination
11
+ *
12
+ * Ported from: plugins/push-todo/scripts/connect.py (1866 lines)
6
13
  */
7
14
 
8
- import { execSync } from 'child_process';
9
- import { setTimeout } from 'timers/promises';
15
+ import { execSync, spawnSync, spawn } from 'child_process';
16
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, statSync } from 'fs';
17
+ import { setTimeout as sleep } from 'timers/promises';
18
+ import { homedir } from 'os';
19
+ import { join, dirname } from 'path';
20
+ import { fileURLToPath } from 'url';
21
+ import * as readline from 'readline';
10
22
  import * as api from './api.js';
11
- import { getApiKey, saveCredentials, getConfigValue } from './config.js';
23
+ import { getApiKey, saveCredentials, clearCredentials, getConfigValue, getEmail } from './config.js';
12
24
  import { getMachineId, getMachineName } from './machine-id.js';
13
25
  import { getRegistry } from './project-registry.js';
14
- import { getGitRemote, isGitRepo, getGitRoot } from './utils/git.js';
26
+ import { getGitRemote, isGitRepo, getGitRoot, normalizeGitRemote } from './utils/git.js';
15
27
  import { isE2EEAvailable } from './encryption.js';
16
- import { bold, green, yellow, red, cyan, dim, muted } from './utils/colors.js';
28
+ import { ensureDaemonRunning } from './daemon-health.js';
29
+ import { bold, green, yellow, red, cyan, dim } from './utils/colors.js';
30
+
31
+ const __filename = fileURLToPath(import.meta.url);
32
+ const __dirname = dirname(__filename);
17
33
 
18
34
  // Supabase anonymous key for auth flow
19
35
  const ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imp4dXpxY2JxaGlheG1maXR6eGxvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzI0ODA5MjIsImV4cCI6MjA0ODA1NjkyMn0.Qxov5qJTVLWmseyFNhBQBJN7-t5sXlHZyzFKhSN_e5g';
36
+ const API_BASE = 'https://jxuzqcbqhiaxmfitzxlo.supabase.co/functions/v1';
20
37
 
21
- const VERSION = '3.0.0';
38
+ // Remote URLs for updates
39
+ const REMOTE_PACKAGE_JSON_URL = 'https://raw.githubusercontent.com/MasslessAI/push-todo-cli/main/npm/push-todo/package.json';
40
+
41
+ // Get version from package.json
42
+ function getVersion() {
43
+ try {
44
+ const pkgPath = join(__dirname, '..', 'package.json');
45
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
46
+ return pkg.version || '3.0.0';
47
+ } catch {
48
+ return '3.0.0';
49
+ }
50
+ }
51
+
52
+ const VERSION = getVersion();
53
+
54
+ // Client types
55
+ const CLIENT_NAMES = {
56
+ 'claude-code': 'Claude Code',
57
+ 'openai-codex': 'OpenAI Codex',
58
+ 'clawdbot': 'Clawdbot'
59
+ };
60
+
61
+ // ============================================================================
62
+ // INSTALLATION METHOD DETECTION
63
+ // ============================================================================
22
64
 
23
65
  /**
24
- * Check if a newer version is available.
66
+ * Detect how the package was installed.
25
67
  *
26
- * @returns {Promise<Object>} Version check result
68
+ * Returns:
69
+ * "npm-global" - Installed via npm install -g
70
+ * "npm-local" - Installed locally in node_modules
71
+ * "development" - Linked for development
72
+ */
73
+ function getInstallationMethod() {
74
+ const pkgPath = join(__dirname, '..');
75
+
76
+ // Check if it's a symlink (development setup)
77
+ try {
78
+ const stats = statSync(pkgPath, { throwIfNoEntry: false });
79
+ if (stats?.isSymbolicLink?.()) {
80
+ return 'development';
81
+ }
82
+ } catch {}
83
+
84
+ // Check if in node_modules (local install)
85
+ if (pkgPath.includes('node_modules')) {
86
+ return 'npm-local';
87
+ }
88
+
89
+ // Default to global npm install
90
+ return 'npm-global';
91
+ }
92
+
93
+ // ============================================================================
94
+ // E2EE SETUP (End-to-End Encryption)
95
+ // ============================================================================
96
+
97
+ /**
98
+ * Get the plugin/package root directory.
99
+ */
100
+ function getPluginRoot() {
101
+ if (process.env.CLAUDE_PLUGIN_ROOT) {
102
+ return process.env.CLAUDE_PLUGIN_ROOT;
103
+ }
104
+ return join(__dirname, '..');
105
+ }
106
+
107
+ /**
108
+ * Get path to the Swift keychain helper binary.
109
+ */
110
+ function getSwiftHelperPath() {
111
+ return join(getPluginRoot(), 'bin', 'push-keychain-helper');
112
+ }
113
+
114
+ /**
115
+ * Get path to the Swift keychain helper source.
116
+ */
117
+ function getSwiftSourcePath() {
118
+ return join(getPluginRoot(), 'natives', 'KeychainHelper.swift');
119
+ }
120
+
121
+ /**
122
+ * Check if Swift compiler is available.
123
+ */
124
+ function checkSwiftcAvailable() {
125
+ try {
126
+ const result = spawnSync('which', ['swiftc'], { timeout: 5000 });
127
+ return result.status === 0;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Check if E2EE key exists in keychain.
135
+ */
136
+ function checkE2EEKeyExists() {
137
+ const helperPath = getSwiftHelperPath();
138
+ if (!existsSync(helperPath)) {
139
+ return false;
140
+ }
141
+
142
+ try {
143
+ const result = spawnSync(helperPath, ['--check'], { timeout: 5000 });
144
+ return result.status === 0;
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Compile the Swift keychain helper from source.
152
+ */
153
+ function compileSwiftHelper() {
154
+ const sourcePath = getSwiftSourcePath();
155
+ const binDir = join(getPluginRoot(), 'bin');
156
+ const helperPath = getSwiftHelperPath();
157
+
158
+ // Check if source exists
159
+ if (!existsSync(sourcePath)) {
160
+ return {
161
+ status: 'no_source',
162
+ message: `Swift source not found at ${sourcePath}`
163
+ };
164
+ }
165
+
166
+ // Check for Swift compiler
167
+ if (!checkSwiftcAvailable()) {
168
+ return {
169
+ status: 'no_swiftc',
170
+ message: 'Swift compiler not found. Install Xcode Command Line Tools.'
171
+ };
172
+ }
173
+
174
+ // Create bin directory
175
+ mkdirSync(binDir, { recursive: true });
176
+
177
+ // Compile
178
+ try {
179
+ const result = spawnSync('swiftc', ['-O', sourcePath, '-o', helperPath], {
180
+ timeout: 60000,
181
+ encoding: 'utf8'
182
+ });
183
+
184
+ if (result.status === 0) {
185
+ return {
186
+ status: 'success',
187
+ message: 'Compiled encryption helper from source',
188
+ path: helperPath
189
+ };
190
+ } else {
191
+ return {
192
+ status: 'compile_error',
193
+ message: `Compilation failed: ${result.stderr}`
194
+ };
195
+ }
196
+ } catch (error) {
197
+ return {
198
+ status: 'compile_error',
199
+ message: `Compilation error: ${error.message}`
200
+ };
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Set up E2EE support for the CLI.
206
+ */
207
+ function setupE2EE() {
208
+ const helperPath = getSwiftHelperPath();
209
+ const sourcePath = getSwiftSourcePath();
210
+
211
+ // Case 1: Helper already exists
212
+ if (existsSync(helperPath)) {
213
+ const keyExists = checkE2EEKeyExists();
214
+ if (keyExists) {
215
+ return {
216
+ status: 'ready',
217
+ message: 'E2EE ready',
218
+ keyAvailable: true
219
+ };
220
+ } else {
221
+ return {
222
+ status: 'not_enabled',
223
+ message: 'E2EE helper ready, but no key found (enable in iOS app)',
224
+ keyAvailable: false
225
+ };
226
+ }
227
+ }
228
+
229
+ // Case 2: Need to compile helper
230
+ if (existsSync(sourcePath)) {
231
+ if (checkSwiftcAvailable()) {
232
+ const compileResult = compileSwiftHelper();
233
+
234
+ if (compileResult.status === 'success') {
235
+ const keyExists = checkE2EEKeyExists();
236
+ return {
237
+ status: 'compiled',
238
+ message: 'Compiled encryption helper from source',
239
+ keyAvailable: keyExists,
240
+ sourcePath
241
+ };
242
+ } else {
243
+ return {
244
+ status: 'error',
245
+ message: compileResult.message,
246
+ keyAvailable: false
247
+ };
248
+ }
249
+ } else {
250
+ return {
251
+ status: 'needs_setup',
252
+ message: 'Swift compiler not found',
253
+ keyAvailable: false,
254
+ options: [
255
+ 'Install Xcode Command Line Tools: xcode-select --install',
256
+ 'Or use pre-signed binary (downloaded during npm install)'
257
+ ],
258
+ sourcePath
259
+ };
260
+ }
261
+ }
262
+
263
+ // Case 3: No source file - check if binary was downloaded
264
+ return {
265
+ status: 'error',
266
+ message: 'E2EE helper not found. Run: npm rebuild @masslessai/push-todo',
267
+ keyAvailable: false
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Store E2EE key directly without interactive prompt.
273
+ */
274
+ function storeE2EEKeyDirect(keyInput) {
275
+ keyInput = keyInput.trim();
276
+
277
+ // Validate format (should be base64, 44 chars for 32 bytes)
278
+ try {
279
+ const keyData = Buffer.from(keyInput, 'base64');
280
+ if (keyData.length !== 32) {
281
+ return {
282
+ status: 'error',
283
+ message: `Invalid key size: expected 32 bytes, got ${keyData.length}`
284
+ };
285
+ }
286
+ } catch {
287
+ return {
288
+ status: 'error',
289
+ message: 'Invalid base64 encoding'
290
+ };
291
+ }
292
+
293
+ // Store via Swift helper
294
+ let helperPath = getSwiftHelperPath();
295
+ if (!existsSync(helperPath)) {
296
+ const compileResult = compileSwiftHelper();
297
+ if (compileResult.status !== 'success') {
298
+ return {
299
+ status: 'error',
300
+ message: `Cannot compile helper: ${compileResult.message}`
301
+ };
302
+ }
303
+ }
304
+
305
+ try {
306
+ const result = spawnSync(helperPath, ['--store'], {
307
+ input: keyInput,
308
+ timeout: 10000,
309
+ encoding: 'utf8'
310
+ });
311
+
312
+ if (result.status === 0) {
313
+ return {
314
+ status: 'success',
315
+ message: 'Key stored in macOS Keychain'
316
+ };
317
+ } else {
318
+ return {
319
+ status: 'error',
320
+ message: `Failed to store key: ${result.stderr?.trim() || 'Unknown error'}`
321
+ };
322
+ }
323
+ } catch (error) {
324
+ return {
325
+ status: 'error',
326
+ message: `Error storing key: ${error.message}`
327
+ };
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Check if running in an interactive terminal.
333
+ */
334
+ function isInteractive() {
335
+ return process.stdin.isTTY && process.stdout.isTTY;
336
+ }
337
+
338
+ /**
339
+ * Check if user has any encrypted todos.
340
+ */
341
+ async function checkUserHasEncryptedTodos() {
342
+ try {
343
+ const apiKey = getApiKey();
344
+ if (!apiKey) return false;
345
+
346
+ const response = await fetch(
347
+ `${API_BASE}/synced-todos?is_encrypted=true&limit=1`,
348
+ {
349
+ headers: {
350
+ 'Authorization': `Bearer ${apiKey}`,
351
+ 'Content-Type': 'application/json'
352
+ }
353
+ }
354
+ );
355
+
356
+ if (!response.ok) return false;
357
+
358
+ const data = await response.json();
359
+ const todos = data.todos || data;
360
+ return Array.isArray(todos) && todos.length > 0;
361
+ } catch {
362
+ return false;
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Interactive E2EE key import (for TTY only).
368
+ */
369
+ async function importE2EEKey() {
370
+ if (!isInteractive()) {
371
+ console.log('');
372
+ console.log(' E2EE_KEY_IMPORT_AVAILABLE');
373
+ console.log(' Use: push-todo connect --store-e2ee-key <base64_key>');
374
+ return false;
375
+ }
376
+
377
+ console.log('');
378
+ console.log(' 🔐 Import Encryption Key');
379
+ console.log(' ' + '-'.repeat(38));
380
+ console.log('');
381
+ console.log(' Your Push account has E2EE enabled.');
382
+ console.log(' To decrypt tasks on this Mac, import your encryption key.');
383
+ console.log('');
384
+ console.log(' On your iPhone:');
385
+ console.log(' 1. Open Push app');
386
+ console.log(' 2. Go to Settings > End-to-End Encryption');
387
+ console.log(" 3. Tap 'Export Encryption Key'");
388
+ console.log(' 4. Copy the key');
389
+ console.log('');
390
+
391
+ // Prompt for key
392
+ const rl = readline.createInterface({
393
+ input: process.stdin,
394
+ output: process.stdout
395
+ });
396
+
397
+ return new Promise((resolve) => {
398
+ rl.question(' Paste your encryption key (or press Enter to skip): ', (keyInput) => {
399
+ rl.close();
400
+
401
+ keyInput = keyInput.trim();
402
+ if (!keyInput) {
403
+ console.log(' Skipped key import.');
404
+ resolve(false);
405
+ return;
406
+ }
407
+
408
+ const result = storeE2EEKeyDirect(keyInput);
409
+ if (result.status === 'success') {
410
+ console.log(' ✓ Key stored in macOS Keychain');
411
+ resolve(true);
412
+ } else {
413
+ console.log(` ✗ ${result.message}`);
414
+ resolve(false);
415
+ }
416
+ });
417
+ });
418
+ }
419
+
420
+ /**
421
+ * Show E2EE status and optionally prompt for import.
422
+ */
423
+ async function showE2EEStatus(promptForImport = true) {
424
+ const e2eeStatus = setupE2EE();
425
+
426
+ console.log('');
427
+ console.log(' E2EE Status');
428
+ console.log(' ' + '-'.repeat(38));
429
+
430
+ if (e2eeStatus.status === 'ready') {
431
+ console.log(' ✓ End-to-end encryption ready');
432
+ console.log(' ✓ Encryption key available');
433
+ return;
434
+ }
435
+
436
+ if (e2eeStatus.status === 'compiled') {
437
+ console.log(' ✓ Compiled encryption helper from source');
438
+ if (e2eeStatus.sourcePath) {
439
+ console.log(` 📄 Source: ${e2eeStatus.sourcePath}`);
440
+ }
441
+ if (e2eeStatus.keyAvailable) {
442
+ console.log(' ✓ Encryption key available');
443
+ } else {
444
+ console.log(' ⚠️ No encryption key found');
445
+ if (promptForImport && await checkUserHasEncryptedTodos()) {
446
+ if (await importE2EEKey()) {
447
+ console.log(' ✓ E2EE setup complete!');
448
+ }
449
+ }
450
+ }
451
+ return;
452
+ }
453
+
454
+ if (e2eeStatus.status === 'not_enabled') {
455
+ console.log(' ✓ Encryption helper ready');
456
+ const hasEncrypted = await checkUserHasEncryptedTodos();
457
+ if (hasEncrypted) {
458
+ console.log(' ⚠️ No encryption key found (E2EE enabled on account)');
459
+ if (promptForImport) {
460
+ if (await importE2EEKey()) {
461
+ console.log(' ✓ E2EE setup complete!');
462
+ }
463
+ }
464
+ } else {
465
+ console.log(' ℹ️ E2EE not enabled (no encrypted todos)');
466
+ }
467
+ return;
468
+ }
469
+
470
+ if (e2eeStatus.status === 'needs_setup') {
471
+ console.log(' ⚠️ E2EE setup needed');
472
+ console.log(' Swift compiler not found. To enable E2EE:');
473
+ console.log(' → Run: xcode-select --install');
474
+ console.log(' → Then run: push-todo connect');
475
+ return;
476
+ }
477
+
478
+ console.log(` ⚠️ E2EE error: ${e2eeStatus.message}`);
479
+
480
+ // Trust-building info
481
+ if (e2eeStatus.sourcePath && ['ready', 'compiled'].includes(e2eeStatus.status)) {
482
+ console.log('');
483
+ console.log(' 🔐 Your encryption key:');
484
+ console.log(' • Stored securely in macOS Keychain');
485
+ console.log(' • Never sent to our servers');
486
+ console.log(' • Only your devices can decrypt');
487
+ }
488
+ }
489
+
490
+ // ============================================================================
491
+ // VERSION CHECK
492
+ // ============================================================================
493
+
494
+ /**
495
+ * Get remote version from npm/GitHub.
496
+ */
497
+ async function getRemoteVersion() {
498
+ try {
499
+ const response = await fetch(REMOTE_PACKAGE_JSON_URL, {
500
+ headers: { 'User-Agent': 'push-cli/1.0' }
501
+ });
502
+ if (!response.ok) return null;
503
+ const data = await response.json();
504
+ return data.version || null;
505
+ } catch {
506
+ return null;
507
+ }
508
+ }
509
+
510
+ /**
511
+ * Parse version string into comparable tuple.
512
+ */
513
+ function parseVersion(versionStr) {
514
+ try {
515
+ return versionStr.split('.').map(p => parseInt(p, 10));
516
+ } catch {
517
+ return [0, 0, 0];
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Check if an update is available.
27
523
  */
28
524
  async function checkVersion() {
29
- const latest = await api.getLatestVersion();
525
+ const method = getInstallationMethod();
526
+
527
+ // Dev installation
528
+ if (method === 'development') {
529
+ return {
530
+ status: 'dev_installation',
531
+ current: VERSION,
532
+ latest: null,
533
+ updateAvailable: false,
534
+ message: `Dev installation (v${VERSION}) - use git pull to update`
535
+ };
536
+ }
537
+
538
+ const remote = await getRemoteVersion();
30
539
 
31
- if (!latest) {
32
- return { current: VERSION, latest: null, updateAvailable: false };
540
+ if (!remote) {
541
+ return {
542
+ status: 'unknown',
543
+ current: VERSION,
544
+ latest: null,
545
+ updateAvailable: false,
546
+ message: 'Could not fetch remote version (network error)'
547
+ };
33
548
  }
34
549
 
35
- const currentParts = VERSION.split('.').map(Number);
36
- const latestParts = latest.split('.').map(Number);
550
+ const localParts = parseVersion(VERSION);
551
+ const remoteParts = parseVersion(remote);
37
552
 
38
553
  let updateAvailable = false;
39
554
  for (let i = 0; i < 3; i++) {
40
- if (latestParts[i] > currentParts[i]) {
555
+ if ((remoteParts[i] || 0) > (localParts[i] || 0)) {
41
556
  updateAvailable = true;
42
557
  break;
43
- } else if (latestParts[i] < currentParts[i]) {
558
+ } else if ((remoteParts[i] || 0) < (localParts[i] || 0)) {
44
559
  break;
45
560
  }
46
561
  }
47
562
 
48
- return { current: VERSION, latest, updateAvailable };
563
+ return {
564
+ status: updateAvailable ? 'update_available' : 'up_to_date',
565
+ current: VERSION,
566
+ latest: remote,
567
+ updateAvailable,
568
+ message: updateAvailable
569
+ ? `Update available: ${VERSION} → ${remote}`
570
+ : `Plugin is up to date (v${VERSION})`
571
+ };
49
572
  }
50
573
 
574
+ /**
575
+ * Update the package to latest version.
576
+ */
577
+ async function doUpdate() {
578
+ const method = getInstallationMethod();
579
+
580
+ if (method === 'development') {
581
+ return {
582
+ status: 'skipped',
583
+ message: 'Development installation - use git pull instead'
584
+ };
585
+ }
586
+
587
+ console.log('Updating @masslessai/push-todo...');
588
+
589
+ try {
590
+ execSync('npm update -g @masslessai/push-todo', { stdio: 'inherit' });
591
+ return { status: 'success', message: 'Updated successfully' };
592
+ } catch (error) {
593
+ return { status: 'error', message: error.message };
594
+ }
595
+ }
596
+
597
+ // ============================================================================
598
+ // VALIDATION FUNCTIONS
599
+ // ============================================================================
600
+
51
601
  /**
52
602
  * Validate the current API key.
53
- *
54
- * @returns {Promise<Object>} Validation result
55
603
  */
56
604
  async function validateApiKeyStatus() {
57
- const apiKey = getApiKey();
58
-
59
- if (!apiKey) {
605
+ let apiKey;
606
+ try {
607
+ apiKey = getApiKey();
608
+ } catch {
60
609
  return { status: 'missing', message: 'No API key configured' };
61
610
  }
62
611
 
@@ -71,8 +620,6 @@ async function validateApiKeyStatus() {
71
620
 
72
621
  /**
73
622
  * Validate machine registration.
74
- *
75
- * @returns {Promise<Object>} Validation result
76
623
  */
77
624
  async function validateMachineStatus() {
78
625
  const machineId = getMachineId();
@@ -97,9 +644,72 @@ async function validateMachineStatus() {
97
644
  }
98
645
 
99
646
  /**
100
- * Validate project registration.
101
- *
102
- * @returns {Object} Validation result
647
+ * Validate project registration (full validation with warnings).
648
+ */
649
+ function validateProjectInfo() {
650
+ const warnings = [];
651
+ const projectPath = process.cwd();
652
+ const gitRemoteRaw = getGitRemote();
653
+ const gitRemote = gitRemoteRaw ? normalizeGitRemote(gitRemoteRaw) : null;
654
+
655
+ // Check if git repo
656
+ const isGit = isGitRepo();
657
+ if (!isGit) {
658
+ warnings.push('Not a git repository (no .git folder)');
659
+ }
660
+
661
+ // Check git remote
662
+ if (isGit && !gitRemoteRaw) {
663
+ warnings.push("Git repo has no 'origin' remote configured");
664
+ }
665
+
666
+ if (gitRemoteRaw && !gitRemote) {
667
+ warnings.push(`Could not normalize git remote: ${gitRemoteRaw}`);
668
+ }
669
+
670
+ // Check local registry
671
+ let localRegistryStatus = 'not_registered';
672
+ if (gitRemote) {
673
+ const registry = getRegistry();
674
+ const registeredPath = registry.getPathWithoutUpdate(gitRemote);
675
+ if (registeredPath) {
676
+ if (registeredPath === projectPath) {
677
+ localRegistryStatus = 'registered';
678
+ } else {
679
+ localRegistryStatus = 'path_mismatch';
680
+ warnings.push(`Local registry has different path: ${registeredPath}`);
681
+ }
682
+ } else {
683
+ warnings.push("Project not in local registry (daemon won't route tasks here)");
684
+ }
685
+ }
686
+
687
+ // Determine overall status
688
+ let status;
689
+ if (!isGit || !gitRemote) {
690
+ status = 'warnings';
691
+ } else if (warnings.length > 0) {
692
+ status = 'warnings';
693
+ } else {
694
+ status = 'valid';
695
+ }
696
+
697
+ return {
698
+ status,
699
+ projectPath,
700
+ isGitRepo: isGit,
701
+ gitRemote,
702
+ gitRemoteRaw,
703
+ localRegistryStatus,
704
+ warnings,
705
+ message: status === 'valid'
706
+ ? `Project valid: ${gitRemote}`
707
+ : `Project has ${warnings.length} warning(s)`
708
+ };
709
+ }
710
+
711
+ /**
712
+ * Simple project validation (JSON output).
103
713
  */
104
714
  function validateProjectStatus() {
105
715
  if (!isGitRepo()) {
@@ -111,270 +721,580 @@ function validateProjectStatus() {
111
721
  return { status: 'no_remote', message: 'No git remote configured' };
112
722
  }
113
723
 
724
+ const normalized = normalizeGitRemote(gitRemote);
114
725
  const registry = getRegistry();
115
- const isRegistered = registry.isRegistered(gitRemote);
116
- const localPath = registry.getPathWithoutUpdate(gitRemote);
726
+ const isRegistered = registry.isRegistered(normalized);
727
+ const localPath = registry.getPathWithoutUpdate(normalized);
117
728
 
118
729
  return {
119
730
  status: isRegistered ? 'registered' : 'unregistered',
120
- gitRemote,
731
+ gitRemote: normalized,
121
732
  localPath,
122
733
  gitRoot: getGitRoot()
123
734
  };
124
735
  }
125
736
 
737
+ // ============================================================================
738
+ // AUTH FLOW
739
+ // ============================================================================
740
+
126
741
  /**
127
- * Generate a random auth code for the authentication flow.
128
- *
129
- * @returns {string} 6-character alphanumeric code
742
+ * Get device name for registration.
130
743
  */
131
- function generateAuthCode() {
132
- const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Omit confusing chars
133
- let code = '';
134
- for (let i = 0; i < 6; i++) {
135
- code += chars[Math.floor(Math.random() * chars.length)];
744
+ function getDeviceName() {
745
+ try {
746
+ return require('os').hostname() || 'Unknown Device';
747
+ } catch {
748
+ return 'Unknown Device';
136
749
  }
137
- return code;
750
+ }
751
+
752
+ /**
753
+ * Initiate device code flow.
754
+ */
755
+ async function initiateDeviceFlow(clientType = 'claude-code') {
756
+ const clientName = CLIENT_NAMES[clientType] || 'Claude Code';
757
+
758
+ const response = await fetch(`${API_BASE}/device-auth/init`, {
759
+ method: 'POST',
760
+ headers: {
761
+ 'Content-Type': 'application/json',
762
+ 'apikey': ANON_KEY
763
+ },
764
+ body: JSON.stringify({
765
+ client_name: clientName,
766
+ client_type: clientType,
767
+ client_version: VERSION,
768
+ device_name: getDeviceName(),
769
+ project_path: process.cwd(),
770
+ git_remote: getGitRemote()
771
+ })
772
+ });
773
+
774
+ if (!response.ok) {
775
+ throw new Error(`Failed to initiate auth: ${response.status}`);
776
+ }
777
+
778
+ return response.json();
779
+ }
780
+
781
+ /**
782
+ * Poll for authorization status.
783
+ */
784
+ async function pollStatus(deviceCode) {
785
+ const response = await fetch(`${API_BASE}/device-auth/poll`, {
786
+ method: 'POST',
787
+ headers: {
788
+ 'Content-Type': 'application/json',
789
+ 'apikey': ANON_KEY
790
+ },
791
+ body: JSON.stringify({ device_code: deviceCode })
792
+ });
793
+
794
+ if (!response.ok) {
795
+ const body = await response.json().catch(() => ({}));
796
+ if (body.error === 'slow_down') {
797
+ return { status: 'slow_down', interval: body.interval || 10 };
798
+ }
799
+ throw new Error(`Poll failed: ${response.status}`);
800
+ }
801
+
802
+ return response.json();
138
803
  }
139
804
 
140
805
  /**
141
806
  * Open a URL in the default browser.
142
- *
143
- * @param {string} url - URL to open
144
807
  */
145
808
  function openBrowser(url) {
146
809
  try {
147
810
  if (process.platform === 'darwin') {
148
811
  execSync(`open "${url}"`, { stdio: 'ignore' });
812
+ return true;
149
813
  } else if (process.platform === 'linux') {
150
814
  execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
815
+ return true;
151
816
  } else if (process.platform === 'win32') {
152
817
  execSync(`start "" "${url}"`, { stdio: 'ignore' });
818
+ return true;
153
819
  }
154
- } catch {
155
- console.log(`Please open this URL manually: ${url}`);
156
- }
820
+ } catch {}
821
+ return false;
157
822
  }
158
823
 
159
824
  /**
160
- * Poll for authentication completion.
161
- *
162
- * @param {string} authCode - The auth code to poll for
163
- * @param {number} timeout - Timeout in seconds
164
- * @returns {Promise<Object|null>} Credentials or null if timeout
825
+ * Full device auth flow with browser sign-in.
165
826
  */
166
- async function pollForAuth(authCode, timeout = 300) {
827
+ async function doFullDeviceAuth(clientType = 'claude-code') {
828
+ const clientName = CLIENT_NAMES[clientType] || 'Claude Code';
829
+
830
+ console.log(' Initializing...');
831
+
832
+ let deviceData;
833
+ try {
834
+ deviceData = await initiateDeviceFlow(clientType);
835
+ } catch (error) {
836
+ console.error(` Error: Failed to initiate connection: ${error.message}`);
837
+ process.exit(1);
838
+ }
839
+
840
+ const deviceCode = deviceData.device_code;
841
+ const expiresIn = deviceData.expires_in;
842
+ let pollInterval = deviceData.interval || 5;
843
+
844
+ const authUrl = deviceData.verification_uri_complete ||
845
+ `https://pushto.do/auth/cli?code=${deviceCode}`;
846
+
847
+ console.log('');
848
+ console.log(' Opening browser for Sign in with Apple...');
849
+ console.log('');
850
+
851
+ const browserOpened = openBrowser(authUrl);
852
+
853
+ if (browserOpened) {
854
+ console.log(" If the browser didn't open, visit:");
855
+ } else {
856
+ console.log(' Open this URL in your browser:');
857
+ }
858
+ console.log(` ${authUrl}`);
859
+ console.log('');
860
+ console.log(` Waiting for authorization (${Math.floor(expiresIn / 60)} min timeout)...`);
861
+ console.log(' Press Ctrl+C to cancel');
862
+ console.log('');
863
+
167
864
  const startTime = Date.now();
168
- const pollInterval = 2000; // 2 seconds
169
865
 
170
- while ((Date.now() - startTime) < timeout * 1000) {
866
+ while (true) {
867
+ const elapsed = (Date.now() - startTime) / 1000;
868
+ if (elapsed > expiresIn) {
869
+ console.log('');
870
+ console.log(' Error: Authorization timed out. Please run connect again.');
871
+ console.log('');
872
+ process.exit(1);
873
+ }
874
+
171
875
  try {
172
- const response = await fetch(
173
- `https://jxuzqcbqhiaxmfitzxlo.supabase.co/functions/v1/poll-auth?code=${authCode}`,
174
- {
175
- headers: {
176
- 'Authorization': `Bearer ${ANON_KEY}`,
177
- 'Content-Type': 'application/json'
178
- }
179
- }
180
- );
876
+ const result = await pollStatus(deviceCode);
877
+
878
+ if (result.status === 'authorized') {
879
+ const apiKeyResult = result.api_key;
880
+ const email = result.email || 'Unknown';
881
+ const actionName = result.normalized_name || result.action_name || clientName;
181
882
 
182
- if (response.ok) {
183
- const data = await response.json();
184
- if (data.api_key) {
185
- return data;
883
+ if (apiKeyResult) {
884
+ return {
885
+ api_key: apiKeyResult,
886
+ email,
887
+ action_name: actionName
888
+ };
889
+ } else {
890
+ console.log('');
891
+ console.log(' Error: Authorization succeeded but no API key received.');
892
+ console.log('');
893
+ process.exit(1);
186
894
  }
187
895
  }
188
- } catch {
189
- // Ignore errors during polling
896
+
897
+ if (result.status === 'denied') {
898
+ console.log('');
899
+ console.log(' Authorization denied.');
900
+ console.log('');
901
+ process.exit(1);
902
+ }
903
+
904
+ if (result.status === 'expired') {
905
+ console.log('');
906
+ console.log(' Error: Authorization expired. Please run connect again.');
907
+ console.log('');
908
+ process.exit(1);
909
+ }
910
+
911
+ if (result.status === 'slow_down') {
912
+ pollInterval = result.interval || pollInterval + 5;
913
+ }
914
+
915
+ // Still pending
916
+ const remaining = Math.floor(expiresIn - elapsed);
917
+ const mins = Math.floor(remaining / 60);
918
+ const secs = remaining % 60;
919
+ process.stdout.write(`\r Waiting... (${mins}:${secs.toString().padStart(2, '0')} remaining) `);
920
+
921
+ } catch (error) {
922
+ process.stdout.write(`\r Error: ${error.message}. Retrying... `);
190
923
  }
191
924
 
192
- await setTimeout(pollInterval);
925
+ await sleep(pollInterval * 1000);
193
926
  }
194
-
195
- return null;
196
927
  }
197
928
 
198
929
  /**
199
- * Run the authentication flow.
200
- *
201
- * @returns {Promise<boolean>} True if successful
930
+ * Register project with backend.
202
931
  */
203
- async function runAuthFlow() {
204
- const authCode = generateAuthCode();
205
- const authUrl = `https://pushto.do/connect?code=${authCode}`;
932
+ async function registerProjectWithBackend(apiKey, clientType = 'claude-code', keywords = '', description = '') {
933
+ const clientName = CLIENT_NAMES[clientType] || 'Claude Code';
206
934
 
207
- console.log('');
208
- console.log(bold('Authentication Required'));
209
- console.log('');
210
- console.log(`Opening browser to: ${cyan(authUrl)}`);
211
- console.log('');
212
- console.log(`Or enter this code manually: ${bold(authCode)}`);
213
- console.log('');
214
- console.log(dim('Waiting for authentication...'));
935
+ const payload = {
936
+ client_type: clientType,
937
+ client_name: clientName,
938
+ device_name: getDeviceName(),
939
+ project_path: process.cwd(),
940
+ git_remote: getGitRemote()
941
+ };
215
942
 
216
- openBrowser(authUrl);
943
+ if (keywords) payload.keywords = keywords;
944
+ if (description) payload.description = description;
217
945
 
218
- const credentials = await pollForAuth(authCode);
946
+ const response = await fetch(`${API_BASE}/register-project`, {
947
+ method: 'POST',
948
+ headers: {
949
+ 'Content-Type': 'application/json',
950
+ 'apikey': ANON_KEY,
951
+ 'Authorization': `Bearer ${apiKey}`
952
+ },
953
+ body: JSON.stringify(payload)
954
+ });
219
955
 
220
- if (!credentials) {
221
- console.log(red('Authentication timed out. Please try again.'));
222
- return false;
956
+ if (!response.ok) {
957
+ if (response.status === 401) {
958
+ return { status: 'unauthorized', message: 'API key invalid or revoked' };
959
+ }
960
+ const body = await response.json().catch(() => ({}));
961
+ return { status: 'error', message: body.error_description || `HTTP ${response.status}` };
223
962
  }
224
963
 
225
- // Save credentials
226
- saveCredentials(credentials.api_key, credentials.user_id, credentials.email);
227
-
228
- console.log('');
229
- console.log(green('✓ Authentication successful!'));
230
- console.log(` Logged in as: ${credentials.email}`);
964
+ const data = await response.json();
965
+ if (data.success) {
966
+ return {
967
+ status: 'success',
968
+ action_name: data.normalized_name || data.action_name || 'Unknown',
969
+ created: data.created !== false,
970
+ message: data.message || ''
971
+ };
972
+ }
231
973
 
232
- return true;
974
+ return { status: 'error', message: 'Unknown error' };
233
975
  }
234
976
 
235
977
  /**
236
- * Register the current project.
237
- *
238
- * @param {string[]} keywords - Project keywords
239
- * @param {string} description - Project description
240
- * @returns {Promise<boolean>} True if successful
978
+ * Register project in local registry for daemon routing.
241
979
  */
242
- async function registerCurrentProject(keywords = [], description = '') {
243
- const gitRemote = getGitRemote();
244
- const gitRoot = getGitRoot();
980
+ function registerProjectLocally(gitRemoteRaw, localPath) {
981
+ if (!gitRemoteRaw) return false;
245
982
 
246
- if (!gitRemote || !gitRoot) {
247
- console.log(yellow('Cannot register: not in a git repository with a remote.'));
248
- return false;
249
- }
983
+ const gitRemote = normalizeGitRemote(gitRemoteRaw);
984
+ if (!gitRemote) return false;
250
985
 
251
- // Register locally
252
986
  const registry = getRegistry();
253
- const isNew = registry.register(gitRemote, gitRoot);
987
+ return registry.register(gitRemote, localPath);
988
+ }
254
989
 
255
- // Register with backend
256
- try {
257
- await api.registerProject(gitRemote, keywords, description);
258
- console.log(green(`✓ Project registered: ${gitRemote}`));
259
- return true;
260
- } catch (error) {
261
- console.log(yellow(`Local registration OK, but backend registration failed: ${error.message}`));
262
- return false;
990
+ /**
991
+ * Show migration hint for legacy installations.
992
+ */
993
+ function showMigrationHint() {
994
+ const method = getInstallationMethod();
995
+
996
+ if (method === 'npm-local') {
997
+ console.log('');
998
+ console.log(' ' + '-'.repeat(50));
999
+ console.log(' TIP: You have a local installation.');
1000
+ console.log(' For global access, install globally:');
1001
+ console.log('');
1002
+ console.log(' npm install -g @masslessai/push-todo');
1003
+ console.log('');
1004
+ console.log(' ' + '-'.repeat(50));
263
1005
  }
264
1006
  }
265
1007
 
1008
+ // ============================================================================
1009
+ // STATUS DISPLAY
1010
+ // ============================================================================
1011
+
266
1012
  /**
267
- * Print a status line with icon.
268
- *
269
- * @param {string} icon - Status icon
270
- * @param {string} label - Status label
271
- * @param {string} value - Status value
1013
+ * Show current connection status.
272
1014
  */
273
- function printStatus(icon, label, value) {
274
- console.log(` ${icon} ${label}: ${value}`);
1015
+ async function showStatus() {
1016
+ console.log('');
1017
+ console.log(' Push Connection Status');
1018
+ console.log(' ' + '='.repeat(40));
1019
+ console.log('');
1020
+
1021
+ let existingKey, existingEmail;
1022
+ try {
1023
+ existingKey = getApiKey();
1024
+ } catch {}
1025
+ existingEmail = getEmail();
1026
+
1027
+ if (existingKey && existingEmail) {
1028
+ console.log(` ✓ Connected as ${existingEmail}`);
1029
+ console.log(` ✓ API key: ${existingKey.slice(0, 16)}...`);
1030
+ console.log('');
1031
+ console.log(' Current project:');
1032
+ const gitRemote = getGitRemote();
1033
+ if (gitRemote) {
1034
+ console.log(` Git remote: ${gitRemote}`);
1035
+ } else {
1036
+ console.log(` Path: ${process.cwd()}`);
1037
+ }
1038
+ console.log('');
1039
+ console.log(" Run 'push-todo connect' to register this project.");
1040
+ console.log(" Run 'push-todo connect --reauth' to re-authenticate.");
1041
+ } else if (existingKey) {
1042
+ console.log(' ⚠ Partial config (missing email)');
1043
+ console.log(` API key: ${existingKey.slice(0, 16)}...`);
1044
+ console.log('');
1045
+ console.log(" Run 'push-todo connect --reauth' to fix.");
1046
+ } else {
1047
+ console.log(' ✗ Not connected');
1048
+ console.log('');
1049
+ console.log(" Run 'push-todo connect' to connect your Push account.");
1050
+ }
1051
+
1052
+ console.log('');
275
1053
  }
276
1054
 
1055
+ // ============================================================================
1056
+ // MAIN CONNECT FLOW
1057
+ // ============================================================================
1058
+
277
1059
  /**
278
1060
  * Run the connect/doctor flow.
279
1061
  *
280
1062
  * @param {Object} options - Options from CLI
281
- * @returns {Promise<void>}
282
1063
  */
283
1064
  export async function runConnect(options = {}) {
284
- console.log('');
285
- console.log(bold('Push Connect - Diagnostic Check'));
286
- console.log(dim('=' .repeat(40)));
287
- console.log('');
1065
+ // Self-healing: ensure daemon is running
1066
+ ensureDaemonRunning();
288
1067
 
289
- let allPassed = true;
1068
+ // Auto-detect client type from installation method
1069
+ let clientType = options.client || 'claude-code';
1070
+ const clientName = CLIENT_NAMES[clientType] || 'Claude Code';
290
1071
 
291
- // Step 1: Version check
292
- console.log(bold('1. Version Check'));
293
- const versionStatus = await checkVersion();
294
- if (versionStatus.updateAvailable) {
295
- printStatus(yellow('⚠'), 'Version', `${versionStatus.current} → ${versionStatus.latest} available`);
296
- console.log(dim(` Update: npm update -g @masslessai/push-todo`));
297
- allPassed = false;
298
- } else {
299
- printStatus(green('✓'), 'Version', `${versionStatus.current} (latest)`);
1072
+ // Handle --check-version (JSON output)
1073
+ if (options['check-version'] || options.checkVersion) {
1074
+ const result = await checkVersion();
1075
+ console.log(JSON.stringify(result, null, 2));
1076
+ return;
300
1077
  }
301
- console.log('');
302
1078
 
303
- // Step 2: API Key validation
304
- console.log(bold('2. API Key'));
305
- let keyStatus = await validateApiKeyStatus();
1079
+ // Handle --update
1080
+ if (options.update) {
1081
+ const result = await doUpdate();
1082
+ console.log(JSON.stringify(result, null, 2));
1083
+ return;
1084
+ }
306
1085
 
307
- if (keyStatus.status === 'missing' || keyStatus.status === 'invalid') {
308
- printStatus(red(''), 'API Key', keyStatus.message || 'Invalid');
309
- console.log('');
1086
+ // Handle --validate-key (JSON output)
1087
+ if (options['validate-key'] || options.validateKey) {
1088
+ const result = await validateApiKeyStatus();
1089
+ console.log(JSON.stringify(result, null, 2));
1090
+ return;
1091
+ }
310
1092
 
311
- // Run auth flow
312
- const authSuccess = await runAuthFlow();
313
- if (authSuccess) {
314
- keyStatus = await validateApiKeyStatus();
315
- } else {
316
- allPassed = false;
317
- }
1093
+ // Handle --validate-machine (JSON output)
1094
+ if (options['validate-machine'] || options.validateMachine) {
1095
+ const result = await validateMachineStatus();
1096
+ console.log(JSON.stringify(result, null, 2));
1097
+ return;
318
1098
  }
319
1099
 
320
- if (keyStatus.status === 'valid') {
321
- printStatus(green(''), 'API Key', `Valid (${keyStatus.email})`);
1100
+ // Handle --validate-project (JSON output)
1101
+ if (options['validate-project'] || options.validateProject) {
1102
+ const result = validateProjectStatus();
1103
+ console.log(JSON.stringify(result, null, 2));
1104
+ return;
322
1105
  }
323
- console.log('');
324
1106
 
325
- // Step 3: Machine validation
326
- console.log(bold('3. Machine'));
327
- const machineStatus = await validateMachineStatus();
328
- if (machineStatus.status === 'valid') {
329
- printStatus(green('✓'), 'Machine', machineStatus.machineName);
330
- printStatus(dim('·'), 'ID', machineStatus.machineId);
331
- } else {
332
- printStatus(yellow('⚠'), 'Machine', machineStatus.message || 'Not validated');
333
- allPassed = false;
1107
+ // Handle --store-e2ee-key
1108
+ if (options['store-e2ee-key'] || options.storeE2eeKey) {
1109
+ const key = options['store-e2ee-key'] || options.storeE2eeKey;
1110
+ const result = storeE2EEKeyDirect(key);
1111
+ console.log(JSON.stringify(result, null, 2));
1112
+ return;
1113
+ }
1114
+
1115
+ // Handle --status (show status without registering)
1116
+ if (options.status) {
1117
+ await showStatus();
1118
+ return;
334
1119
  }
1120
+
1121
+ // Handle --reauth
1122
+ if (options.reauth) {
1123
+ console.log(' Forcing re-authentication...');
1124
+ clearCredentials();
1125
+ }
1126
+
1127
+ // ─────────────────────────────────────────────────────────────────
1128
+ // FULL DOCTOR FLOW
1129
+ // ─────────────────────────────────────────────────────────────────
1130
+
1131
+ console.log('');
1132
+ console.log(` Push Voice Tasks Connect`);
1133
+ console.log(' ' + '='.repeat(40));
335
1134
  console.log('');
336
1135
 
337
- // Step 4: Project validation
338
- console.log(bold('4. Project'));
339
- const projectStatus = validateProjectStatus();
1136
+ let existingKey, existingEmail;
1137
+ try {
1138
+ existingKey = getApiKey();
1139
+ } catch {}
1140
+ existingEmail = getEmail();
1141
+
1142
+ const keywords = options.keywords || '';
1143
+ const description = options.description || '';
340
1144
 
341
- if (projectStatus.status === 'registered') {
342
- printStatus(green('✓'), 'Project', projectStatus.gitRemote);
343
- printStatus(dim('·'), 'Path', projectStatus.localPath);
344
- } else if (projectStatus.status === 'unregistered') {
345
- printStatus(yellow('⚠'), 'Project', `${projectStatus.gitRemote} (not registered)`);
346
- console.log('');
1145
+ if (existingKey && existingEmail && !options.reauth) {
1146
+ // ─────────────────────────────────────────────────────────────────
1147
+ // FAST PATH: Already authenticated, just register project
1148
+ // ─────────────────────────────────────────────────────────────────
1149
+ console.log(` Connected as ${existingEmail}`);
1150
+ console.log(' Registering project...');
1151
+
1152
+ const result = await registerProjectWithBackend(existingKey, clientType, keywords, description);
1153
+
1154
+ if (result.status === 'success') {
1155
+ // Register in local project registry for global daemon routing
1156
+ const gitRemoteRaw = getGitRemote();
1157
+ const localPath = process.cwd();
1158
+ const isNewLocal = registerProjectLocally(gitRemoteRaw, localPath);
1159
+
1160
+ console.log('');
1161
+ console.log(' ' + '='.repeat(40));
1162
+ if (result.created) {
1163
+ console.log(` Created action: "${result.action_name}"`);
1164
+ } else {
1165
+ console.log(` Found existing action: "${result.action_name}"`);
1166
+ }
1167
+
1168
+ // Validate and show project info
1169
+ const projectInfo = validateProjectInfo();
1170
+ if (projectInfo.gitRemote) {
1171
+ console.log(` Git remote: ${projectInfo.gitRemote}`);
1172
+ }
1173
+ for (const warning of projectInfo.warnings) {
1174
+ console.log(` ⚠️ ${warning}`);
1175
+ }
1176
+
1177
+ // Show local registry status
1178
+ if (gitRemoteRaw) {
1179
+ if (isNewLocal) {
1180
+ console.log(` Local path registered: ${localPath}`);
1181
+ } else {
1182
+ console.log(` Local path updated: ${localPath}`);
1183
+ }
1184
+ }
1185
+
1186
+ // Validate and show machine ID
1187
+ const machineInfo = await validateMachineStatus();
1188
+ if (machineInfo.status === 'valid') {
1189
+ console.log(` Machine: ${machineInfo.machineName}`);
1190
+ } else {
1191
+ console.log(` ⚠️ Machine ID: ${machineInfo.message}`);
1192
+ }
1193
+ console.log(' ' + '='.repeat(40));
1194
+ console.log('');
1195
+
1196
+ if (result.created) {
1197
+ console.log(' Your iOS app will sync this automatically.');
1198
+ } else {
1199
+ console.log(' This project is already configured.');
1200
+ }
1201
+
1202
+ // Show E2EE status
1203
+ await showE2EEStatus();
347
1204
 
348
- // Offer to register
349
- if (keyStatus.status === 'valid') {
350
- const keywords = options.keywords ? options.keywords.split(',') : [];
351
- const description = options.description || '';
352
- await registerCurrentProject(keywords, description);
1205
+ // Show migration hint
1206
+ showMigrationHint();
1207
+ console.log('');
1208
+ return;
1209
+ }
1210
+
1211
+ if (result.status === 'unauthorized') {
1212
+ console.log('');
1213
+ console.log(' Session expired, re-authenticating...');
1214
+ console.log('');
1215
+ clearCredentials();
1216
+ // Fall through to full auth
1217
+ } else {
1218
+ console.log('');
1219
+ console.log(` Registration failed: ${result.message || 'Unknown error'}`);
1220
+ console.log(' Trying full connection...');
1221
+ console.log('');
1222
+ // Fall through to full auth
353
1223
  }
354
- } else if (projectStatus.status === 'no_remote') {
355
- printStatus(yellow('⚠'), 'Project', 'No git remote configured');
356
- allPassed = false;
357
- } else {
358
- printStatus(dim('·'), 'Project', 'Not in a git repository');
359
1224
  }
360
- console.log('');
361
1225
 
362
- // Step 5: E2EE check
363
- console.log(bold('5. E2EE (End-to-End Encryption)'));
364
- const [e2eeAvailable, e2eeMessage] = isE2EEAvailable();
365
- if (e2eeAvailable) {
366
- printStatus(green('✓'), 'E2EE', 'Available');
1226
+ // ─────────────────────────────────────────────────────────────────────
1227
+ // SLOW PATH: First time or re-auth needed
1228
+ // ─────────────────────────────────────────────────────────────────────
1229
+ const isReauth = existingKey !== undefined;
1230
+
1231
+ const authResult = await doFullDeviceAuth(clientType);
1232
+
1233
+ // Save credentials
1234
+ saveCredentials(authResult.api_key, authResult.email);
1235
+
1236
+ // Register in local project registry for global daemon routing
1237
+ const gitRemoteRaw = getGitRemote();
1238
+ const localPath = process.cwd();
1239
+ const isNewLocal = registerProjectLocally(gitRemoteRaw, localPath);
1240
+
1241
+ // Show success
1242
+ console.log('');
1243
+ console.log(' ' + '='.repeat(40));
1244
+ if (isReauth) {
1245
+ console.log(` Re-connected as ${authResult.email}`);
367
1246
  } else {
368
- printStatus(yellow('⚠'), 'E2EE', e2eeMessage);
1247
+ console.log(` Connected as ${authResult.email}`);
1248
+ }
1249
+ console.log(` Created action: "${authResult.action_name}"`);
1250
+
1251
+ // Validate and show project info
1252
+ const projectInfo = validateProjectInfo();
1253
+ if (projectInfo.gitRemote) {
1254
+ console.log(` Git remote: ${projectInfo.gitRemote}`);
1255
+ }
1256
+ for (const warning of projectInfo.warnings) {
1257
+ console.log(` ⚠️ ${warning}`);
1258
+ }
1259
+
1260
+ // Show local registry status
1261
+ if (gitRemoteRaw) {
1262
+ if (isNewLocal) {
1263
+ console.log(` Local path registered: ${localPath}`);
1264
+ } else {
1265
+ console.log(` Local path updated: ${localPath}`);
1266
+ }
369
1267
  }
370
- console.log('');
371
1268
 
372
- // Summary
373
- console.log(dim('=' .repeat(40)));
374
- if (allPassed) {
375
- console.log(green(bold('All checks passed!')));
1269
+ // Validate and show machine ID
1270
+ const machineInfo = await validateMachineStatus();
1271
+ if (machineInfo.status === 'valid') {
1272
+ console.log(` Machine: ${machineInfo.machineName}`);
376
1273
  } else {
377
- console.log(yellow('Some checks need attention. See above for details.'));
1274
+ console.log(` ⚠️ Machine ID: ${machineInfo.message}`);
378
1275
  }
1276
+ console.log(' ' + '='.repeat(40));
1277
+ console.log('');
1278
+ console.log(' Your iOS app will sync this automatically.');
1279
+
1280
+ // Show E2EE status
1281
+ await showE2EEStatus();
1282
+
1283
+ // Show migration hint
1284
+ showMigrationHint();
379
1285
  console.log('');
380
1286
  }
1287
+
1288
+ export {
1289
+ checkVersion,
1290
+ doUpdate,
1291
+ validateApiKeyStatus,
1292
+ validateMachineStatus,
1293
+ validateProjectStatus,
1294
+ validateProjectInfo,
1295
+ setupE2EE,
1296
+ storeE2EEKeyDirect,
1297
+ showStatus,
1298
+ getInstallationMethod,
1299
+ VERSION
1300
+ };