@startanaicompany/cli 1.3.1 → 1.4.1

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/git_auth.md ADDED
@@ -0,0 +1,961 @@
1
+ # Git Authentication Implementation Guide for saac-cli
2
+
3
+ **Date**: 2026-01-26
4
+ **Status**: Wrapper OAuth implementation COMPLETE, CLI integration PENDING
5
+ **Related Project**: `~/projects/coolifywrapper` (OAuth backend)
6
+
7
+ ---
8
+
9
+ ## Executive Summary
10
+
11
+ The Coolify Wrapper API now has **full OAuth support** for Git authentication, matching industry standards (Railway, Vercel, Netlify). Users can connect their Git accounts once and deploy unlimited applications without providing tokens repeatedly.
12
+
13
+ **What's Changed:**
14
+ - ✅ Wrapper API supports OAuth for Gitea, GitHub, and GitLab
15
+ - ✅ OAuth tokens stored encrypted (AES-256-GCM) with auto-refresh
16
+ - ✅ Fallback to manual `--git-token` if OAuth not connected
17
+ - ⏳ **CLI needs updates to support OAuth flow**
18
+
19
+ **User Experience Goal:**
20
+ ```bash
21
+ # First time - user connects Git account
22
+ saac create my-app --git git@git.startanaicompany.com:user/repo.git
23
+ ❌ Git account not connected for git.startanaicompany.com
24
+ Would you like to connect now? (Y/n): y
25
+ Opening browser for authentication...
26
+ ✅ Connected to git.startanaicompany.com as mikael.westoo
27
+ ✅ Application created: my-app
28
+
29
+ # Future deployments - no token needed!
30
+ saac create another-app --git git@git.startanaicompany.com:user/repo2.git
31
+ ✅ Using connected account: mikael.westoo@git.startanaicompany.com
32
+ ✅ Application created: another-app
33
+ ```
34
+
35
+ ---
36
+
37
+ ## What the Wrapper API Provides
38
+
39
+ ### OAuth Endpoints (Already Implemented)
40
+
41
+ **1. Initiate OAuth Flow**
42
+ ```
43
+ GET /oauth/authorize?git_host=<host>&session_id=<id>
44
+ Headers: X-API-Key: cw_xxx
45
+
46
+ Response: HTTP 302 Redirect to Git provider OAuth page
47
+ ```
48
+
49
+ **2. Callback Handler** (Automatic)
50
+ ```
51
+ GET /oauth/callback?code=<code>&state=<state>
52
+
53
+ Response: HTML success page (user closes browser)
54
+ ```
55
+
56
+ **3. Poll for Completion** (CLI uses this)
57
+ ```
58
+ GET /oauth/poll/:session_id
59
+ Headers: X-API-Key: cw_xxx
60
+
61
+ Response:
62
+ {
63
+ "sessionId": "abc123",
64
+ "gitHost": "git.startanaicompany.com",
65
+ "status": "pending" | "completed" | "failed",
66
+ "gitUsername": "mikael.westoo",
67
+ "completedAt": "2026-01-26T12:00:00Z"
68
+ }
69
+ ```
70
+
71
+ **4. List Connections**
72
+ ```
73
+ GET /api/v1/users/me/oauth
74
+ Headers: X-API-Key: cw_xxx
75
+
76
+ Response:
77
+ {
78
+ "connections": [
79
+ {
80
+ "gitHost": "git.startanaicompany.com",
81
+ "gitUsername": "mikael.westoo",
82
+ "providerType": "gitea",
83
+ "scopes": ["read:repository", "write:repository"],
84
+ "expiresAt": "2026-01-26T13:00:00Z",
85
+ "createdAt": "2026-01-26T12:00:00Z",
86
+ "lastUsedAt": "2026-01-26T12:30:00Z"
87
+ }
88
+ ],
89
+ "count": 1
90
+ }
91
+ ```
92
+
93
+ **5. Revoke Connection**
94
+ ```
95
+ DELETE /api/v1/users/me/oauth/:git_host
96
+ Headers: X-API-Key: cw_xxx
97
+
98
+ Response: { "success": true }
99
+ ```
100
+
101
+ ### Application Creation (Auto-Uses OAuth)
102
+
103
+ **Wrapper's `createApplication()` logic:**
104
+ 1. Extract `git_host` from `git_repository` URL
105
+ 2. Check if user has OAuth connection for that `git_host`
106
+ 3. If YES → Use OAuth token (automatic)
107
+ 4. If NO → Fall back to manual `git_api_token` (if provided)
108
+ 5. If NEITHER → Return error with OAuth connection URL
109
+
110
+ **Error Response Format:**
111
+ ```json
112
+ {
113
+ "error": "Git account not connected for git.startanaicompany.com. Please connect your Git account at https://apps.startanaicompany.com/oauth/authorize?git_host=git.startanaicompany.com or provide --git-token when creating the application."
114
+ }
115
+ ```
116
+
117
+ ---
118
+
119
+ ## CLI Implementation Tasks
120
+
121
+ ### Task 1: Add OAuth Helper Module
122
+
123
+ **File**: `lib/oauth.js`
124
+
125
+ ```javascript
126
+ // lib/oauth.js
127
+ const axios = require('axios');
128
+ const open = require('open');
129
+ const crypto = require('crypto');
130
+ const chalk = require('chalk');
131
+
132
+ const WRAPPER_API_URL = process.env.WRAPPER_API_URL || 'https://apps.startanaicompany.com';
133
+
134
+ /**
135
+ * Extract git_host from repository URL
136
+ * @param {string} gitUrl - Git repository URL (SSH or HTTPS)
137
+ * @returns {string} - Git host domain
138
+ */
139
+ function extractGitHost(gitUrl) {
140
+ // SSH format: git@git.startanaicompany.com:user/repo.git
141
+ const sshMatch = gitUrl.match(/git@([^:]+):/);
142
+ if (sshMatch) {
143
+ return sshMatch[1];
144
+ }
145
+
146
+ // HTTPS format: https://git.startanaicompany.com/user/repo.git
147
+ const httpsMatch = gitUrl.match(/https?:\/\/([^/]+)/);
148
+ if (httpsMatch) {
149
+ return httpsMatch[1];
150
+ }
151
+
152
+ throw new Error('Invalid Git repository URL format');
153
+ }
154
+
155
+ /**
156
+ * Initiate OAuth flow for a git_host
157
+ * @param {string} gitHost - Git host domain
158
+ * @param {string} apiKey - User's API key
159
+ * @returns {Promise<object>} - { gitUsername, gitHost }
160
+ */
161
+ async function connectGitAccount(gitHost, apiKey) {
162
+ const sessionId = crypto.randomBytes(16).toString('hex');
163
+
164
+ console.log(chalk.blue(`\n🔐 Connecting to ${gitHost}...`));
165
+ console.log(chalk.gray(`Session ID: ${sessionId}\n`));
166
+
167
+ // Build authorization URL
168
+ const authUrl = `${WRAPPER_API_URL}/oauth/authorize?git_host=${encodeURIComponent(gitHost)}&session_id=${sessionId}`;
169
+
170
+ console.log(chalk.yellow('Opening browser for authentication...'));
171
+ console.log(chalk.gray(`If browser doesn't open, visit: ${authUrl}\n`));
172
+
173
+ // Open browser
174
+ await open(authUrl);
175
+
176
+ console.log(chalk.blue('Waiting for authorization...'));
177
+
178
+ // Poll for completion
179
+ return await pollForCompletion(sessionId, apiKey);
180
+ }
181
+
182
+ /**
183
+ * Poll OAuth session until completed
184
+ * @param {string} sessionId - Session ID
185
+ * @param {string} apiKey - User's API key
186
+ * @returns {Promise<object>} - { gitUsername, gitHost }
187
+ */
188
+ async function pollForCompletion(sessionId, apiKey) {
189
+ const pollInterval = 2000; // 2 seconds
190
+ const maxAttempts = 150; // 5 minutes total (150 * 2s)
191
+
192
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
193
+ await sleep(pollInterval);
194
+
195
+ try {
196
+ const response = await axios.get(
197
+ `${WRAPPER_API_URL}/oauth/poll/${sessionId}`,
198
+ {
199
+ headers: {
200
+ 'X-API-Key': apiKey,
201
+ },
202
+ }
203
+ );
204
+
205
+ const { status, gitUsername, gitHost } = response.data;
206
+
207
+ if (status === 'completed') {
208
+ console.log(chalk.green(`\n✅ Connected to ${gitHost} as ${gitUsername}\n`));
209
+ return { gitUsername, gitHost };
210
+ }
211
+
212
+ if (status === 'failed') {
213
+ throw new Error('OAuth authorization failed');
214
+ }
215
+
216
+ // Still pending, continue polling
217
+ process.stdout.write(chalk.gray('.'));
218
+ } catch (error) {
219
+ if (error.response?.status === 404) {
220
+ throw new Error('OAuth session not found or expired');
221
+ }
222
+ throw error;
223
+ }
224
+ }
225
+
226
+ throw new Error('OAuth authorization timed out (5 minutes)');
227
+ }
228
+
229
+ /**
230
+ * Check if user has OAuth connection for git_host
231
+ * @param {string} gitHost - Git host domain
232
+ * @param {string} apiKey - User's API key
233
+ * @returns {Promise<object|null>} - Connection object or null
234
+ */
235
+ async function getConnection(gitHost, apiKey) {
236
+ try {
237
+ const response = await axios.get(
238
+ `${WRAPPER_API_URL}/api/v1/users/me/oauth`,
239
+ {
240
+ headers: {
241
+ 'X-API-Key': apiKey,
242
+ },
243
+ }
244
+ );
245
+
246
+ const connection = response.data.connections.find(
247
+ (conn) => conn.gitHost === gitHost
248
+ );
249
+
250
+ return connection || null;
251
+ } catch (error) {
252
+ return null;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * List all OAuth connections
258
+ * @param {string} apiKey - User's API key
259
+ * @returns {Promise<array>} - Array of connection objects
260
+ */
261
+ async function listConnections(apiKey) {
262
+ const response = await axios.get(
263
+ `${WRAPPER_API_URL}/api/v1/users/me/oauth`,
264
+ {
265
+ headers: {
266
+ 'X-API-Key': apiKey,
267
+ },
268
+ }
269
+ );
270
+
271
+ return response.data.connections;
272
+ }
273
+
274
+ /**
275
+ * Revoke OAuth connection for git_host
276
+ * @param {string} gitHost - Git host domain
277
+ * @param {string} apiKey - User's API key
278
+ */
279
+ async function revokeConnection(gitHost, apiKey) {
280
+ await axios.delete(
281
+ `${WRAPPER_API_URL}/api/v1/users/me/oauth/${encodeURIComponent(gitHost)}`,
282
+ {
283
+ headers: {
284
+ 'X-API-Key': apiKey,
285
+ },
286
+ }
287
+ );
288
+ }
289
+
290
+ function sleep(ms) {
291
+ return new Promise((resolve) => setTimeout(resolve, ms));
292
+ }
293
+
294
+ module.exports = {
295
+ extractGitHost,
296
+ connectGitAccount,
297
+ getConnection,
298
+ listConnections,
299
+ revokeConnection,
300
+ };
301
+ ```
302
+
303
+ **Dependencies to Add:**
304
+ ```bash
305
+ npm install open axios crypto
306
+ ```
307
+
308
+ ---
309
+
310
+ ### Task 2: Update `saac create` Command
311
+
312
+ **File**: `bin/saac-create.js`
313
+
314
+ **Changes Needed:**
315
+
316
+ ```javascript
317
+ // bin/saac-create.js
318
+ const { extractGitHost, getConnection, connectGitAccount } = require('../lib/oauth');
319
+
320
+ // ... existing imports and code ...
321
+
322
+ program
323
+ .name('create')
324
+ .description('Create a new application')
325
+ .requiredOption('--name <name>', 'Application name')
326
+ .requiredOption('--git <repository>', 'Git repository URL (SSH or HTTPS)')
327
+ .option('--git-branch <branch>', 'Git branch', 'master')
328
+ .option('--git-token <token>', 'Git API token (optional if OAuth connected)')
329
+ .option('--subdomain <subdomain>', 'Subdomain for the application')
330
+ // ... other options ...
331
+ .action(async (options) => {
332
+ try {
333
+ const apiKey = process.env.WRAPPER_API_KEY;
334
+ if (!apiKey) {
335
+ console.error(chalk.red('❌ WRAPPER_API_KEY not set'));
336
+ process.exit(1);
337
+ }
338
+
339
+ // Extract git_host from repository URL
340
+ const gitHost = extractGitHost(options.git);
341
+ console.log(chalk.gray(`Git host: ${gitHost}`));
342
+
343
+ // Check if OAuth connected for this git_host
344
+ const connection = await getConnection(gitHost, apiKey);
345
+
346
+ if (connection) {
347
+ console.log(
348
+ chalk.green(
349
+ `✅ Using connected account: ${connection.gitUsername}@${connection.gitHost}`
350
+ )
351
+ );
352
+ // No need for --git-token, OAuth will be used automatically
353
+ } else if (!options.gitToken) {
354
+ // No OAuth connection AND no manual token provided
355
+ console.log(
356
+ chalk.yellow(
357
+ `\n⚠️ Git account not connected for ${gitHost}`
358
+ )
359
+ );
360
+
361
+ const readline = require('readline');
362
+ const rl = readline.createInterface({
363
+ input: process.stdin,
364
+ output: process.stdout,
365
+ });
366
+
367
+ const answer = await new Promise((resolve) => {
368
+ rl.question(
369
+ chalk.blue('Would you like to connect now? (Y/n): '),
370
+ resolve
371
+ );
372
+ });
373
+ rl.close();
374
+
375
+ if (answer.toLowerCase() === 'n') {
376
+ console.log(
377
+ chalk.red(
378
+ `\n❌ Cannot create application without Git authentication.\n`
379
+ )
380
+ );
381
+ console.log(chalk.gray('Options:'));
382
+ console.log(chalk.gray(' 1. Connect Git account: saac git connect'));
383
+ console.log(
384
+ chalk.gray(' 2. Provide token: saac create ... --git-token <token>\n')
385
+ );
386
+ process.exit(1);
387
+ }
388
+
389
+ // Initiate OAuth flow
390
+ await connectGitAccount(gitHost, apiKey);
391
+ }
392
+
393
+ // Proceed with application creation
394
+ console.log(chalk.blue('\n📦 Creating application...\n'));
395
+
396
+ const createData = {
397
+ name: options.name,
398
+ subdomain: options.subdomain || options.name,
399
+ git_repository: options.git,
400
+ git_branch: options.gitBranch,
401
+ git_api_token: options.gitToken, // Optional - wrapper will use OAuth if not provided
402
+ template_type: options.template || 'custom',
403
+ environment_variables: options.env ? parseEnv(options.env) : {},
404
+ // ... other fields ...
405
+ };
406
+
407
+ const response = await axios.post(
408
+ `${WRAPPER_API_URL}/api/v1/applications`,
409
+ createData,
410
+ {
411
+ headers: {
412
+ 'X-API-Key': apiKey,
413
+ 'Content-Type': 'application/json',
414
+ },
415
+ }
416
+ );
417
+
418
+ console.log(chalk.green('✅ Application created successfully!\n'));
419
+ console.log(chalk.bold('Application Details:'));
420
+ console.log(chalk.gray(` Name: ${response.data.name}`));
421
+ console.log(chalk.gray(` UUID: ${response.data.uuid}`));
422
+ console.log(chalk.gray(` Domain: ${response.data.domain}`));
423
+ console.log(chalk.gray(` Status: ${response.data.status}\n`));
424
+
425
+ } catch (error) {
426
+ if (error.response?.data?.error) {
427
+ console.error(chalk.red(`\n❌ ${error.response.data.error}\n`));
428
+
429
+ // Check if error is about missing OAuth connection
430
+ if (error.response.data.error.includes('Git account not connected')) {
431
+ console.log(chalk.yellow('💡 Tip: Connect your Git account to skip providing tokens:'));
432
+ console.log(chalk.gray(` saac git connect ${gitHost}\n`));
433
+ }
434
+ } else {
435
+ console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
436
+ }
437
+ process.exit(1);
438
+ }
439
+ });
440
+
441
+ program.parse();
442
+ ```
443
+
444
+ ---
445
+
446
+ ### Task 3: Add `saac git` Commands
447
+
448
+ **File**: `bin/saac-git.js` (NEW FILE)
449
+
450
+ ```javascript
451
+ #!/usr/bin/env node
452
+ // bin/saac-git.js
453
+ const { Command } = require('commander');
454
+ const chalk = require('chalk');
455
+ const {
456
+ connectGitAccount,
457
+ listConnections,
458
+ revokeConnection,
459
+ extractGitHost,
460
+ } = require('../lib/oauth');
461
+
462
+ const program = new Command();
463
+
464
+ program
465
+ .name('git')
466
+ .description('Manage Git account connections (OAuth)');
467
+
468
+ // saac git connect <repository-or-host>
469
+ program
470
+ .command('connect')
471
+ .description('Connect a Git account via OAuth')
472
+ .argument('[host]', 'Git host domain or repository URL')
473
+ .action(async (hostOrUrl) => {
474
+ try {
475
+ const apiKey = process.env.WRAPPER_API_KEY;
476
+ if (!apiKey) {
477
+ console.error(chalk.red('❌ WRAPPER_API_KEY not set'));
478
+ process.exit(1);
479
+ }
480
+
481
+ let gitHost;
482
+
483
+ if (!hostOrUrl) {
484
+ // No argument - ask user which provider
485
+ const readline = require('readline');
486
+ const rl = readline.createInterface({
487
+ input: process.stdin,
488
+ output: process.stdout,
489
+ });
490
+
491
+ console.log(chalk.blue('Select Git provider:'));
492
+ console.log(' 1. git.startanaicompany.com (Gitea)');
493
+ console.log(' 2. github.com');
494
+ console.log(' 3. gitlab.com');
495
+ console.log(' 4. Custom host\n');
496
+
497
+ const choice = await new Promise((resolve) => {
498
+ rl.question(chalk.blue('Enter choice (1-4): '), resolve);
499
+ });
500
+
501
+ switch (choice) {
502
+ case '1':
503
+ gitHost = 'git.startanaicompany.com';
504
+ break;
505
+ case '2':
506
+ gitHost = 'github.com';
507
+ break;
508
+ case '3':
509
+ gitHost = 'gitlab.com';
510
+ break;
511
+ case '4':
512
+ const custom = await new Promise((resolve) => {
513
+ rl.question(chalk.blue('Enter Git host domain: '), resolve);
514
+ });
515
+ gitHost = custom;
516
+ break;
517
+ default:
518
+ console.error(chalk.red('Invalid choice'));
519
+ process.exit(1);
520
+ }
521
+
522
+ rl.close();
523
+ } else if (hostOrUrl.includes('git@') || hostOrUrl.includes('http')) {
524
+ // Repository URL provided
525
+ gitHost = extractGitHost(hostOrUrl);
526
+ } else {
527
+ // Host domain provided
528
+ gitHost = hostOrUrl;
529
+ }
530
+
531
+ await connectGitAccount(gitHost, apiKey);
532
+ } catch (error) {
533
+ console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
534
+ process.exit(1);
535
+ }
536
+ });
537
+
538
+ // saac git list
539
+ program
540
+ .command('list')
541
+ .description('List connected Git accounts')
542
+ .action(async () => {
543
+ try {
544
+ const apiKey = process.env.WRAPPER_API_KEY;
545
+ if (!apiKey) {
546
+ console.error(chalk.red('❌ WRAPPER_API_KEY not set'));
547
+ process.exit(1);
548
+ }
549
+
550
+ const connections = await listConnections(apiKey);
551
+
552
+ if (connections.length === 0) {
553
+ console.log(chalk.yellow('\n⚠️ No Git accounts connected\n'));
554
+ console.log(chalk.gray('Connect an account: saac git connect\n'));
555
+ return;
556
+ }
557
+
558
+ console.log(chalk.bold('\n🔐 Connected Git Accounts:\n'));
559
+
560
+ connections.forEach((conn, index) => {
561
+ console.log(chalk.blue(`${index + 1}. ${conn.gitHost}`));
562
+ console.log(chalk.gray(` Username: ${conn.gitUsername}`));
563
+ console.log(chalk.gray(` Provider: ${conn.providerType}`));
564
+ console.log(
565
+ chalk.gray(
566
+ ` Expires: ${new Date(conn.expiresAt).toLocaleString()}`
567
+ )
568
+ );
569
+ console.log(
570
+ chalk.gray(
571
+ ` Last used: ${new Date(conn.lastUsedAt).toLocaleString()}`
572
+ )
573
+ );
574
+ console.log('');
575
+ });
576
+ } catch (error) {
577
+ console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
578
+ process.exit(1);
579
+ }
580
+ });
581
+
582
+ // saac git disconnect <host>
583
+ program
584
+ .command('disconnect')
585
+ .description('Disconnect a Git account')
586
+ .argument('<host>', 'Git host domain to disconnect')
587
+ .action(async (host) => {
588
+ try {
589
+ const apiKey = process.env.WRAPPER_API_KEY;
590
+ if (!apiKey) {
591
+ console.error(chalk.red('❌ WRAPPER_API_KEY not set'));
592
+ process.exit(1);
593
+ }
594
+
595
+ console.log(chalk.yellow(`\n⚠️ Disconnecting from ${host}...\n`));
596
+
597
+ await revokeConnection(host, apiKey);
598
+
599
+ console.log(chalk.green(`✅ Disconnected from ${host}\n`));
600
+ } catch (error) {
601
+ if (error.response?.status === 404) {
602
+ console.error(
603
+ chalk.red(`\n❌ No connection found for ${host}\n`)
604
+ );
605
+ } else {
606
+ console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
607
+ }
608
+ process.exit(1);
609
+ }
610
+ });
611
+
612
+ program.parse();
613
+ ```
614
+
615
+ **Register in `bin/saac.js`:**
616
+
617
+ ```javascript
618
+ // bin/saac.js
619
+ program
620
+ .command('git', 'Manage Git account connections')
621
+ .description('Connect, list, or disconnect Git accounts (OAuth)');
622
+ ```
623
+
624
+ ---
625
+
626
+ ### Task 4: Update `saac status` Command
627
+
628
+ **File**: `bin/saac-status.js`
629
+
630
+ **Add OAuth connection info to status output:**
631
+
632
+ ```javascript
633
+ // bin/saac-status.js
634
+ const { listConnections } = require('../lib/oauth');
635
+
636
+ // ... existing status code ...
637
+
638
+ // Add OAuth connections section
639
+ try {
640
+ const connections = await listConnections(apiKey);
641
+
642
+ if (connections.length > 0) {
643
+ console.log(chalk.bold('\n🔐 Connected Git Accounts:'));
644
+ connections.forEach((conn) => {
645
+ console.log(chalk.green(` ✅ ${conn.gitHost} (${conn.gitUsername})`));
646
+ });
647
+ } else {
648
+ console.log(chalk.gray('\n🔐 No Git accounts connected'));
649
+ console.log(chalk.gray(' Connect: saac git connect'));
650
+ }
651
+ } catch (error) {
652
+ // Non-fatal, just skip OAuth section
653
+ console.log(chalk.gray('\n🔐 OAuth status unavailable'));
654
+ }
655
+ ```
656
+
657
+ ---
658
+
659
+ ### Task 5: Update Documentation
660
+
661
+ **File**: `README.md`
662
+
663
+ Add OAuth section:
664
+
665
+ ```markdown
666
+ ## Git Authentication
667
+
668
+ saac-cli supports two methods for Git authentication:
669
+
670
+ ### Method 1: OAuth (Recommended)
671
+
672
+ Connect your Git account once, deploy unlimited applications:
673
+
674
+ ```bash
675
+ # Connect Git account
676
+ saac git connect git.startanaicompany.com
677
+ # Opens browser for authentication
678
+
679
+ # List connected accounts
680
+ saac git list
681
+
682
+ # Disconnect account
683
+ saac git disconnect git.startanaicompany.com
684
+
685
+ # Create app - no token needed!
686
+ saac create my-app --git git@git.startanaicompany.com:user/repo.git
687
+ ```
688
+
689
+ ### Method 2: Manual Token
690
+
691
+ Provide Git API token for each application:
692
+
693
+ ```bash
694
+ saac create my-app \
695
+ --git git@git.startanaicompany.com:user/repo.git \
696
+ --git-token your_gitea_token_here
697
+ ```
698
+
699
+ **OAuth automatically tries first, falls back to manual token if not connected.**
700
+ ```
701
+
702
+ ---
703
+
704
+ ## Implementation Checklist
705
+
706
+ ### Phase 1: Core OAuth Support (Week 1)
707
+ - [ ] Create `lib/oauth.js` with OAuth helper functions
708
+ - [ ] Install dependencies: `npm install open axios crypto`
709
+ - [ ] Test OAuth flow manually with wrapper API
710
+ - [ ] Verify polling works correctly
711
+
712
+ ### Phase 2: Update Commands (Week 2)
713
+ - [ ] Create `bin/saac-git.js` with `connect`, `list`, `disconnect` subcommands
714
+ - [ ] Update `bin/saac-create.js` to auto-prompt for OAuth
715
+ - [ ] Update `bin/saac-status.js` to show OAuth connections
716
+ - [ ] Register `git` command in main `bin/saac.js`
717
+
718
+ ### Phase 3: Testing (Week 3)
719
+ - [ ] Test OAuth flow with Gitea (git.startanaicompany.com)
720
+ - [ ] Test OAuth flow with GitHub (github.com)
721
+ - [ ] Test application creation with OAuth token
722
+ - [ ] Test fallback to manual `--git-token`
723
+ - [ ] Test error handling (expired sessions, timeouts)
724
+ - [ ] Test `saac git list` and `saac git disconnect`
725
+
726
+ ### Phase 4: Documentation & Release (Week 4)
727
+ - [ ] Update README.md with OAuth documentation
728
+ - [ ] Create migration guide for existing users
729
+ - [ ] Update examples in all command files
730
+ - [ ] Create demo video/GIF showing OAuth flow
731
+ - [ ] Announce OAuth support to users
732
+
733
+ ---
734
+
735
+ ## Testing Guide
736
+
737
+ ### Test 1: OAuth Connection Flow
738
+
739
+ ```bash
740
+ # Set API key
741
+ export WRAPPER_API_KEY="cw_your_key_here"
742
+
743
+ # Test connect command
744
+ saac git connect git.startanaicompany.com
745
+
746
+ # Expected:
747
+ # - Browser opens to OAuth page
748
+ # - User authorizes
749
+ # - CLI shows "✅ Connected to git.startanaicompany.com as username"
750
+ ```
751
+
752
+ ### Test 2: List Connections
753
+
754
+ ```bash
755
+ saac git list
756
+
757
+ # Expected output:
758
+ # 🔐 Connected Git Accounts:
759
+ #
760
+ # 1. git.startanaicompany.com
761
+ # Username: mikael.westoo
762
+ # Provider: gitea
763
+ # Expires: 2026-01-26 13:00:00
764
+ # Last used: 2026-01-26 12:30:00
765
+ ```
766
+
767
+ ### Test 3: Create App with OAuth
768
+
769
+ ```bash
770
+ saac create test-oauth-app \
771
+ --git git@git.startanaicompany.com:user/repo.git \
772
+ --subdomain testoauth
773
+
774
+ # Expected:
775
+ # ✅ Using connected account: mikael.westoo@git.startanaicompany.com
776
+ # 📦 Creating application...
777
+ # ✅ Application created successfully!
778
+ ```
779
+
780
+ ### Test 4: Create App Without OAuth (Should Prompt)
781
+
782
+ ```bash
783
+ # Disconnect first
784
+ saac git disconnect git.startanaicompany.com
785
+
786
+ # Try to create without --git-token
787
+ saac create test-app --git git@git.startanaicompany.com:user/repo.git
788
+
789
+ # Expected:
790
+ # ⚠️ Git account not connected for git.startanaicompany.com
791
+ # Would you like to connect now? (Y/n):
792
+ ```
793
+
794
+ ### Test 5: Fallback to Manual Token
795
+
796
+ ```bash
797
+ saac create test-manual \
798
+ --git git@git.startanaicompany.com:user/repo.git \
799
+ --git-token gto_your_token_here
800
+
801
+ # Expected:
802
+ # ✅ Application created successfully!
803
+ # (Using manual token, not OAuth)
804
+ ```
805
+
806
+ ---
807
+
808
+ ## Error Handling
809
+
810
+ ### Common Errors & Solutions
811
+
812
+ **1. "OAuth session not found or expired"**
813
+ - User took too long to authorize (>10 minutes)
814
+ - Solution: Try `saac git connect` again
815
+
816
+ **2. "OAuth authorization timed out (5 minutes)"**
817
+ - Polling timed out waiting for user
818
+ - Solution: Increase `maxAttempts` in `pollForCompletion()`
819
+
820
+ **3. "Git account not connected"**
821
+ - User hasn't connected Git account yet
822
+ - Solution: Prompt to run `saac git connect <host>`
823
+
824
+ **4. Browser doesn't open automatically**
825
+ - `open` package failed on some systems
826
+ - Solution: Display URL and ask user to open manually
827
+
828
+ **5. "Invalid Git repository URL format"**
829
+ - Repository URL not in SSH or HTTPS format
830
+ - Solution: Show expected formats in error message
831
+
832
+ ---
833
+
834
+ ## Architecture Notes
835
+
836
+ ### Why git_host Instead of Provider Name?
837
+
838
+ **User-Friendly:**
839
+ - Users know "git.startanaicompany.com" (from their repository URL)
840
+ - Users DON'T know "gitea" vs "github" vs "gitlab"
841
+
842
+ **Example:**
843
+ ```bash
844
+ # ❌ Confusing (users don't know provider names)
845
+ saac git connect gitea
846
+
847
+ # ✅ Clear (users recognize the domain)
848
+ saac git connect git.startanaicompany.com
849
+ ```
850
+
851
+ ### Auto-Detection Flow
852
+
853
+ ```
854
+ Repository URL: git@git.startanaicompany.com:user/repo.git
855
+
856
+ Extract git_host: git.startanaicompany.com
857
+
858
+ Check OAuth connection for git.startanaicompany.com
859
+
860
+ Connected?
861
+ ├─ YES → Use OAuth token (automatic)
862
+ └─ NO → Fall back to --git-token (if provided)
863
+ ```
864
+
865
+ ### Security Considerations
866
+
867
+ 1. **API Key Storage**: Never commit API keys, always use environment variables
868
+ 2. **OAuth State**: Wrapper handles CSRF protection with state parameter
869
+ 3. **Token Storage**: Wrapper encrypts tokens (AES-256-GCM), CLI never stores tokens
870
+ 4. **Session IDs**: Use crypto.randomBytes() for unpredictable session IDs
871
+ 5. **Polling Timeout**: Limit polling to prevent infinite loops (5 minutes max)
872
+
873
+ ---
874
+
875
+ ## Dependencies
876
+
877
+ ### Required npm Packages
878
+
879
+ ```json
880
+ {
881
+ "dependencies": {
882
+ "axios": "^1.6.0",
883
+ "chalk": "^4.1.2",
884
+ "commander": "^11.0.0",
885
+ "open": "^10.0.0"
886
+ }
887
+ }
888
+ ```
889
+
890
+ **Install:**
891
+ ```bash
892
+ cd ~/projects/saac-cli
893
+ npm install open axios
894
+ ```
895
+
896
+ ---
897
+
898
+ ## Wrapper API Contract
899
+
900
+ ### What Wrapper Guarantees
901
+
902
+ 1. **OAuth Flow**: Redirects to correct provider based on git_host
903
+ 2. **Token Management**: Auto-refreshes expired tokens (1-hour expiration)
904
+ 3. **Fallback**: Always supports manual `git_api_token` as fallback
905
+ 4. **Error Messages**: Provides actionable error messages with OAuth URLs
906
+ 5. **Multi-Provider**: Supports Gitea, GitHub, GitLab without CLI changes
907
+
908
+ ### What CLI Must Do
909
+
910
+ 1. **Extract git_host**: Parse repository URL to get git_host domain
911
+ 2. **Prompt User**: Ask user to connect if OAuth not available and no token provided
912
+ 3. **Poll Session**: Poll `/oauth/poll/:session_id` every 2 seconds until completed
913
+ 4. **Handle Timeouts**: Abort after 5 minutes with clear error message
914
+ 5. **Show Feedback**: Display connection status and connected username
915
+
916
+ ---
917
+
918
+ ## Timeline & Milestones
919
+
920
+ ### Week 1: Foundation
921
+ - Implement `lib/oauth.js`
922
+ - Test OAuth flow manually with curl
923
+ - Verify polling mechanism works
924
+
925
+ ### Week 2: Integration
926
+ - Update `saac create` command
927
+ - Implement `saac git` commands
928
+ - Update `saac status` command
929
+
930
+ ### Week 3: Testing
931
+ - End-to-end testing with Gitea
932
+ - End-to-end testing with GitHub
933
+ - Error scenario testing
934
+ - Performance testing (polling overhead)
935
+
936
+ ### Week 4: Release
937
+ - Documentation updates
938
+ - Migration guide for existing users
939
+ - Release notes
940
+ - User announcement
941
+
942
+ ---
943
+
944
+ ## Support & Questions
945
+
946
+ **Wrapper API Status**: ✅ PRODUCTION READY (commit a0d171a)
947
+ **Documentation**: All docs in `~/projects/coolifywrapper/*.md`
948
+ **OAuth Endpoints**: https://apps.startanaicompany.com/oauth/*
949
+
950
+ **Key Reference Files:**
951
+ - `~/projects/coolifywrapper/OAUTH_USER_FLOW_CORRECTED.md` - Complete user flow
952
+ - `~/projects/coolifywrapper/OAUTH_IMPLEMENTATION_ARCHITECTURE.md` - Technical details
953
+ - `~/projects/coolifywrapper/src/routes/oauth.js` - Endpoint implementation
954
+
955
+ **Contact**: Check wrapper deployment status at https://apps.startanaicompany.com/api/v1/health
956
+
957
+ ---
958
+
959
+ **READY TO IMPLEMENT** 🚀
960
+
961
+ All backend infrastructure is complete. CLI team can start implementation immediately.