@trendify/cli 0.1.4 → 0.1.6

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/.env ADDED
@@ -0,0 +1,2 @@
1
+ SUPABASE_URL=https://aahmatblmzpnnjxcobiw.supabase.co
2
+ SUPABASE_PUBLISHABLE_DEFAULT_KEY=sb_publishable_NfUhgFipitVoRuDpvA1Itg_I8a87q_l
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  CLI do Trendify feita com Ink. O pacote publicado no npm e `@trendify/cli`, mas o comando instalado globalmente continua sendo `trendify`.
4
4
 
5
+ O pacote publicado leva um `.env` interno com a configuracao padrao do backend, entao a CLI instalada globalmente ja funciona sem o usuario precisar preencher variaveis do Supabase.
6
+
5
7
  ## Instalacao
6
8
 
7
9
  Publicacao do pacote:
@@ -18,7 +20,7 @@ trendify --help
18
20
 
19
21
  ## Ambiente
20
22
 
21
- Crie `apps/cli/.env` a partir de `apps/cli/.env.example` ou exporte estas variaveis no shell:
23
+ No desenvolvimento local, voce pode criar `apps/cli/.env` a partir de `apps/cli/.env.example` ou exportar estas variaveis no shell:
22
24
 
23
25
  ```sh
24
26
  SUPABASE_URL=
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAaA,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,CAAC;AAkGF,wBAAgB,GAAG,CAAC,EAAE,UAAU,EAAE,YAAY,EAAE,EAAE,QAAQ,2CAyVzD"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAiBA,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,CAAC;AAkHF,wBAAgB,GAAG,CAAC,EAAE,UAAU,EAAE,YAAY,EAAE,EAAE,QAAQ,2CA+czD"}
package/dist/app.js CHANGED
@@ -1,9 +1,9 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Text, useApp, useInput } from 'ink';
3
3
  import { useEffect, useMemo, useState } from 'react';
4
4
  import { DiscoveryPage } from './modules/discovery/page/discovery-page.js';
5
5
  import { getUserDisplayName } from './modules/auth/auth-user.js';
6
- import { getAuthBootstrapResult } from './modules/auth/auth-service.js';
6
+ import { getAuthBootstrapResult, } from './modules/auth/auth-service.js';
7
7
  import { LoginPage } from './modules/auth/page/login-page.js';
8
8
  import { ProfilePage } from './modules/profile/page/profile-page.js';
9
9
  import { ActionMenuPage } from './shared/components/action-menu-page.js';
@@ -80,18 +80,30 @@ const LOGOUT_OPTIONS = [
80
80
  function formatScopeLabel(scope) {
81
81
  return scope === 'amplo' ? 'mais ampla' : 'mais restrita';
82
82
  }
83
+ function formatWorkspaceRole(role) {
84
+ if (role === 'owner') {
85
+ return 'Proprietario';
86
+ }
87
+ if (role === 'admin') {
88
+ return 'Administrador';
89
+ }
90
+ return 'Membro';
91
+ }
83
92
  function getAuthenticatedUserLabel(user) {
84
93
  if (!user) {
85
94
  return 'usuario autenticado';
86
95
  }
87
96
  return getUserDisplayName(user) ?? user.email ?? 'usuario autenticado';
88
97
  }
98
+ function getActiveWorkspaceLabel(workspace) {
99
+ return workspace?.name ?? 'espaco de trabalho';
100
+ }
89
101
  function getWelcomeTitle(user) {
90
102
  const displayName = user ? getUserDisplayName(user) : null;
91
103
  if (displayName) {
92
- return `Bem-vindo de volta, ${displayName}.`;
104
+ return `Bem-vindo de volta, ${displayName} 👋`;
93
105
  }
94
- return 'Bem-vindo de volta.';
106
+ return 'Bem-vindo de volta. 👋';
95
107
  }
96
108
  export function App({ appVersion, initialInput }) {
97
109
  const { exit } = useApp();
@@ -104,10 +116,32 @@ export function App({ appVersion, initialInput }) {
104
116
  const [menuKey, setMenuKey] = useState(0);
105
117
  const [authStatus, setAuthStatus] = useState(authBootstrap.ok ? 'booting' : 'config-error');
106
118
  const [authUser, setAuthUser] = useState(null);
119
+ const [availableWorkspaces, setAvailableWorkspaces] = useState([]);
120
+ const [activeWorkspace, setActiveWorkspace] = useState(null);
107
121
  const [email, setEmail] = useState('');
108
122
  const [password, setPassword] = useState('');
109
123
  const [authBusy, setAuthBusy] = useState(false);
110
124
  const [logoutSelectionIndex, setLogoutSelectionIndex] = useState(0);
125
+ const [workspaceSelectionIndex, setWorkspaceSelectionIndex] = useState(0);
126
+ function resetAuthenticatedContext() {
127
+ setAuthUser(null);
128
+ setAvailableWorkspaces([]);
129
+ setActiveWorkspace(null);
130
+ setWorkspaceSelectionIndex(0);
131
+ }
132
+ function applyAuthenticatedSnapshot(snapshot) {
133
+ setAuthStatus('authenticated');
134
+ setAuthUser(snapshot.user);
135
+ setAvailableWorkspaces(snapshot.workspaces);
136
+ setActiveWorkspace(snapshot.activeWorkspace);
137
+ setWorkspaceSelectionIndex(0);
138
+ if (snapshot.requiresWorkspaceSelection) {
139
+ setScreen('select-workspace');
140
+ return;
141
+ }
142
+ setScreen('menu');
143
+ setMenuKey((current) => current + 1);
144
+ }
111
145
  useInput((input, key) => {
112
146
  if (key.ctrl && input === 'c') {
113
147
  exit();
@@ -142,6 +176,29 @@ export function App({ appVersion, initialInput }) {
142
176
  setNotificationTone('info');
143
177
  }
144
178
  });
179
+ useInput((_input, key) => {
180
+ if (screen !== 'select-workspace' || authBusy) {
181
+ return;
182
+ }
183
+ if (!availableWorkspaces.length) {
184
+ return;
185
+ }
186
+ if (key.escape) {
187
+ void handleLogout();
188
+ return;
189
+ }
190
+ if (key.upArrow) {
191
+ setWorkspaceSelectionIndex((current) => (current === 0 ? availableWorkspaces.length - 1 : current - 1));
192
+ return;
193
+ }
194
+ if (key.downArrow) {
195
+ setWorkspaceSelectionIndex((current) => (current === availableWorkspaces.length - 1 ? 0 : current + 1));
196
+ return;
197
+ }
198
+ if (key.return) {
199
+ void handleWorkspaceSelection();
200
+ }
201
+ });
145
202
  useEffect(() => {
146
203
  if (!authService) {
147
204
  return;
@@ -158,12 +215,15 @@ export function App({ appVersion, initialInput }) {
158
215
  setNotificationTone('error');
159
216
  }
160
217
  if (result.data?.user) {
161
- setAuthStatus('authenticated');
162
- setAuthUser(result.data.user);
218
+ applyAuthenticatedSnapshot(result.data);
219
+ if (result.data.requiresWorkspaceSelection) {
220
+ setNotification('Escolha o espaco de trabalho que deseja usar nesta sessao.');
221
+ setNotificationTone('info');
222
+ }
163
223
  }
164
224
  else {
165
225
  setAuthStatus('unauthenticated');
166
- setAuthUser(null);
226
+ resetAuthenticatedContext();
167
227
  }
168
228
  setAuthBusy(false);
169
229
  };
@@ -173,13 +233,11 @@ export function App({ appVersion, initialInput }) {
173
233
  return;
174
234
  }
175
235
  if (snapshot.user) {
176
- setAuthStatus('authenticated');
177
236
  setAuthUser(snapshot.user);
178
- setScreen((current) => (current === 'menu' || current === 'discovery' || current === 'profile' ? current : 'menu'));
179
237
  return;
180
238
  }
181
239
  setAuthStatus('unauthenticated');
182
- setAuthUser(null);
240
+ resetAuthenticatedContext();
183
241
  setScreen('menu');
184
242
  setMenuKey((current) => current + 1);
185
243
  });
@@ -207,15 +265,39 @@ export function App({ appVersion, initialInput }) {
207
265
  setNotification(result.error ?? 'Nao foi possivel autenticar este usuario.');
208
266
  setNotificationTone('error');
209
267
  setAuthStatus('unauthenticated');
268
+ resetAuthenticatedContext();
210
269
  return;
211
270
  }
212
- setAuthStatus('authenticated');
213
- setAuthUser(result.data.user);
271
+ applyAuthenticatedSnapshot(result.data);
214
272
  setPassword('');
215
- setNotification(`Sessao iniciada para ${result.data.user.email ?? trimmedEmail}.`);
216
- setNotificationTone('success');
273
+ if (result.data.requiresWorkspaceSelection) {
274
+ setNotification('Login concluido. Escolha o espaco de trabalho para continuar.');
275
+ setNotificationTone('info');
276
+ return;
277
+ }
278
+ setNotification(null);
279
+ }
280
+ async function handleWorkspaceSelection() {
281
+ if (!authService || authBusy || !availableWorkspaces.length) {
282
+ return;
283
+ }
284
+ const workspace = availableWorkspaces[workspaceSelectionIndex];
285
+ if (!workspace) {
286
+ return;
287
+ }
288
+ setAuthBusy(true);
289
+ const result = await authService.selectWorkspace(workspace.id);
290
+ setAuthBusy(false);
291
+ if (result.error || !result.data) {
292
+ setNotification(result.error ?? 'Nao foi possivel ativar este espaco de trabalho.');
293
+ setNotificationTone('error');
294
+ return;
295
+ }
296
+ setActiveWorkspace(result.data);
217
297
  setScreen('menu');
218
298
  setMenuKey((current) => current + 1);
299
+ setNotification(`Espaco de trabalho ativo: ${result.data.name}.`);
300
+ setNotificationTone('success');
219
301
  }
220
302
  async function handleLogout() {
221
303
  if (!authService || authBusy) {
@@ -232,6 +314,7 @@ export function App({ appVersion, initialInput }) {
232
314
  setScreen('menu');
233
315
  setEmail('');
234
316
  setPassword('');
317
+ resetAuthenticatedContext();
235
318
  setMenuKey((current) => current + 1);
236
319
  setLogoutSelectionIndex(0);
237
320
  setNotification('Sessao local removida. Faca login novamente para continuar.');
@@ -241,15 +324,23 @@ export function App({ appVersion, initialInput }) {
241
324
  return (_jsx(AppShell, { appVersion: appVersion, title: "Configuracao do Supabase pendente.", subtitle: "Defina as variaveis de ambiente antes de usar a CLI.", notification: authBootstrap.error, notificationTone: "error", children: _jsx(Text, { children: "Crie um arquivo .env com base em .env.example e preencha as chaves do projeto Supabase." }) }));
242
325
  }
243
326
  if (authStatus === 'booting') {
244
- return (_jsx(AppShell, { appVersion: appVersion, title: "Validando sua sessao.", subtitle: "A CLI esta consultando o Supabase para restaurar uma sessao existente.", notification: notification, notificationTone: notificationTone, children: _jsx(Text, { dimColor: true, children: authBusy ? 'Aguarde alguns instantes...' : 'Pronto para autenticar.' }) }));
327
+ return (_jsx(AppShell, { appVersion: appVersion, title: "Validando sua sessao.", subtitle: "A CLI esta consultando o Supabase para restaurar a sessao e o espaco de trabalho atual.", notification: notification, notificationTone: notificationTone, children: _jsx(Text, { dimColor: true, children: authBusy ? 'Aguarde alguns instantes...' : 'Pronto para autenticar.' }) }));
245
328
  }
246
329
  if (authStatus === 'unauthenticated') {
247
- return (_jsx(AppShell, { appVersion: appVersion, title: "Login obrigatorio.", subtitle: "Somente usuarios ja cadastrados no Supabase podem usar a CLI.", notification: notification, notificationTone: notificationTone, children: _jsx(LoginPage, { busy: authBusy, email: email, password: password, sessionStorageFilePath: sessionStorageFilePath, onEmailChange: setEmail, onPasswordChange: setPassword, onSubmit: () => {
330
+ return (_jsx(AppShell, { appVersion: appVersion, title: "Login obrigatorio.", subtitle: "Somente usuarios vinculados a um espaco de trabalho no Supabase podem usar a CLI.", notification: notification, notificationTone: notificationTone, children: _jsx(LoginPage, { busy: authBusy, email: email, password: password, sessionStorageFilePath: sessionStorageFilePath, onEmailChange: setEmail, onPasswordChange: setPassword, onSubmit: () => {
248
331
  void handleLogin();
249
332
  } }) }));
250
333
  }
334
+ if (screen === 'select-workspace') {
335
+ const workspaceOptions = availableWorkspaces.map((workspace) => ({
336
+ value: workspace.id,
337
+ label: workspace.name,
338
+ description: `${workspace.slug} • ${formatWorkspaceRole(workspace.role)}`,
339
+ }));
340
+ return (_jsx(AppShell, { appVersion: appVersion, title: "Escolha o espaco de trabalho", subtitle: `Conta autenticada para ${getAuthenticatedUserLabel(authUser)}. Selecione onde deseja operar.`, notification: notification, notificationTone: notificationTone, children: _jsx(ActionMenuPage, { title: "Espaco de trabalho ativo", subtitle: "Esse contexto sera usado nas consultas protegidas e nas regras de seguranca por linha.", options: workspaceOptions, selectedIndex: workspaceSelectionIndex, hintText: "Use as setas para navegar. Enter ativa o espaco. Esc encerra a sessao.", children: _jsxs(Text, { children: ["Conta: ", authUser?.email ?? 'nao informado'] }) }) }));
341
+ }
251
342
  if (screen === 'discovery') {
252
- return (_jsx(AppShell, { appVersion: appVersion, title: "Discovery", subtitle: `Sessao ativa para ${getAuthenticatedUserLabel(authUser)}.`, notification: notification, notificationTone: notificationTone, children: _jsx(DiscoveryPage, { onCancel: () => {
343
+ return (_jsx(AppShell, { appVersion: appVersion, title: "Discovery", subtitle: `Sessao ativa para ${getAuthenticatedUserLabel(authUser)} em ${getActiveWorkspaceLabel(activeWorkspace)}.`, notification: notification, notificationTone: notificationTone, children: _jsx(DiscoveryPage, { onCancel: () => {
253
344
  setScreen('menu');
254
345
  setMenuKey((current) => current + 1);
255
346
  }, onComplete: ({ theme, scope }) => {
@@ -260,7 +351,7 @@ export function App({ appVersion, initialInput }) {
260
351
  } }) }));
261
352
  }
262
353
  if (screen === 'profile' && authService && authUser) {
263
- return (_jsx(AppShell, { appVersion: appVersion, title: "Profile", subtitle: `Sessao ativa para ${getAuthenticatedUserLabel(authUser)}.`, notification: notification, notificationTone: notificationTone, children: _jsx(ProfilePage, { authService: authService, user: authUser, onBack: () => {
354
+ return (_jsx(AppShell, { appVersion: appVersion, title: "Profile", subtitle: `Sessao ativa para ${getAuthenticatedUserLabel(authUser)} em ${getActiveWorkspaceLabel(activeWorkspace)}.`, notification: notification, notificationTone: notificationTone, children: _jsx(ProfilePage, { authService: authService, user: authUser, onBack: () => {
264
355
  setScreen('menu');
265
356
  setMenuKey((current) => current + 1);
266
357
  }, onNotificationChange: (nextNotification, nextTone = 'success') => {
@@ -269,9 +360,9 @@ export function App({ appVersion, initialInput }) {
269
360
  } }) }));
270
361
  }
271
362
  if (screen === 'confirm-logout') {
272
- return (_jsx(AppShell, { appVersion: appVersion, title: "Confirmar encerramento de sessao", subtitle: `Voce esta autenticado como ${getAuthenticatedUserLabel(authUser)}.`, notification: notification, notificationTone: notificationTone, children: _jsx(ActionMenuPage, { title: "Tem certeza que deseja encerrar a sessao atual?", subtitle: "Se confirmar, a CLI vai remover a sessao salva localmente e pedir login novamente.", options: LOGOUT_OPTIONS, selectedIndex: logoutSelectionIndex, hintText: "Use as setas para navegar. Enter confirma. Esc volta ao menu." }) }));
363
+ return (_jsx(AppShell, { appVersion: appVersion, title: "Confirmar encerramento de sessao", subtitle: `Voce esta autenticado como ${getAuthenticatedUserLabel(authUser)} em ${getActiveWorkspaceLabel(activeWorkspace)}.`, notification: notification, notificationTone: notificationTone, children: _jsx(ActionMenuPage, { title: "Tem certeza que deseja encerrar a sessao atual?", subtitle: "Se confirmar, a CLI vai remover a sessao salva localmente e pedir login novamente.", options: LOGOUT_OPTIONS, selectedIndex: logoutSelectionIndex, hintText: "Use as setas para navegar. Enter confirma. Esc volta ao menu." }) }));
273
364
  }
274
- return (_jsx(AppShell, { appVersion: appVersion, title: getWelcomeTitle(authUser), subtitle: `Use a barra para abrir os comandos. Sessao ativa para ${getAuthenticatedUserLabel(authUser)}.`, notification: notification, notificationTone: notificationTone, children: _jsx(AppMenu, { initialInput: menuKey === 0 ? initialInput : '', items: MENU_ITEMS, onInputChange: () => {
365
+ return (_jsx(AppShell, { appVersion: appVersion, title: getWelcomeTitle(authUser), subtitle: `Workspace: ${getActiveWorkspaceLabel(activeWorkspace)}\n\nUse a barra para abrir os comandos.`, notification: notification, notificationTone: notificationTone, children: _jsx(AppMenu, { initialInput: menuKey === 0 ? initialInput : '', items: MENU_ITEMS, onInputChange: () => {
275
366
  if (notification) {
276
367
  setNotification(null);
277
368
  }
@@ -7,9 +7,9 @@ const currentDirectory = dirname(currentFilePath);
7
7
  const appRootDirectory = resolve(currentDirectory, '..', '..');
8
8
  const workspaceRootDirectory = resolve(appRootDirectory, '..', '..');
9
9
  const ENV_FILE_PATHS = [
10
- resolve(process.cwd(), '.env'),
11
10
  resolve(appRootDirectory, '.env'),
12
11
  resolve(workspaceRootDirectory, '.env'),
12
+ resolve(process.cwd(), '.env'),
13
13
  ];
14
14
  let envLoaded = false;
15
15
  function loadCliEnvFiles() {
@@ -1,8 +1,18 @@
1
- import { type AuthSession, type AuthUser } from '@supabase/supabase-js';
1
+ import { type AuthSession, type AuthUser, type SupabaseClient } from '@supabase/supabase-js';
2
2
  import { type CliEnv } from '../../config/env.js';
3
+ export type WorkspaceRole = 'admin' | 'member' | 'owner';
4
+ export type WorkspaceMembership = {
5
+ readonly id: string;
6
+ readonly name: string;
7
+ readonly role: WorkspaceRole;
8
+ readonly slug: string;
9
+ };
3
10
  export type AuthSnapshot = {
11
+ readonly activeWorkspace: WorkspaceMembership | null;
12
+ readonly requiresWorkspaceSelection: boolean;
4
13
  readonly session: AuthSession | null;
5
14
  readonly user: AuthUser | null;
15
+ readonly workspaces: readonly WorkspaceMembership[];
6
16
  };
7
17
  export type AuthActionResult<TData> = {
8
18
  readonly data: TData | null;
@@ -19,21 +29,32 @@ export type AuthBootstrapResult = {
19
29
  };
20
30
  export declare class AuthService {
21
31
  private readonly client;
32
+ private readonly storage;
33
+ private readonly env;
22
34
  private readonly storageFilePath;
23
35
  static create(env: CliEnv): AuthService;
24
36
  private constructor();
37
+ private activeWorkspaceId;
25
38
  getSessionStorageFilePath(): string;
26
39
  onAuthStateChange(listener: (snapshot: AuthSnapshot) => void): {
27
40
  data: {
28
41
  subscription: import("@supabase/supabase-js").Subscription;
29
42
  };
30
43
  };
44
+ getActiveWorkspaceId(): string | null;
45
+ getWorkspaceScopedClient(): Promise<AuthActionResult<SupabaseClient>>;
31
46
  restoreSession(): Promise<AuthActionResult<AuthSnapshot>>;
32
47
  signInWithPassword(email: string, password: string): Promise<AuthActionResult<AuthSnapshot>>;
33
48
  signOut(): Promise<AuthActionResult<null>>;
49
+ selectWorkspace(workspaceId: string): Promise<AuthActionResult<WorkspaceMembership>>;
34
50
  updateDisplayName(displayName: string): Promise<AuthActionResult<AuthUser>>;
35
51
  sendPasswordReauthenticationCode(): Promise<AuthActionResult<null>>;
36
52
  updatePassword(password: string, nonce?: string): Promise<AuthActionResult<AuthUser>>;
53
+ private buildAuthenticatedSnapshot;
54
+ private listMyWorkspaces;
55
+ private readStoredActiveWorkspaceId;
56
+ private storeActiveWorkspaceId;
57
+ private clearStoredActiveWorkspaceId;
37
58
  }
38
59
  export declare function getAuthBootstrapResult(): AuthBootstrapResult;
39
60
  //# sourceMappingURL=auth-service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-service.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,QAAQ,EAEd,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAA6B,KAAK,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAK7E,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,KAAK,IAChC;IACE,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC,CAAC;AAEJ,MAAM,MAAM,mBAAmB,GAC3B;IACE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;CACnB,GACD;IACE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;CACpB,CAAC;AAUN,qBAAa,WAAW;IAiBpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,eAAe;WAjBpB,MAAM,CAAC,GAAG,EAAE,MAAM;IAehC,OAAO;IAKA,yBAAyB,IAAI,MAAM;IAInC,iBAAiB,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI;;;;;IAStD,cAAc,IAAI,OAAO,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC;IA6DzD,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC;IAwC5F,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;IA0B1C,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAmE3E,gCAAgC,IAAI,OAAO,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;IA0BnE,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;CA0CnG;AAID,wBAAgB,sBAAsB,IAAI,mBAAmB,CAoB5D"}
1
+ {"version":3,"file":"auth-service.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth-service.ts"],"names":[],"mappings":"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"}
@@ -3,14 +3,39 @@ import { TRENDIFY_AUTH_STORAGE_FILE } from '../../config/app-paths.js';
3
3
  import { getCliEnvValidationResult } from '../../config/env.js';
4
4
  import { FileStorage } from './auth-storage.js';
5
5
  const AUTH_STORAGE_KEY = 'trendify.auth.token';
6
+ const ACTIVE_WORKSPACE_STORAGE_KEY = 'trendify.auth.active-workspace-id';
6
7
  function toAuthErrorMessage(error, fallbackMessage) {
7
8
  if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') {
8
9
  return error.message;
9
10
  }
10
11
  return fallbackMessage;
11
12
  }
13
+ function isWorkspaceRole(value) {
14
+ return value === 'owner' || value === 'admin' || value === 'member';
15
+ }
16
+ function normalizeWorkspaceMembership(value) {
17
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
18
+ return null;
19
+ }
20
+ const candidate = value;
21
+ const id = typeof candidate.workspace_id === 'string' ? candidate.workspace_id.trim() : '';
22
+ const name = typeof candidate.workspace_name === 'string' ? candidate.workspace_name.trim() : '';
23
+ const slug = typeof candidate.workspace_slug === 'string' ? candidate.workspace_slug.trim() : '';
24
+ const role = candidate.role;
25
+ if (!id || !name || !slug || !isWorkspaceRole(role)) {
26
+ return null;
27
+ }
28
+ return {
29
+ id,
30
+ name,
31
+ role,
32
+ slug,
33
+ };
34
+ }
12
35
  export class AuthService {
13
36
  client;
37
+ storage;
38
+ env;
14
39
  storageFilePath;
15
40
  static create(env) {
16
41
  const storage = new FileStorage(TRENDIFY_AUTH_STORAGE_FILE);
@@ -23,23 +48,85 @@ export class AuthService {
23
48
  storageKey: AUTH_STORAGE_KEY,
24
49
  },
25
50
  });
26
- return new AuthService(client, TRENDIFY_AUTH_STORAGE_FILE);
51
+ return new AuthService(client, storage, env, TRENDIFY_AUTH_STORAGE_FILE);
27
52
  }
28
- constructor(client, storageFilePath) {
53
+ constructor(client, storage, env, storageFilePath) {
29
54
  this.client = client;
55
+ this.storage = storage;
56
+ this.env = env;
30
57
  this.storageFilePath = storageFilePath;
58
+ this.activeWorkspaceId = null;
31
59
  }
60
+ activeWorkspaceId;
32
61
  getSessionStorageFilePath() {
33
62
  return this.storageFilePath;
34
63
  }
35
64
  onAuthStateChange(listener) {
36
65
  return this.client.auth.onAuthStateChange((_event, session) => {
37
66
  listener({
67
+ activeWorkspace: null,
68
+ requiresWorkspaceSelection: false,
38
69
  session,
39
70
  user: session?.user ?? null,
71
+ workspaces: [],
40
72
  });
41
73
  });
42
74
  }
75
+ getActiveWorkspaceId() {
76
+ return this.activeWorkspaceId;
77
+ }
78
+ async getWorkspaceScopedClient() {
79
+ try {
80
+ const { data: { session }, error, } = await this.client.auth.getSession();
81
+ if (error) {
82
+ return {
83
+ data: null,
84
+ error: error.message,
85
+ errorCode: error.code ?? null,
86
+ };
87
+ }
88
+ if (!session?.access_token) {
89
+ return {
90
+ data: null,
91
+ error: 'Nao existe uma sessao ativa para criar um cliente com espaco de trabalho.',
92
+ errorCode: null,
93
+ };
94
+ }
95
+ const activeWorkspaceId = this.activeWorkspaceId ?? (await this.readStoredActiveWorkspaceId());
96
+ if (!activeWorkspaceId) {
97
+ return {
98
+ data: null,
99
+ error: 'Selecione um espaco de trabalho antes de consultar dados protegidos.',
100
+ errorCode: null,
101
+ };
102
+ }
103
+ const workspaceClient = createClient(this.env.supabaseUrl, this.env.supabasePublishableDefaultKey, {
104
+ auth: {
105
+ autoRefreshToken: false,
106
+ detectSessionInUrl: false,
107
+ persistSession: false,
108
+ },
109
+ global: {
110
+ headers: {
111
+ Authorization: `Bearer ${session.access_token}`,
112
+ 'x-tenant-id': activeWorkspaceId,
113
+ },
114
+ },
115
+ });
116
+ return {
117
+ data: workspaceClient,
118
+ error: null,
119
+ errorCode: null,
120
+ };
121
+ }
122
+ catch (error) {
123
+ return {
124
+ data: null,
125
+ error: toAuthErrorMessage(error, 'Nao foi possivel preparar o cliente do espaco de trabalho atual.'),
126
+ errorCode: null,
127
+ };
128
+ }
129
+ }
43
130
  async restoreSession() {
44
131
  try {
45
132
  const { data: { session }, error: sessionError, } = await this.client.auth.getSession();
@@ -53,8 +140,11 @@ export class AuthService {
53
140
  if (!session) {
54
141
  return {
55
142
  data: {
143
+ activeWorkspace: null,
144
+ requiresWorkspaceSelection: false,
56
145
  session: null,
57
146
  user: null,
147
+ workspaces: [],
58
148
  },
59
149
  error: null,
60
150
  errorCode: null,
@@ -63,23 +153,20 @@ export class AuthService {
63
153
  const { data: { user }, error: userError, } = await this.client.auth.getUser();
64
154
  if (userError || !user) {
65
155
  await this.client.auth.signOut({ scope: 'local' });
156
+ await this.clearStoredActiveWorkspaceId();
66
157
  return {
67
158
  data: {
159
+ activeWorkspace: null,
160
+ requiresWorkspaceSelection: false,
68
161
  session: null,
69
162
  user: null,
163
+ workspaces: [],
70
164
  },
71
165
  error: userError?.message ?? 'Sua sessao expirou. Faca login novamente.',
72
166
  errorCode: userError?.code ?? null,
73
167
  };
74
168
  }
75
- return {
76
- data: {
77
- session,
78
- user,
79
- },
80
- error: null,
81
- errorCode: null,
82
- };
169
+ return await this.buildAuthenticatedSnapshot(session, user);
83
170
  }
84
171
  catch (error) {
85
172
  return {
@@ -109,14 +196,7 @@ export class AuthService {
109
196
  errorCode: null,
110
197
  };
111
198
  }
112
- return {
113
- data: {
114
- session: data.session,
115
- user: data.user,
116
- },
117
- error: null,
118
- errorCode: null,
119
- };
199
+ return await this.buildAuthenticatedSnapshot(data.session, data.user);
120
200
  }
121
201
  catch (error) {
122
202
  return {
@@ -136,6 +216,7 @@ export class AuthService {
136
216
  errorCode: error.code ?? null,
137
217
  };
138
218
  }
219
+ await this.clearStoredActiveWorkspaceId();
139
220
  return {
140
221
  data: null,
141
222
  error: null,
@@ -150,6 +231,47 @@ export class AuthService {
150
231
  };
151
232
  }
152
233
  }
234
+ async selectWorkspace(workspaceId) {
235
+ const trimmedWorkspaceId = workspaceId.trim();
236
+ if (!trimmedWorkspaceId) {
237
+ return {
238
+ data: null,
239
+ error: 'Informe um espaco de trabalho valido para continuar.',
240
+ errorCode: null,
241
+ };
242
+ }
243
+ try {
244
+ const workspaceResult = await this.listMyWorkspaces();
245
+ if (workspaceResult.error || !workspaceResult.data) {
246
+ return {
247
+ data: null,
248
+ error: workspaceResult.error ?? 'Nao foi possivel carregar os espacos de trabalho disponiveis.',
249
+ errorCode: workspaceResult.errorCode,
250
+ };
251
+ }
252
+ const selectedWorkspace = workspaceResult.data.find((workspace) => workspace.id === trimmedWorkspaceId);
253
+ if (!selectedWorkspace) {
254
+ return {
255
+ data: null,
256
+ error: 'Esse espaco de trabalho nao esta disponivel para a sua conta.',
257
+ errorCode: null,
258
+ };
259
+ }
260
+ await this.storeActiveWorkspaceId(selectedWorkspace.id);
261
+ return {
262
+ data: selectedWorkspace,
263
+ error: null,
264
+ errorCode: null,
265
+ };
266
+ }
267
+ catch (error) {
268
+ return {
269
+ data: null,
270
+ error: toAuthErrorMessage(error, 'Nao foi possivel selecionar o espaco de trabalho agora.'),
271
+ errorCode: null,
272
+ };
273
+ }
274
+ }
153
275
  async updateDisplayName(displayName) {
154
276
  try {
155
277
  const { data: { user: currentUser }, error: currentUserError, } = await this.client.auth.getUser();
@@ -268,6 +390,90 @@ export class AuthService {
268
390
  };
269
391
  }
270
392
  }
393
+ async buildAuthenticatedSnapshot(session, user) {
394
+ const workspaceResult = await this.listMyWorkspaces();
395
+ if (workspaceResult.error || !workspaceResult.data) {
396
+ return {
397
+ data: null,
398
+ error: workspaceResult.error ?? 'Nao foi possivel carregar os espacos de trabalho desta conta.',
399
+ errorCode: workspaceResult.errorCode,
400
+ };
401
+ }
402
+ if (workspaceResult.data.length === 0) {
403
+ await this.clearStoredActiveWorkspaceId();
404
+ return {
405
+ data: null,
406
+ error: 'Sua conta nao esta vinculada a nenhum espaco de trabalho. Fale com um administrador.',
407
+ errorCode: 'workspace_membership_missing',
408
+ };
409
+ }
410
+ const storedActiveWorkspaceId = await this.readStoredActiveWorkspaceId();
411
+ const storedWorkspace = storedActiveWorkspaceId
412
+ ? workspaceResult.data.find((workspace) => workspace.id === storedActiveWorkspaceId)
413
+ : null;
414
+ const singleWorkspace = workspaceResult.data.length === 1 ? workspaceResult.data[0] ?? null : null;
415
+ const activeWorkspace = storedWorkspace ?? singleWorkspace;
416
+ const requiresWorkspaceSelection = activeWorkspace === null;
417
+ if (activeWorkspace) {
418
+ await this.storeActiveWorkspaceId(activeWorkspace.id);
419
+ }
420
+ else {
421
+ await this.clearStoredActiveWorkspaceId();
422
+ }
423
+ return {
424
+ data: {
425
+ activeWorkspace,
426
+ requiresWorkspaceSelection,
427
+ session,
428
+ user,
429
+ workspaces: workspaceResult.data,
430
+ },
431
+ error: null,
432
+ errorCode: null,
433
+ };
434
+ }
435
+ async listMyWorkspaces() {
436
+ try {
437
+ const { data, error } = await this.client.rpc('list_my_workspaces');
438
+ if (error) {
439
+ return {
440
+ data: null,
441
+ error: error.message,
442
+ errorCode: error.code ?? null,
443
+ };
444
+ }
445
+ const workspaces = Array.isArray(data)
446
+ ? data
447
+ .map(normalizeWorkspaceMembership)
448
+ .filter((workspace) => workspace !== null)
449
+ : [];
450
+ return {
451
+ data: workspaces,
452
+ error: null,
453
+ errorCode: null,
454
+ };
455
+ }
456
+ catch (error) {
457
+ return {
458
+ data: null,
459
+ error: toAuthErrorMessage(error, 'Nao foi possivel consultar os espacos de trabalho desta conta.'),
460
+ errorCode: null,
461
+ };
462
+ }
463
+ }
464
+ async readStoredActiveWorkspaceId() {
465
+ const storedValue = await this.storage.getItem(ACTIVE_WORKSPACE_STORAGE_KEY);
466
+ const trimmedValue = storedValue?.trim() ?? '';
467
+ return trimmedValue ? trimmedValue : null;
468
+ }
469
+ async storeActiveWorkspaceId(workspaceId) {
470
+ this.activeWorkspaceId = workspaceId;
471
+ await this.storage.setItem(ACTIVE_WORKSPACE_STORAGE_KEY, workspaceId);
472
+ }
473
+ async clearStoredActiveWorkspaceId() {
474
+ this.activeWorkspaceId = null;
475
+ await this.storage.removeItem(ACTIVE_WORKSPACE_STORAGE_KEY);
476
+ }
271
477
  }
272
478
  let cachedAuthBootstrap = null;
273
479
  export function getAuthBootstrapResult() {
@@ -1 +1 @@
1
- {"version":3,"file":"login-page.d.ts","sourceRoot":"","sources":["../../../../src/modules/auth/page/login-page.tsx"],"names":[],"mappings":"AAIA,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,QAAQ,CAAC,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;CACzC,CAAC;AAIF,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,KAAK,EACL,aAAa,EACb,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,sBAAsB,GACvB,EAAE,cAAc,2CAqDhB"}
1
+ {"version":3,"file":"login-page.d.ts","sourceRoot":"","sources":["../../../../src/modules/auth/page/login-page.tsx"],"names":[],"mappings":"AAIA,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,QAAQ,CAAC,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;CACzC,CAAC;AAIF,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,KAAK,EACL,aAAa,EACb,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,sBAAsB,GACvB,EAAE,cAAc,2CAsDhB"}
@@ -12,7 +12,7 @@ export function LoginPage({ busy, email, onEmailChange, onPasswordChange, onSubm
12
12
  setFocusField((current) => (current === 'email' ? 'password' : 'email'));
13
13
  }
14
14
  });
15
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Entre com um usuario ja cadastrado no Supabase para liberar os comandos internos." }), _jsx(Text, { dimColor: true, children: "Cadastro nao esta habilitado nesta CLI." }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: focusField === 'email' ? 'greenBright' : 'white', children: "Email" }), _jsx(TextField, { focus: focusField === 'email', placeholder: "voce@empresa.com", value: email, onChange: onEmailChange, onSubmit: (value) => {
15
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Entre com um usuario ja cadastrado no Supabase para liberar os comandos internos." }), _jsx(Text, { dimColor: true, children: "Apos o login, a CLI tambem valida o espaco de trabalho vinculado a sua conta." }), _jsx(Text, { dimColor: true, children: "Cadastro nao esta habilitado nesta CLI." }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: focusField === 'email' ? 'greenBright' : 'white', children: "Email" }), _jsx(TextField, { focus: focusField === 'email', placeholder: "voce@empresa.com", value: email, onChange: onEmailChange, onSubmit: (value) => {
16
16
  if (value.trim()) {
17
17
  setFocusField('password');
18
18
  }
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const APP_VERSION = "0.1.4";
1
+ export declare const APP_VERSION = "0.1.6";
2
2
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const APP_VERSION = '0.1.4';
1
+ export const APP_VERSION = '0.1.6';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trendify/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "CLI do Trendify para descoberta de temas e fluxos de conta.",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "files": [
12
12
  "dist",
13
+ ".env",
13
14
  "README.md",
14
15
  ".env.example"
15
16
  ],