@trendify/cli 0.1.5 → 0.1.7

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/app.d.ts CHANGED
@@ -1,6 +1,14 @@
1
+ import { type CliUpdateCheckResult } from './cli-update-service.js';
1
2
  export type AppProps = {
2
3
  readonly appVersion: string;
3
4
  readonly initialInput: string;
5
+ readonly initialNotification?: string | null;
6
+ readonly initialNotificationTone?: NotificationTone;
7
+ readonly onSelfUpdate: (update: Extract<CliUpdateCheckResult, {
8
+ status: 'available';
9
+ }>) => void | Promise<void>;
4
10
  };
5
- export declare function App({ appVersion, initialInput }: AppProps): import("react/jsx-runtime").JSX.Element;
11
+ type NotificationTone = 'error' | 'info' | 'success';
12
+ export declare function App({ appVersion, initialInput, initialNotification, initialNotificationTone, onSelfUpdate, }: AppProps): import("react/jsx-runtime").JSX.Element;
13
+ export {};
6
14
  //# sourceMappingURL=app.d.ts.map
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":"AAYA,OAAO,EAAqB,KAAK,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAMvF,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,QAAQ,CAAC,uBAAuB,CAAC,EAAE,gBAAgB,CAAC;IACpD,QAAQ,CAAC,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,oBAAoB,EAAE;QAAE,MAAM,EAAE,WAAW,CAAA;KAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjH,CAAC;AA6DF,KAAK,gBAAgB,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAyFrD,wBAAgB,GAAG,CAAC,EAClB,UAAU,EACV,YAAY,EACZ,mBAA0B,EAC1B,uBAAmC,EACnC,YAAY,GACb,EAAE,QAAQ,2CAmfV"}
package/dist/app.js CHANGED
@@ -1,11 +1,12 @@
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
+ import { checkForCliUpdate } from './cli-update-service.js';
9
10
  import { ActionMenuPage } from './shared/components/action-menu-page.js';
10
11
  import { AppMenu } from './shared/template/app-menu.js';
11
12
  import { AppShell } from './shared/template/app-shell.js';
