vibehub-cli 1.0.44 → 1.1.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/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +71 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +92 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/profiles.d.ts +3 -0
- package/dist/commands/profiles.d.ts.map +1 -0
- package/dist/commands/profiles.js +77 -0
- package/dist/commands/profiles.js.map +1 -0
- package/dist/commands/use.d.ts +3 -0
- package/dist/commands/use.d.ts.map +1 -0
- package/dist/commands/use.js +83 -0
- package/dist/commands/use.js.map +1 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +105 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/auth-helper.d.ts +14 -3
- package/dist/lib/auth-helper.d.ts.map +1 -1
- package/dist/lib/auth-helper.js +111 -35
- package/dist/lib/auth-helper.js.map +1 -1
- package/dist/lib/profile-manager.d.ts +116 -0
- package/dist/lib/profile-manager.d.ts.map +1 -0
- package/dist/lib/profile-manager.js +351 -0
- package/dist/lib/profile-manager.js.map +1 -0
- package/dist/lib/supabase-auth.d.ts +42 -0
- package/dist/lib/supabase-auth.d.ts.map +1 -0
- package/dist/lib/supabase-auth.js +398 -0
- package/dist/lib/supabase-auth.js.map +1 -0
- package/package.json +6 -5
package/dist/lib/auth-helper.js
CHANGED
|
@@ -1,58 +1,134 @@
|
|
|
1
|
-
import { GlobalConfigManager } from './global-config.js';
|
|
2
|
-
import { authenticateWithFirebase } from './firebase-auth.js';
|
|
3
1
|
import chalk from 'chalk';
|
|
2
|
+
import { ProfileManager } from './profile-manager.js';
|
|
3
|
+
import { SupabaseAuth } from './supabase-auth.js';
|
|
4
4
|
/**
|
|
5
|
-
* Get authentication token -
|
|
6
|
-
* This is the main entry point for authentication in all commands
|
|
5
|
+
* Get authentication token - uses profile system with automatic token refresh.
|
|
6
|
+
* This is the main entry point for authentication in all commands.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. Check for migration from old format (prompts browser login if needed)
|
|
10
|
+
* 2. Get active profile (project-level override or default)
|
|
11
|
+
* 3. If no profile exists, prompt browser login
|
|
12
|
+
* 4. If token expired/expiring, silently refresh using refresh token
|
|
13
|
+
* 5. Return valid token
|
|
7
14
|
*/
|
|
8
15
|
export async function getAuthToken(forceReauth = false) {
|
|
9
|
-
const
|
|
10
|
-
//
|
|
16
|
+
const profileManager = new ProfileManager();
|
|
17
|
+
// Check for migration from old format
|
|
18
|
+
const needsMigration = await profileManager.migrateIfNeeded();
|
|
19
|
+
if (needsMigration) {
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(chalk.yellow('═'.repeat(55)));
|
|
22
|
+
console.log(chalk.yellow.bold(' VibeHub Authentication Upgrade'));
|
|
23
|
+
console.log(chalk.yellow('═'.repeat(55)));
|
|
24
|
+
console.log('');
|
|
25
|
+
console.log('VibeHub now uses Google Sign-In for better security');
|
|
26
|
+
console.log('and seamless authentication across all your devices.');
|
|
27
|
+
console.log('');
|
|
28
|
+
console.log(chalk.gray('Your existing configuration has been backed up.'));
|
|
29
|
+
console.log('');
|
|
30
|
+
}
|
|
31
|
+
// If force reauth, delete current profile and re-authenticate
|
|
11
32
|
if (forceReauth) {
|
|
12
|
-
await
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
const activeProfile = await profileManager.getActiveProfile();
|
|
34
|
+
if (activeProfile) {
|
|
35
|
+
await profileManager.deleteProfile(activeProfile.name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Get active profile (project-level override or default)
|
|
39
|
+
let profile = await profileManager.getActiveProfile();
|
|
40
|
+
// If no profile exists, prompt for authentication
|
|
41
|
+
if (!profile) {
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(chalk.yellow('Authentication required'));
|
|
44
|
+
console.log(chalk.blue('Opening browser for Google sign-in...'));
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(chalk.gray('If your browser doesn\'t open automatically, please visit:'));
|
|
47
|
+
console.log(chalk.gray('https://vibehub.co.in/cli-auth'));
|
|
48
|
+
console.log('');
|
|
49
|
+
try {
|
|
50
|
+
const supabaseAuth = new SupabaseAuth();
|
|
51
|
+
const profileData = await supabaseAuth.loginWithGoogle();
|
|
52
|
+
// Save to default profile
|
|
53
|
+
await profileManager.saveProfile('default', profileData);
|
|
54
|
+
profile = await profileManager.getProfile('default');
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log(chalk.green(`✓ Authenticated as ${profileData.email}`));
|
|
57
|
+
console.log(chalk.gray('Token saved - you won\'t need to authenticate again for a while.'));
|
|
58
|
+
console.log('');
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log(chalk.red('✗ Authentication failed'));
|
|
63
|
+
console.log(chalk.red(` ${error.message}`));
|
|
64
|
+
console.log('');
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!profile) {
|
|
69
|
+
throw new Error('Authentication failed. Please run \'vibe login\' to sign in.');
|
|
70
|
+
}
|
|
71
|
+
// Refresh token if needed (silent refresh)
|
|
72
|
+
try {
|
|
73
|
+
profile = await profileManager.refreshTokenIfNeeded(profile);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// Token refresh failed - need to re-authenticate
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log(chalk.yellow('Session expired. Re-authenticating...'));
|
|
79
|
+
console.log(chalk.blue('Opening browser for Google sign-in...'));
|
|
80
|
+
console.log('');
|
|
81
|
+
const supabaseAuth = new SupabaseAuth();
|
|
82
|
+
const profileData = await supabaseAuth.loginWithGoogle();
|
|
83
|
+
await profileManager.saveProfile(profile.name, profileData);
|
|
84
|
+
profile = await profileManager.getProfile(profile.name);
|
|
85
|
+
if (!profile) {
|
|
86
|
+
throw new Error('Authentication failed after refresh.');
|
|
87
|
+
}
|
|
88
|
+
console.log(chalk.green(`✓ Re-authenticated as ${profile.email}`));
|
|
89
|
+
console.log('');
|
|
90
|
+
}
|
|
31
91
|
return {
|
|
32
|
-
email:
|
|
33
|
-
token:
|
|
92
|
+
email: profile.email,
|
|
93
|
+
token: profile.accessToken
|
|
34
94
|
};
|
|
35
95
|
}
|
|
36
96
|
/**
|
|
37
97
|
* Check if user is authenticated (without prompting)
|
|
38
98
|
*/
|
|
39
99
|
export async function isAuthenticated() {
|
|
40
|
-
const
|
|
41
|
-
|
|
100
|
+
const profileManager = new ProfileManager();
|
|
101
|
+
const profile = await profileManager.getActiveProfile();
|
|
102
|
+
return profile !== null;
|
|
42
103
|
}
|
|
43
104
|
/**
|
|
44
105
|
* Get stored email without prompting
|
|
45
106
|
*/
|
|
46
107
|
export async function getStoredEmail() {
|
|
47
|
-
const
|
|
48
|
-
|
|
108
|
+
const profileManager = new ProfileManager();
|
|
109
|
+
const profile = await profileManager.getActiveProfile();
|
|
110
|
+
return profile?.email || null;
|
|
49
111
|
}
|
|
50
112
|
/**
|
|
51
|
-
* Clear authentication (logout)
|
|
113
|
+
* Clear authentication (logout from active profile)
|
|
52
114
|
*/
|
|
53
115
|
export async function clearAuth() {
|
|
54
|
-
const
|
|
55
|
-
await
|
|
56
|
-
|
|
116
|
+
const profileManager = new ProfileManager();
|
|
117
|
+
const profile = await profileManager.getActiveProfile();
|
|
118
|
+
if (profile) {
|
|
119
|
+
await profileManager.deleteProfile(profile.name);
|
|
120
|
+
console.log(chalk.green(`✓ Logged out from ${profile.email}`));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.log(chalk.yellow('Not logged in.'));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get the current profile name (for display purposes)
|
|
128
|
+
*/
|
|
129
|
+
export async function getCurrentProfileName() {
|
|
130
|
+
const profileManager = new ProfileManager();
|
|
131
|
+
const profile = await profileManager.getActiveProfile();
|
|
132
|
+
return profile?.name || null;
|
|
57
133
|
}
|
|
58
134
|
//# sourceMappingURL=auth-helper.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-helper.js","sourceRoot":"","sources":["../../src/lib/auth-helper.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"auth-helper.js","sourceRoot":"","sources":["../../src/lib/auth-helper.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile data structure for storing authentication tokens
|
|
3
|
+
*/
|
|
4
|
+
export interface ProfileData {
|
|
5
|
+
name: string;
|
|
6
|
+
email: string;
|
|
7
|
+
userId: string;
|
|
8
|
+
accessToken: string;
|
|
9
|
+
refreshToken: string;
|
|
10
|
+
tokenExpiresAt: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
lastUsedAt: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Global configuration structure
|
|
16
|
+
*/
|
|
17
|
+
export interface GlobalConfig {
|
|
18
|
+
version: string;
|
|
19
|
+
defaultProfile: string;
|
|
20
|
+
migratedFrom?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Project-level profile configuration
|
|
24
|
+
*/
|
|
25
|
+
export interface ProjectProfileConfig {
|
|
26
|
+
profile?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* ProfileManager handles multi-profile authentication token storage and management.
|
|
30
|
+
*
|
|
31
|
+
* Storage structure:
|
|
32
|
+
* ~/.vibehub/
|
|
33
|
+
* ├── config.json # Global settings (defaultProfile, version)
|
|
34
|
+
* └── profiles/
|
|
35
|
+
* ├── default.json # Default profile tokens
|
|
36
|
+
* └── work.json # Named profile tokens
|
|
37
|
+
*
|
|
38
|
+
* Project-level override:
|
|
39
|
+
* project/.vibehub/config.json # Contains "profile" field to override default
|
|
40
|
+
*/
|
|
41
|
+
export declare class ProfileManager {
|
|
42
|
+
private baseDir;
|
|
43
|
+
private configPath;
|
|
44
|
+
private profilesDir;
|
|
45
|
+
private supabase;
|
|
46
|
+
constructor();
|
|
47
|
+
/**
|
|
48
|
+
* Ensure the profile directories exist
|
|
49
|
+
*/
|
|
50
|
+
private ensureDirectories;
|
|
51
|
+
/**
|
|
52
|
+
* Get the global configuration
|
|
53
|
+
*/
|
|
54
|
+
getGlobalConfig(): Promise<GlobalConfig>;
|
|
55
|
+
/**
|
|
56
|
+
* Save the global configuration
|
|
57
|
+
*/
|
|
58
|
+
saveGlobalConfig(config: GlobalConfig): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Get a profile by name
|
|
61
|
+
*/
|
|
62
|
+
getProfile(name: string): Promise<ProfileData | null>;
|
|
63
|
+
/**
|
|
64
|
+
* Save a profile
|
|
65
|
+
*/
|
|
66
|
+
saveProfile(name: string, data: Omit<ProfileData, 'name'>): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Delete a profile
|
|
69
|
+
*/
|
|
70
|
+
deleteProfile(name: string): Promise<boolean>;
|
|
71
|
+
/**
|
|
72
|
+
* List all profiles
|
|
73
|
+
*/
|
|
74
|
+
listProfiles(): Promise<ProfileData[]>;
|
|
75
|
+
/**
|
|
76
|
+
* Delete all profiles
|
|
77
|
+
*/
|
|
78
|
+
deleteAllProfiles(): Promise<number>;
|
|
79
|
+
/**
|
|
80
|
+
* Get the project-level profile override (if any)
|
|
81
|
+
*/
|
|
82
|
+
getProjectProfile(projectPath?: string): Promise<string | null>;
|
|
83
|
+
/**
|
|
84
|
+
* Set the profile for the current project
|
|
85
|
+
*/
|
|
86
|
+
setProjectProfile(profileName: string, projectPath?: string): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Clear the project profile override
|
|
89
|
+
*/
|
|
90
|
+
clearProjectProfile(projectPath?: string): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Get the active profile (project-level override or default)
|
|
93
|
+
*/
|
|
94
|
+
getActiveProfile(projectPath?: string): Promise<ProfileData | null>;
|
|
95
|
+
/**
|
|
96
|
+
* Check if a token is expired or about to expire
|
|
97
|
+
*/
|
|
98
|
+
isTokenExpired(profile: ProfileData, bufferMinutes?: number): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Refresh tokens if needed (silent refresh using refresh token)
|
|
101
|
+
*/
|
|
102
|
+
refreshTokenIfNeeded(profile: ProfileData): Promise<ProfileData>;
|
|
103
|
+
/**
|
|
104
|
+
* Check if migration from old format is needed and perform it
|
|
105
|
+
*/
|
|
106
|
+
migrateIfNeeded(): Promise<boolean>;
|
|
107
|
+
/**
|
|
108
|
+
* Get the profile status (for display)
|
|
109
|
+
*/
|
|
110
|
+
getProfileStatus(profile: ProfileData): 'active' | 'expiring' | 'expired';
|
|
111
|
+
/**
|
|
112
|
+
* Get time until token expires (formatted string)
|
|
113
|
+
*/
|
|
114
|
+
getTokenExpiryString(profile: ProfileData): string;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=profile-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile-manager.d.ts","sourceRoot":"","sources":["../../src/lib/profile-manager.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAMD;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAiB;;IASjC;;OAEG;YACW,iBAAiB;IAK/B;;OAEG;IACG,eAAe,IAAI,OAAO,CAAC,YAAY,CAAC;IAe9C;;OAEG;IACG,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAK3D;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAY3D;;OAEG;IACG,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB/E;;OAEG;IACG,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBnD;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAyB5C;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;IAW1C;;OAEG;IACG,iBAAiB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA2BrE;;OAEG;IACG,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBjF;;OAEG;IACG,mBAAmB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAe9D;;OAEG;IACG,gBAAgB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAiBzE;;OAEG;IACH,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,aAAa,GAAE,MAAU,GAAG,OAAO;IAOxE;;OAEG;IACG,oBAAoB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAmCtE;;OAEG;IACG,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;IAsCzC;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS;IAazE;;OAEG;IACH,oBAAoB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM;CAqBnD"}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { createClient } from '@supabase/supabase-js';
|
|
5
|
+
// Supabase configuration
|
|
6
|
+
const SUPABASE_URL = 'https://keogzaeakmndknpgrjif.supabase.co';
|
|
7
|
+
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtlb2d6YWVha21uZGtucGdyamlmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTc0MTYzMjQsImV4cCI6MjA3Mjk5MjMyNH0.38TymM-WiTS3ONu2KirEGbQT3XwRqSC5UBhZCQnlwO0';
|
|
8
|
+
/**
|
|
9
|
+
* ProfileManager handles multi-profile authentication token storage and management.
|
|
10
|
+
*
|
|
11
|
+
* Storage structure:
|
|
12
|
+
* ~/.vibehub/
|
|
13
|
+
* ├── config.json # Global settings (defaultProfile, version)
|
|
14
|
+
* └── profiles/
|
|
15
|
+
* ├── default.json # Default profile tokens
|
|
16
|
+
* └── work.json # Named profile tokens
|
|
17
|
+
*
|
|
18
|
+
* Project-level override:
|
|
19
|
+
* project/.vibehub/config.json # Contains "profile" field to override default
|
|
20
|
+
*/
|
|
21
|
+
export class ProfileManager {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.baseDir = path.join(os.homedir(), '.vibehub');
|
|
24
|
+
this.configPath = path.join(this.baseDir, 'config.json');
|
|
25
|
+
this.profilesDir = path.join(this.baseDir, 'profiles');
|
|
26
|
+
this.supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Ensure the profile directories exist
|
|
30
|
+
*/
|
|
31
|
+
async ensureDirectories() {
|
|
32
|
+
await fs.ensureDir(this.baseDir);
|
|
33
|
+
await fs.ensureDir(this.profilesDir);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get the global configuration
|
|
37
|
+
*/
|
|
38
|
+
async getGlobalConfig() {
|
|
39
|
+
try {
|
|
40
|
+
await this.ensureDirectories();
|
|
41
|
+
if (await fs.pathExists(this.configPath)) {
|
|
42
|
+
return await fs.readJson(this.configPath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Return default config if file doesn't exist or is invalid
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
version: '2.0',
|
|
50
|
+
defaultProfile: 'default'
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Save the global configuration
|
|
55
|
+
*/
|
|
56
|
+
async saveGlobalConfig(config) {
|
|
57
|
+
await this.ensureDirectories();
|
|
58
|
+
await fs.writeJson(this.configPath, config, { spaces: 2 });
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get a profile by name
|
|
62
|
+
*/
|
|
63
|
+
async getProfile(name) {
|
|
64
|
+
try {
|
|
65
|
+
const profilePath = path.join(this.profilesDir, `${name}.json`);
|
|
66
|
+
if (await fs.pathExists(profilePath)) {
|
|
67
|
+
return await fs.readJson(profilePath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Profile doesn't exist or is invalid
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Save a profile
|
|
77
|
+
*/
|
|
78
|
+
async saveProfile(name, data) {
|
|
79
|
+
await this.ensureDirectories();
|
|
80
|
+
const profilePath = path.join(this.profilesDir, `${name}.json`);
|
|
81
|
+
const profile = {
|
|
82
|
+
...data,
|
|
83
|
+
name,
|
|
84
|
+
lastUsedAt: new Date().toISOString()
|
|
85
|
+
};
|
|
86
|
+
await fs.writeJson(profilePath, profile, { spaces: 2 });
|
|
87
|
+
// Update global config if this is the first profile
|
|
88
|
+
const config = await this.getGlobalConfig();
|
|
89
|
+
if (!config.defaultProfile || !(await this.getProfile(config.defaultProfile))) {
|
|
90
|
+
config.defaultProfile = name;
|
|
91
|
+
await this.saveGlobalConfig(config);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Delete a profile
|
|
96
|
+
*/
|
|
97
|
+
async deleteProfile(name) {
|
|
98
|
+
const profilePath = path.join(this.profilesDir, `${name}.json`);
|
|
99
|
+
if (await fs.pathExists(profilePath)) {
|
|
100
|
+
await fs.remove(profilePath);
|
|
101
|
+
// If deleting the default profile, update config
|
|
102
|
+
const config = await this.getGlobalConfig();
|
|
103
|
+
if (config.defaultProfile === name) {
|
|
104
|
+
const profiles = await this.listProfiles();
|
|
105
|
+
config.defaultProfile = profiles.length > 0 ? profiles[0].name : 'default';
|
|
106
|
+
await this.saveGlobalConfig(config);
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* List all profiles
|
|
114
|
+
*/
|
|
115
|
+
async listProfiles() {
|
|
116
|
+
try {
|
|
117
|
+
await this.ensureDirectories();
|
|
118
|
+
const files = await fs.readdir(this.profilesDir);
|
|
119
|
+
const profiles = [];
|
|
120
|
+
for (const file of files) {
|
|
121
|
+
if (file.endsWith('.json')) {
|
|
122
|
+
try {
|
|
123
|
+
const profile = await fs.readJson(path.join(this.profilesDir, file));
|
|
124
|
+
profiles.push(profile);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Skip invalid profile files
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return profiles.sort((a, b) => new Date(b.lastUsedAt).getTime() - new Date(a.lastUsedAt).getTime());
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Delete all profiles
|
|
139
|
+
*/
|
|
140
|
+
async deleteAllProfiles() {
|
|
141
|
+
const profiles = await this.listProfiles();
|
|
142
|
+
let count = 0;
|
|
143
|
+
for (const profile of profiles) {
|
|
144
|
+
if (await this.deleteProfile(profile.name)) {
|
|
145
|
+
count++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return count;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get the project-level profile override (if any)
|
|
152
|
+
*/
|
|
153
|
+
async getProjectProfile(projectPath) {
|
|
154
|
+
try {
|
|
155
|
+
const searchPath = projectPath || process.cwd();
|
|
156
|
+
// Check for .vibehub/config.json in project
|
|
157
|
+
const projectConfigPath = path.join(searchPath, '.vibehub', 'config.json');
|
|
158
|
+
if (await fs.pathExists(projectConfigPath)) {
|
|
159
|
+
const projectConfig = await fs.readJson(projectConfigPath);
|
|
160
|
+
if (projectConfig.profile) {
|
|
161
|
+
return projectConfig.profile;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Also check vibehub.json for backward compatibility
|
|
165
|
+
const vibehubConfigPath = path.join(searchPath, '.vibehub', 'vibehub.json');
|
|
166
|
+
if (await fs.pathExists(vibehubConfigPath)) {
|
|
167
|
+
const vibehubConfig = await fs.readJson(vibehubConfigPath);
|
|
168
|
+
if (vibehubConfig.profile) {
|
|
169
|
+
return vibehubConfig.profile;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// No project profile set
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Set the profile for the current project
|
|
180
|
+
*/
|
|
181
|
+
async setProjectProfile(profileName, projectPath) {
|
|
182
|
+
const searchPath = projectPath || process.cwd();
|
|
183
|
+
const configDir = path.join(searchPath, '.vibehub');
|
|
184
|
+
const configPath = path.join(configDir, 'config.json');
|
|
185
|
+
await fs.ensureDir(configDir);
|
|
186
|
+
// Load existing config or create new
|
|
187
|
+
let config = {};
|
|
188
|
+
try {
|
|
189
|
+
if (await fs.pathExists(configPath)) {
|
|
190
|
+
config = await fs.readJson(configPath);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Start with empty config
|
|
195
|
+
}
|
|
196
|
+
config.profile = profileName;
|
|
197
|
+
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Clear the project profile override
|
|
201
|
+
*/
|
|
202
|
+
async clearProjectProfile(projectPath) {
|
|
203
|
+
const searchPath = projectPath || process.cwd();
|
|
204
|
+
const configPath = path.join(searchPath, '.vibehub', 'config.json');
|
|
205
|
+
try {
|
|
206
|
+
if (await fs.pathExists(configPath)) {
|
|
207
|
+
const config = await fs.readJson(configPath);
|
|
208
|
+
delete config.profile;
|
|
209
|
+
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// No config to update
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get the active profile (project-level override or default)
|
|
218
|
+
*/
|
|
219
|
+
async getActiveProfile(projectPath) {
|
|
220
|
+
// 1. Check for project-level override
|
|
221
|
+
const projectProfileName = await this.getProjectProfile(projectPath);
|
|
222
|
+
if (projectProfileName) {
|
|
223
|
+
const profile = await this.getProfile(projectProfileName);
|
|
224
|
+
if (profile) {
|
|
225
|
+
return profile;
|
|
226
|
+
}
|
|
227
|
+
// Project specifies a profile that doesn't exist
|
|
228
|
+
console.warn(`Warning: Project specifies profile '${projectProfileName}' but it doesn't exist.`);
|
|
229
|
+
}
|
|
230
|
+
// 2. Fall back to default profile
|
|
231
|
+
const config = await this.getGlobalConfig();
|
|
232
|
+
return await this.getProfile(config.defaultProfile);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Check if a token is expired or about to expire
|
|
236
|
+
*/
|
|
237
|
+
isTokenExpired(profile, bufferMinutes = 5) {
|
|
238
|
+
const expiresAt = new Date(profile.tokenExpiresAt);
|
|
239
|
+
const now = new Date();
|
|
240
|
+
const bufferMs = bufferMinutes * 60 * 1000;
|
|
241
|
+
return expiresAt.getTime() - now.getTime() <= bufferMs;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Refresh tokens if needed (silent refresh using refresh token)
|
|
245
|
+
*/
|
|
246
|
+
async refreshTokenIfNeeded(profile) {
|
|
247
|
+
if (!this.isTokenExpired(profile)) {
|
|
248
|
+
// Update last used time
|
|
249
|
+
profile.lastUsedAt = new Date().toISOString();
|
|
250
|
+
await this.saveProfile(profile.name, profile);
|
|
251
|
+
return profile;
|
|
252
|
+
}
|
|
253
|
+
// Token is expired or about to expire, refresh it
|
|
254
|
+
try {
|
|
255
|
+
const { data, error } = await this.supabase.auth.setSession({
|
|
256
|
+
access_token: profile.accessToken,
|
|
257
|
+
refresh_token: profile.refreshToken
|
|
258
|
+
});
|
|
259
|
+
if (error || !data.session) {
|
|
260
|
+
throw new Error(error?.message || 'Failed to refresh session');
|
|
261
|
+
}
|
|
262
|
+
// Update profile with new tokens
|
|
263
|
+
const updatedProfile = {
|
|
264
|
+
...profile,
|
|
265
|
+
accessToken: data.session.access_token,
|
|
266
|
+
refreshToken: data.session.refresh_token || profile.refreshToken,
|
|
267
|
+
tokenExpiresAt: new Date(data.session.expires_at * 1000).toISOString(),
|
|
268
|
+
lastUsedAt: new Date().toISOString()
|
|
269
|
+
};
|
|
270
|
+
await this.saveProfile(profile.name, updatedProfile);
|
|
271
|
+
return updatedProfile;
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
throw new Error(`Token refresh failed: ${error.message}. Please run 'vibe login' again.`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Check if migration from old format is needed and perform it
|
|
279
|
+
*/
|
|
280
|
+
async migrateIfNeeded() {
|
|
281
|
+
try {
|
|
282
|
+
// Check for old-style config with user.token
|
|
283
|
+
const oldConfigPath = path.join(this.baseDir, 'config.json');
|
|
284
|
+
if (!await fs.pathExists(oldConfigPath)) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
const oldConfig = await fs.readJson(oldConfigPath);
|
|
288
|
+
// Check if already migrated (has version 2.0)
|
|
289
|
+
if (oldConfig.version === '2.0') {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
// Check for old user token format
|
|
293
|
+
if (oldConfig.user?.token) {
|
|
294
|
+
// Create backup
|
|
295
|
+
const backupPath = path.join(this.baseDir, 'config.backup.json');
|
|
296
|
+
await fs.copy(oldConfigPath, backupPath);
|
|
297
|
+
// Update config to new format
|
|
298
|
+
const newConfig = {
|
|
299
|
+
version: '2.0',
|
|
300
|
+
defaultProfile: 'default',
|
|
301
|
+
migratedFrom: '1.0'
|
|
302
|
+
};
|
|
303
|
+
await this.saveGlobalConfig(newConfig);
|
|
304
|
+
return true; // Migration needed - caller should prompt for re-authentication
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Get the profile status (for display)
|
|
314
|
+
*/
|
|
315
|
+
getProfileStatus(profile) {
|
|
316
|
+
const expiresAt = new Date(profile.tokenExpiresAt);
|
|
317
|
+
const now = new Date();
|
|
318
|
+
const diffMs = expiresAt.getTime() - now.getTime();
|
|
319
|
+
if (diffMs <= 0) {
|
|
320
|
+
return 'expired';
|
|
321
|
+
}
|
|
322
|
+
else if (diffMs <= 10 * 60 * 1000) { // 10 minutes
|
|
323
|
+
return 'expiring';
|
|
324
|
+
}
|
|
325
|
+
return 'active';
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Get time until token expires (formatted string)
|
|
329
|
+
*/
|
|
330
|
+
getTokenExpiryString(profile) {
|
|
331
|
+
const expiresAt = new Date(profile.tokenExpiresAt);
|
|
332
|
+
const now = new Date();
|
|
333
|
+
const diffMs = expiresAt.getTime() - now.getTime();
|
|
334
|
+
if (diffMs <= 0) {
|
|
335
|
+
return 'Expired';
|
|
336
|
+
}
|
|
337
|
+
const minutes = Math.floor(diffMs / 60000);
|
|
338
|
+
const hours = Math.floor(minutes / 60);
|
|
339
|
+
const days = Math.floor(hours / 24);
|
|
340
|
+
if (days > 0) {
|
|
341
|
+
return `${days} day${days > 1 ? 's' : ''}`;
|
|
342
|
+
}
|
|
343
|
+
else if (hours > 0) {
|
|
344
|
+
return `${hours} hour${hours > 1 ? 's' : ''}`;
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
return `${minutes} minute${minutes > 1 ? 's' : ''}`;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
//# sourceMappingURL=profile-manager.js.map
|