@@ -80,34 +81,106 @@ const LOGOUT_OPTIONS = [
80
81
  function formatScopeLabel(scope) {
81
82
  return scope === 'amplo' ? 'mais ampla' : 'mais restrita';
82
83
  }
84
+ function formatWorkspaceRole(role) {
85
+ if (role === 'owner') {
86
+ return 'Proprietario';
87
+ }
88
+ if (role === 'admin') {
89
+ return 'Administrador';
90
+ }
91
+ return 'Membro';
92
+ }
83
93
  function getAuthenticatedUserLabel(user) {
84
94
  if (!user) {
85
95
  return 'usuario autenticado';
86
96
  }
87
97
  return getUserDisplayName(user) ?? user.email ?? 'usuario autenticado';
88
98
  }
99
+ function getActiveWorkspaceLabel(workspace) {
100
+ return workspace?.name ?? 'espaco de trabalho';
101
+ }
89
102
  function getWelcomeTitle(user) {
90
103
  const displayName = user ? getUserDisplayName(user) : null;
91
104
  if (displayName) {
92
- return `Bem-vindo de volta, ${displayName}.`;
105
+ return `Bem-vindo de volta, ${displayName} 👋`;
93
106
  }
94
- return 'Bem-vindo de volta.';
107
+ return 'Bem-vindo de volta. 👋';
95
108
  }
96
- export function App({ appVersion, initialInput }) {
109
+ function buildMenuItems(cliUpdate) {
110
+ if (cliUpdate.status !== 'available') {
111
+ return MENU_ITEMS;
112
+ }
113
+ const [discoveryItem, profileItem, logoutItem, exitItem] = MENU_ITEMS;
114
+ if (!discoveryItem || !profileItem || !logoutItem || !exitItem) {
115
+ return MENU_ITEMS;
116
+ }
117
+ return [
118
+ discoveryItem,
119
+ profileItem,
120
+ {
121
+ id: 'update-cli',
122
+ titulo: `Atualizar CLI para v${cliUpdate.latestVersion}`,
123
+ comando: 'atualizar-cli',
124
+ aliases: ['update', 'upgrade', 'atualizar', 'atualizar-cli', 'Atualizar CLI'],
125
+ descricao: `Instala a versao ${cliUpdate.latestVersion} do CLI e reinicia a aplicacao.`,
126
+ palavrasChave: [
127
+ 'versao',
128
+ 'nova versao',
129
+ 'ultima versao',
130
+ 'npm',
131
+ 'upgrade',
132
+ 'update',
133
+ 'atualizacao',
134
+ 'atualizar',
135
+ ],
136
+ },
137
+ logoutItem,
138
+ exitItem,
139
+ ];
140
+ }
141
+ export function App({ appVersion, initialInput, initialNotification = null, initialNotificationTone = 'success', onSelfUpdate, }) {
97
142
  const { exit } = useApp();
98
143
  const authBootstrap = useMemo(() => getAuthBootstrapResult(), []);
99
144
  const authService = authBootstrap.ok ? authBootstrap.authService : null;
100
145
  const sessionStorageFilePath = authService?.getSessionStorageFilePath() ?? '~/.trendify/auth/storage.json';
101
146
  const [screen, setScreen] = useState('menu');
102
- const [notification, setNotification] = useState(null);
103
- const [notificationTone, setNotificationTone] = useState('success');
147
+ const [notification, setNotification] = useState(initialNotification);
148
+ const [notificationTone, setNotificationTone] = useState(initialNotificationTone);
104
149
  const [menuKey, setMenuKey] = useState(0);
105
150
  const [authStatus, setAuthStatus] = useState(authBootstrap.ok ? 'booting' : 'config-error');
106
151
  const [authUser, setAuthUser] = useState(null);
152
+ const [availableWorkspaces, setAvailableWorkspaces] = useState([]);
153
+ const [activeWorkspace, setActiveWorkspace] = useState(null);
107
154
  const [email, setEmail] = useState('');
108
155
  const [password, setPassword] = useState('');
109
156
  const [authBusy, setAuthBusy] = useState(false);
110
157
  const [logoutSelectionIndex, setLogoutSelectionIndex] = useState(0);
158
+ const [workspaceSelectionIndex, setWorkspaceSelectionIndex] = useState(0);
159
+ const [cliUpdate, setCliUpdate] = useState({
160
+ currentVersion: appVersion,
161
+ packageName: '@trendify/cli',
162
+ status: 'checking',
163
+ });
164
+ const menuItems = useMemo(() => buildMenuItems(cliUpdate), [cliUpdate]);
165
+ function resetAuthenticatedContext() {
166
+ setAuthUser(null);
167
+ setAvailableWorkspaces([]);
168
+ setActiveWorkspace(null);
169
+ setWorkspaceSelectionIndex(0);
170
+ }
171
+ function applyAuthenticatedSnapshot(snapshot) {
172
+ setAuthStatus('authenticated');
173
+ setAuthUser(snapshot.user);
174
+ setAvailableWorkspaces(snapshot.workspaces);
175
+ setActiveWorkspace(snapshot.activeWorkspace);
176
+ setWorkspaceSelectionIndex(0);
177
+ if (snapshot.requiresWorkspaceSelection) {
178
+ setScreen('select-workspace');
179
+ return;
180
+ }
181
+ setScreen('menu');
182
+ setMenuKey((current) => current + 1);
183
+ }
111
184
  useInput((input, key) => {
112
185
  if (key.ctrl && input === 'c') {
113
186
  exit();
@@ -142,6 +215,43 @@ export function App({ appVersion, initialInput }) {
142
215
  setNotificationTone('info');
143
216
  }
144
217
  });
218
+ useInput((_input, key) => {
219
+ if (screen !== 'select-workspace' || authBusy) {
220
+ return;
221
+ }
222
+ if (!availableWorkspaces.length) {
223
+ return;
224
+ }
225
+ if (key.escape) {
226
+ void handleLogout();
227
+ return;
228
+ }
229
+ if (key.upArrow) {
230
+ setWorkspaceSelectionIndex((current) => (current === 0 ? availableWorkspaces.length - 1 : current - 1));
231
+ return;
232
+ }
233
+ if (key.downArrow) {
234
+ setWorkspaceSelectionIndex((current) => (current === availableWorkspaces.length - 1 ? 0 : current + 1));
235
+ return;
236
+ }
237
+ if (key.return) {
238
+ void handleWorkspaceSelection();
239
+ }
240
+ });
241
+ useEffect(() => {
242
+ let active = true;
243
+ const checkUpdates = async () => {
244
+ const result = await checkForCliUpdate(appVersion);
245
+ if (!active) {
246
+ return;
247
+ }
248
+ setCliUpdate(result);
249
+ };
250
+ void checkUpdates();
251
+ return () => {
252
+ active = false;
253
+ };
254
+ }, [appVersion]);
145
255
  useEffect(() => {
146
256
  if (!authService) {
147
257
  return;
@@ -158,12 +268,15 @@ export function App({ appVersion, initialInput }) {
158
268
  setNotificationTone('error');
159
269
  }
160
270
  if (result.data?.user) {
161
- setAuthStatus('authenticated');
162
- setAuthUser(result.data.user);
271
+ applyAuthenticatedSnapshot(result.data);
272
+ if (result.data.requiresWorkspaceSelection) {
273
+ setNotification('Escolha o espaco de trabalho que deseja usar nesta sessao.');
274
+ setNotificationTone('info');
275
+ }
163
276
  }
164
277
  else {
165
278
  setAuthStatus('unauthenticated');
166
- setAuthUser(null);
279
+ resetAuthenticatedContext();
167
280
  }
168
281
  setAuthBusy(false);
169
282
  };
@@ -173,13 +286,11 @@ export function App({ appVersion, initialInput }) {
173
286
  return;
174
287
  }
175
288
  if (snapshot.user) {
176
- setAuthStatus('authenticated');
177
289
  setAuthUser(snapshot.user);
178
- setScreen((current) => (current === 'menu' || current === 'discovery' || current === 'profile' ? current : 'menu'));
179
290
  return;
180
291
  }
181
292
  setAuthStatus('unauthenticated');
182
- setAuthUser(null);
293
+ resetAuthenticatedContext();
183
294
  setScreen('menu');
184
295
  setMenuKey((current) => current + 1);
185
296
  });
@@ -207,15 +318,39 @@ export function App({ appVersion, initialInput }) {
207
318
  setNotification(result.error ?? 'Nao foi possivel autenticar este usuario.');
208
319
  setNotificationTone('error');
209
320
  setAuthStatus('unauthenticated');
321
+ resetAuthenticatedContext();
210
322
  return;
211
323
  }
212
- setAuthStatus('authenticated');
213
- setAuthUser(result.data.user);
324
+ applyAuthenticatedSnapshot(result.data);
214
325
  setPassword('');
215
- setNotification(`Sessao iniciada para ${result.data.user.email ?? trimmedEmail}.`);
216
- setNotificationTone('success');
326
+ if (result.data.requiresWorkspaceSelection) {
327
+ setNotification('Login concluido. Escolha o espaco de trabalho para continuar.');
328
+ setNotificationTone('info');
329
+ return;
330
+ }
331
+ setNotification(null);
332
+ }
333
+ async function handleWorkspaceSelection() {
334
+ if (!authService || authBusy || !availableWorkspaces.length) {
335
+ return;
336
+ }
337
+ const workspace = availableWorkspaces[workspaceSelectionIndex];
338
+ if (!workspace) {
339
+ return;
340
+ }
341
+ setAuthBusy(true);
342
+ const result = await authService.selectWorkspace(workspace.id);
343
+ setAuthBusy(false);
344
+ if (result.error || !result.data) {
345
+ setNotification(result.error ?? 'Nao foi possivel ativar este espaco de trabalho.');
346
+ setNotificationTone('error');
347
+ return;
348
+ }
349
+ setActiveWorkspace(result.data);
217
350
  setScreen('menu');
218
351
  setMenuKey((current) => current + 1);
352
+ setNotification(`Espaco de trabalho ativo: ${result.data.name}.`);
353
+ setNotificationTone('success');
219
354
  }
220
355
  async function handleLogout() {
221
356
  if (!authService || authBusy) {
@@ -232,6 +367,7 @@ export function App({ appVersion, initialInput }) {
232
367
  setScreen('menu');
233
368
  setEmail('');
234
369
  setPassword('');
370
+ resetAuthenticatedContext();
235
371
  setMenuKey((current) => current + 1);
236
372
  setLogoutSelectionIndex(0);
237
373
  setNotification('Sessao local removida. Faca login novamente para continuar.');
@@ -241,15 +377,23 @@ export function App({ appVersion, initialInput }) {
241
377
  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
378
  }
243
379
  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.' }) }));
380
+ 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
381
  }
246
382
  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: () => {
383
+ 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
384
  void handleLogin();
249
385
  } }) }));
250
386
  }
387
+ if (screen === 'select-workspace') {
388
+ const workspaceOptions = availableWorkspaces.map((workspace) => ({
389
+ value: workspace.id,
390
+ label: workspace.name,
391
+ description: `${workspace.slug} • ${formatWorkspaceRole(workspace.role)}`,
392
+ }));
393
+ 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'] }) }) }));
394
+ }
251
395
  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: () => {
396
+ 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
397
  setScreen('menu');
254
398
  setMenuKey((current) => current + 1);
255
399
  }, onComplete: ({ theme, scope }) => {
@@ -260,7 +404,7 @@ export function App({ appVersion, initialInput }) {
260
404
  } }) }));
261
405
  }
262
406
  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: () => {
407
+ 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
408
  setScreen('menu');
265
409
  setMenuKey((current) => current + 1);
266
410
  }, onNotificationChange: (nextNotification, nextTone = 'success') => {
@@ -269,32 +413,36 @@ export function App({ appVersion, initialInput }) {
269
413
  } }) }));
270
414
  }
271
415
  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." }) }));
416
+ 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
417
  }
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: () => {
275
- if (notification) {
276
- setNotification(null);
277
- }
278
- }, onSelect: (item) => {
279
- if (item.id === 'logout') {
280
- setNotification(null);
281
- setNotificationTone('info');
282
- setLogoutSelectionIndex(0);
283
- setScreen('confirm-logout');
284
- return;
285
- }
286
- if (item.id === 'profile') {
418
+ return (_jsxs(AppShell, { appVersion: appVersion, title: getWelcomeTitle(authUser), subtitle: `Workspace: ${getActiveWorkspaceLabel(activeWorkspace)}\n\nUse a barra para abrir os comandos.`, notification: notification, notificationTone: notificationTone, children: [cliUpdate.status === 'available' ? (_jsxs(Text, { color: "yellowBright", children: ["Nova versao disponivel: v", cliUpdate.latestVersion, ". Abra o menu com / e escolha \"Atualizar CLI\"."] })) : null, _jsx(AppMenu, { initialInput: menuKey === 0 ? initialInput : '', items: menuItems, onInputChange: () => {
419
+ if (notification) {
420
+ setNotification(null);
421
+ }
422
+ }, onSelect: (item) => {
423
+ if (item.id === 'update-cli' && cliUpdate.status === 'available') {
424
+ void onSelfUpdate(cliUpdate);
425
+ return;
426
+ }
427
+ if (item.id === 'logout') {
428
+ setNotification(null);
429
+ setNotificationTone('info');
430
+ setLogoutSelectionIndex(0);
431
+ setScreen('confirm-logout');
432
+ return;
433
+ }
434
+ if (item.id === 'profile') {
435
+ setNotification(null);
436
+ setNotificationTone('success');
437
+ setScreen('profile');
438
+ return;
439
+ }
440
+ if (item.id === 'exit') {
441
+ exit();
442
+ return;
443
+ }
287
444
  setNotification(null);
288
445
  setNotificationTone('success');
289
- setScreen('profile');
290
- return;
291
- }
292
- if (item.id === 'exit') {
293
- exit();
294
- return;
295
- }
296
- setNotification(null);
297
- setNotificationTone('success');
298
- setScreen('discovery');
299
- } }, menuKey) }));
446
+ setScreen('discovery');
447
+ } }, menuKey)] }));
300
448
  }
@@ -0,0 +1,29 @@
1
+ export type CliUpdateCheckResult = {
2
+ readonly currentVersion: string;
3
+ readonly packageName: string;
4
+ readonly status: 'checking';
5
+ } | {
6
+ readonly currentVersion: string;
7
+ readonly latestVersion: string;
8
+ readonly packageName: string;
9
+ readonly status: 'available';
10
+ } | {
11
+ readonly currentVersion: string;
12
+ readonly latestVersion: string;
13
+ readonly packageName: string;
14
+ readonly status: 'current';
15
+ } | {
16
+ readonly currentVersion: string;
17
+ readonly message: string;
18
+ readonly packageName: string;
19
+ readonly status: 'error';
20
+ };
21
+ export type CliSelfUpdateResult = {
22
+ readonly ok: true;
23
+ } | {
24
+ readonly error: string;
25
+ readonly ok: false;
26
+ };
27
+ export declare function checkForCliUpdate(currentVersion: string): Promise<CliUpdateCheckResult>;
28
+ export declare function runCliSelfUpdate(packageName: string, latestVersion: string): Promise<CliSelfUpdateResult>;
29
+ //# sourceMappingURL=cli-update-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-update-service.d.ts","sourceRoot":"","sources":["../src/cli-update-service.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,oBAAoB,GAC5B;IACE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;CAC7B,GACD;IACE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B,GACD;IACE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;CAC5B,GACD;IACE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEN,MAAM,MAAM,mBAAmB,GAC3B;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;CACnB,GACD;IACE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;CACpB,CAAC;AA4MN,wBAAsB,iBAAiB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA6B7F;AAED,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAwB/G"}
@@ -0,0 +1,206 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+ const DEFAULT_PACKAGE_NAME = '@trendify/cli';
4
+ const VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/u;
5
+ function getPackageManifestPath() {
6
+ return new URL('../package.json', import.meta.url);
7
+ }
8
+ function getNpmCommand() {
9
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm';
10
+ }
11
+ function toErrorMessage(error) {
12
+ if (error instanceof Error && error.message.trim()) {
13
+ return error.message;
14
+ }
15
+ return 'Nao foi possivel concluir a operacao.';
16
+ }
17
+ function readInstalledPackageMetadata() {
18
+ try {
19
+ const manifestContents = readFileSync(getPackageManifestPath(), 'utf8');
20
+ const manifest = JSON.parse(manifestContents);
21
+ const packageName = typeof manifest.name === 'string' && manifest.name.trim().length > 0
22
+ ? manifest.name.trim()
23
+ : DEFAULT_PACKAGE_NAME;
24
+ return { packageName };
25
+ }
26
+ catch {
27
+ return { packageName: DEFAULT_PACKAGE_NAME };
28
+ }
29
+ }
30
+ function parseVersion(version) {
31
+ const match = VERSION_PATTERN.exec(version.trim());
32
+ if (!match) {
33
+ return null;
34
+ }
35
+ const [, major, minor, patch, prerelease] = match;
36
+ if (!major || !minor || !patch) {
37
+ return null;
38
+ }
39
+ return {
40
+ major: Number.parseInt(major, 10),
41
+ minor: Number.parseInt(minor, 10),
42
+ patch: Number.parseInt(patch, 10),
43
+ prerelease: prerelease ?? null,
44
+ };
45
+ }
46
+ function compareVersions(left, right) {
47
+ const parsedLeft = parseVersion(left);
48
+ const parsedRight = parseVersion(right);
49
+ if (!parsedLeft || !parsedRight) {
50
+ return left.localeCompare(right, undefined, { numeric: true, sensitivity: 'base' });
51
+ }
52
+ if (parsedLeft.major !== parsedRight.major) {
53
+ return parsedLeft.major - parsedRight.major;
54
+ }
55
+ if (parsedLeft.minor !== parsedRight.minor) {
56
+ return parsedLeft.minor - parsedRight.minor;
57
+ }
58
+ if (parsedLeft.patch !== parsedRight.patch) {
59
+ return parsedLeft.patch - parsedRight.patch;
60
+ }
61
+ if (parsedLeft.prerelease === parsedRight.prerelease) {
62
+ return 0;
63
+ }
64
+ if (parsedLeft.prerelease === null) {
65
+ return 1;
66
+ }
67
+ if (parsedRight.prerelease === null) {
68
+ return -1;
69
+ }
70
+ return parsedLeft.prerelease.localeCompare(parsedRight.prerelease, undefined, {
71
+ numeric: true,
72
+ sensitivity: 'base',
73
+ });
74
+ }
75
+ function runCommand(command, args) {
76
+ return new Promise((resolve, reject) => {
77
+ const child = spawn(command, [...args], {
78
+ env: process.env,
79
+ stdio: ['ignore', 'pipe', 'pipe'],
80
+ });
81
+ let stdout = '';
82
+ let stderr = '';
83
+ child.stdout.on('data', (chunk) => {
84
+ stdout += chunk.toString();
85
+ });
86
+ child.stderr.on('data', (chunk) => {
87
+ stderr += chunk.toString();
88
+ });
89
+ child.on('error', (error) => {
90
+ reject(error);
91
+ });
92
+ child.on('close', (code) => {
93
+ resolve({
94
+ code: code ?? 1,
95
+ stderr,
96
+ stdout,
97
+ });
98
+ });
99
+ });
100
+ }
101
+ async function fetchLatestPublishedVersion(packageName) {
102
+ const commandResult = await runCommand(getNpmCommand(), ['view', packageName, 'version', '--json', '--silent']);
103
+ if (commandResult.code !== 0) {
104
+ const message = commandResult.stderr.trim() || commandResult.stdout.trim() || 'Falha ao consultar o npm.';
105
+ throw new Error(message);
106
+ }
107
+ const stdout = commandResult.stdout.trim();
108
+ if (!stdout) {
109
+ throw new Error('O npm nao retornou a versao publicada mais recente.');
110
+ }
111
+ try {
112
+ const parsedOutput = JSON.parse(stdout);
113
+ if (typeof parsedOutput === 'string' && parsedOutput.trim()) {
114
+ return parsedOutput.trim();
115
+ }
116
+ }
117
+ catch {
118
+ if (stdout) {
119
+ return stdout.replace(/^"+|"+$/gu, '').trim();
120
+ }
121
+ }
122
+ throw new Error('Nao foi possivel interpretar a versao publicada retornada pelo npm.');
123
+ }
124
+ async function runCommandWithInheritedStdio(command, args) {
125
+ return new Promise((resolve, reject) => {
126
+ const child = spawn(command, [...args], {
127
+ env: process.env,
128
+ stdio: 'inherit',
129
+ });
130
+ child.on('error', (error) => {
131
+ reject(error);
132
+ });
133
+ child.on('close', (code) => {
134
+ resolve(code ?? 1);
135
+ });
136
+ });
137
+ }
138
+ async function restartCli() {
139
+ const entrypoint = process.argv[1];
140
+ if (!entrypoint) {
141
+ throw new Error('Nao foi possivel identificar o entrypoint atual da CLI para reiniciar o processo.');
142
+ }
143
+ await new Promise((resolve, reject) => {
144
+ const child = spawn(process.execPath, [...process.execArgv, entrypoint, ...process.argv.slice(2)], {
145
+ env: process.env,
146
+ stdio: 'inherit',
147
+ });
148
+ child.on('error', (error) => {
149
+ reject(error);
150
+ });
151
+ child.on('spawn', () => {
152
+ resolve();
153
+ });
154
+ });
155
+ }
156
+ export async function checkForCliUpdate(currentVersion) {
157
+ const { packageName } = readInstalledPackageMetadata();
158
+ try {
159
+ const latestVersion = await fetchLatestPublishedVersion(packageName);
160
+ if (compareVersions(latestVersion, currentVersion) > 0) {
161
+ return {
162
+ currentVersion,
163
+ latestVersion,
164
+ packageName,
165
+ status: 'available',
166
+ };
167
+ }
168
+ return {
169
+ currentVersion,
170
+ latestVersion,
171
+ packageName,
172
+ status: 'current',
173
+ };
174
+ }
175
+ catch (error) {
176
+ return {
177
+ currentVersion,
178
+ message: toErrorMessage(error),
179
+ packageName,
180
+ status: 'error',
181
+ };
182
+ }
183
+ }
184
+ export async function runCliSelfUpdate(packageName, latestVersion) {
185
+ try {
186
+ const exitCode = await runCommandWithInheritedStdio(getNpmCommand(), [
187
+ 'install',
188
+ '--global',
189
+ `${packageName}@${latestVersion}`,
190
+ ]);
191
+ if (exitCode !== 0) {
192
+ return {
193
+ error: `O npm encerrou a atualizacao com codigo ${exitCode}.`,
194
+ ok: false,
195
+ };
196
+ }
197
+ await restartCli();
198
+ return { ok: true };
199
+ }
200
+ catch (error) {
201
+ return {
202
+ error: toErrorMessage(error),
203
+ ok: false,
204
+ };
205
+ }
206
+ }
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { render } from 'ink';
4
4
  import { App } from './app.js';
5
+ import { runCliSelfUpdate } from './cli-update-service.js';
5
6
  import { APP_VERSION } from './version.js';
6
7
  const args = process.argv.slice(2);
7
8
  const initialInput = args
@@ -25,4 +26,26 @@ if (args.includes('--version') || args.includes('-v')) {
25
26
  process.exit(0);
26
27
  }
27
28
  console.clear();
28
- render(_jsx(App, { appVersion: APP_VERSION, initialInput: initialInput }));
29
+ let appInstance;
30
+ function mountApp(feedback = null) {
31
+ appInstance = render(_jsx(App, { appVersion: APP_VERSION, initialInput: initialInput, initialNotification: feedback?.message ?? null, initialNotificationTone: feedback?.tone ?? 'success', onSelfUpdate: async (update) => {
32
+ appInstance.unmount();
33
+ console.clear();
34
+ process.stdout.write([
35
+ `Atualizando o CLI de v${update.currentVersion} para v${update.latestVersion}.`,
36
+ `Pacote npm: ${update.packageName}`,
37
+ '',
38
+ ].join('\n'));
39
+ const result = await runCliSelfUpdate(update.packageName, update.latestVersion);
40
+ if (result.ok) {
41
+ process.exit(0);
42
+ }
43
+ process.stdout.write(`\nFalha ao atualizar automaticamente: ${result.error}\n`);
44
+ process.stdout.write('A CLI atual continuara disponivel.\n');
45
+ mountApp({
46
+ message: `Nao foi possivel atualizar a CLI automaticamente. ${result.error}`,
47
+ tone: 'error',
48
+ });
49
+ } }));
50
+ }
51
+ mountApp();
@@ -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.5";
1
+ export declare const APP_VERSION = "0.1.7";
2
2
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const APP_VERSION = '0.1.5';
1
+ export const APP_VERSION = '0.1.7';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trendify/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "CLI do Trendify para descoberta de temas e fluxos de conta.",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",