Skip to content

Commit 697cb43

Browse files
committed
adption login approach for the next launch
1 parent 9edaa10 commit 697cb43

6 files changed

Lines changed: 87 additions & 54 deletions

File tree

src/auth/ensureAuth.ts

Lines changed: 30 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,48 +12,35 @@ import { showModal } from '../ui/ink/components/Modal.js';
1212
import packageJson from '../../package.json' with { type: 'json' };
1313
import type { LoadedConfig } from '../types.js';
1414

15-
// Large FIGlet ASCII art — circles on left (lines 1-2), "Autohand Code" fills all lines
15+
// ASCII logo + ANSI Regular FIGlet "Autohand" — side-by-side, cross-platform
16+
const GAP = ' ';
1617
const LOGO_LINES = [
17-
'◎ ◎ ◎ ◎ ___ __ __ __ ______ __',
18-
'◎ ◎ ◎ ◎ / | __ __/ /_____ / /_ ____ _____ ____/ / / ____/___ ____/ /__',
19-
' / /| |/ / / / __/ __ \\/ __ \\/ __ `/ __ \\/ __ / / / / __ \\/ __ / _ \\',
20-
' / ___ / /_/ / /_/ /_/ / / / / /_/ / / / / /_/ / / /___/ /_/ / /_/ / __/',
21-
' /_/ |_\\__,_/\\__/\\____/_/ /_/\\__,_/_/ /_/\\__,_/ \\____/\\____/\\__,_/\\___/',
18+
'(@) (@) (@) (@)' + GAP + ' █████ ██ ██ ████████ ██████ ██ ██ █████ ███ ██ ██████',
19+
'(@) (@) (@) (@)' + GAP + '██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██',
20+
' ' + GAP + '███████ ██ ██ ██ ██ ██ ███████ ███████ ██ ██ ██ ██ ██',
21+
' ' + GAP + '██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██',
22+
' ' + GAP + '██ ██ ██████ ██ ██████ ██ ██ ██ ██ ██ ████ ██████',
2223
];
2324

25+
/** Total number of rendered lines. */
26+
const WELCOME_HEIGHT = LOGO_LINES.length;
27+
2428
/**
25-
* Typewriter: circles appear column by column on both rows,
26-
* then the full FIGlet text reveals line by line.
29+
* Typewriter: renders the logo line by line, horizontally centered.
2730
*/
2831
async function typewriteWelcome(startRow: number): Promise<void> {
29-
const hide = '\x1b[?25l';
30-
const show = '\x1b[?25h';
31-
const moveTo = (r: number, c: number) => `\x1b[${r};${c}H`;
32-
33-
process.stdout.write(hide);
34-
35-
// Phase 1: Type circles column by column (both rows at once)
36-
for (let col = 0; col < 4; col++) {
37-
const x = 1 + col * 2; // column position (◎ + space = 2 chars)
38-
process.stdout.write(moveTo(startRow, x) + chalk.white('◎'));
39-
process.stdout.write(moveTo(startRow + 1, x) + chalk.white('◎'));
40-
await new Promise(r => setTimeout(r, 100));
41-
}
32+
const cols = process.stdout.columns || 80;
33+
const maxWidth = Math.max(...LOGO_LINES.map(l => l.length));
34+
const leftPad = Math.max(0, Math.floor((cols - maxWidth) / 2));
35+
const pad = ' '.repeat(leftPad);
4236

43-
await new Promise(r => setTimeout(r, 150));
44-
45-
// Phase 2: Reveal the text portion of each line
37+
process.stdout.write('\x1b[?25l'); // hide cursor
4638
for (let i = 0; i < LOGO_LINES.length; i++) {
47-
const line = LOGO_LINES[i];
48-
// For circle lines (0,1), only write the text part after the circles
49-
const textStart = i < 2 ? 9 : 0; // circles take 9 chars "◎ ◎ ◎ ◎ "
50-
const text = i < 2 ? line.slice(textStart) : line;
51-
const col = i < 2 ? 10 : 1;
52-
process.stdout.write(moveTo(startRow + i, col) + chalk.white(text));
53-
await new Promise(r => setTimeout(r, 60));
39+
process.stdout.write(`\x1b[${startRow + i};1H\x1b[2K`);
40+
process.stdout.write(chalk.white(pad + LOGO_LINES[i]));
41+
await new Promise(r => setTimeout(r, 70));
5442
}
55-
56-
process.stdout.write(show);
43+
process.stdout.write('\x1b[?25h'); // show cursor
5744
}
5845

5946
/**
@@ -148,19 +135,22 @@ async function promptLogin(config: LoadedConfig): Promise<LoadedConfig> {
148135
// Clear screen and position content vertically centered
149136
process.stdout.write('\x1b[2J\x1b[H');
150137

151-
// Layout: logo (5 lines) + blank + version + blank + modal (~12 lines)
152-
const logoHeight = LOGO_LINES.length;
153-
const contentHeight = logoHeight + 6;
138+
// Layout: logo (8 lines) + blank + version + blank + modal (~12)
139+
const contentHeight = WELCOME_HEIGHT + 6;
154140
const topPadding = Math.max(0, Math.floor((rows - contentHeight) / 2));
155141
const logoRow = topPadding + 1; // 1-based terminal row
156142

157-
// Typewriter the circles, then reveal the FIGlet text
143+
// Typewriter: icon + FIGlet side-by-side
158144
await typewriteWelcome(logoRow);
159145

160-
// Position cursor below the logo for version + modal
161-
process.stdout.write(`\x1b[${logoRow + logoHeight};1H`);
146+
// Position cursor below the art for version + modal
147+
process.stdout.write(`\x1b[${logoRow + WELCOME_HEIGHT};1H`);
148+
const cols = process.stdout.columns || 80;
149+
const maxWidth = Math.max(...LOGO_LINES.map(l => l.length));
150+
const leftPad = Math.max(0, Math.floor((cols - maxWidth) / 2));
151+
const versionPad = ' '.repeat(leftPad + Math.floor((maxWidth - version.length) / 2));
162152
console.log();
163-
console.log(chalk.gray(` ${version}`));
153+
console.log(chalk.gray(`${versionPad}${version}`));
164154
console.log();
165155

166156
const selected = await showModal({

src/commands/chrome.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,7 @@ export async function chrome(ctx: ChromeCommandContext, args: string[] = []): Pr
162162

163163
case 'reconnect': {
164164
await nativeHostReady;
165-
await openChromeContinuation(
166-
buildChromeOpenUrl({ extensionId, installUrl: ctx.config?.chrome?.installUrl }),
167-
ctx.config?.chrome?.browser ?? 'auto',
168-
{ userDataDir: ctx.config?.chrome?.userDataDir, profileDirectory: ctx.config?.chrome?.profileDirectory },
169-
);
170-
return `${chalk.green('✓')} Native messaging host reinstalled.`;
165+
return `${chalk.green('✓')} Native messaging host reinstalled. Open the Chrome side panel manually if needed.`;
171166
}
172167
}
173168

src/commands/logout.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const metadata = {
1717
implemented: true,
1818
};
1919

20-
type LogoutContext = Pick<SlashCommandContext, 'config'>;
20+
type LogoutContext = Pick<SlashCommandContext, 'config' | 'currentSession'>;
2121

2222
export async function logout(ctx: LogoutContext): Promise<string | null> {
2323
const config = ctx.config as LoadedConfig;
@@ -53,6 +53,11 @@ export async function logout(ctx: LogoutContext): Promise<string | null> {
5353
// Server logout failed, but we still clear local token
5454
}
5555

56+
// Save current session before clearing auth
57+
if (ctx.currentSession) {
58+
await ctx.currentSession.save();
59+
}
60+
5661
// Clear auth from config
5762
const updatedConfig: LoadedConfig = {
5863
...config,
@@ -66,5 +71,6 @@ export async function logout(ctx: LogoutContext): Promise<string | null> {
6671
console.log(chalk.gray('Your local session has been cleared.'));
6772
console.log();
6873

69-
return null;
74+
// Login is enforced — exit the app after logout
75+
process.exit(0);
7076
}

src/core/slashCommandHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ export class SlashCommandHandler {
225225
const { logout } = await import('../commands/logout.js');
226226
this.ctx.onBeforeModal?.();
227227
try {
228-
return await logout({ config: this.ctx.config });
228+
return await logout({ config: this.ctx.config, currentSession: this.ctx.currentSession });
229229
} finally {
230230
this.ctx.onAfterModal?.();
231231
}

src/ui/inputPrompt.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2689,8 +2689,11 @@ function renderPromptLine(
26892689
readline.clearLine(output, 0);
26902690
}
26912691

2692-
// Move down, clearing remaining content + below + help panel + status + slash suggestions
2693-
const downCount = prevContentLines + PROMPT_LINES_BELOW_INPUT + lastRenderedHelpLines + STATUS_LINE_COUNT + lastRenderedSlashLines;
2692+
// Move down, clearing remaining content + below + help panel + status + slash suggestions.
2693+
// Use the larger of old/new line counts so shrinking (e.g. backspace reducing
2694+
// wrapped lines) still clears the full previous footprint.
2695+
const clearContentLines = Math.max(prevContentLines, state.lineCount);
2696+
const downCount = clearContentLines + PROMPT_LINES_BELOW_INPUT + lastRenderedHelpLines + STATUS_LINE_COUNT + lastRenderedSlashLines;
26942697
for (let i = 0; i < downCount; i++) {
26952698
readline.moveCursor(output, 0, 1);
26962699
readline.clearLine(output, 0);

tests/commands/auth.spec.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ describe('logout command', () => {
245245
expect(consoleOutput.some((line) => line.includes('cancelled'))).toBe(true);
246246
});
247247

248-
it('clears auth on confirmed logout', async () => {
248+
it('clears auth, saves session, and exits on confirmed logout', async () => {
249249
const mockConfig: LoadedConfig = {
250250
configPath: '/home/user/.autohand/config.json',
251251
auth: {
@@ -254,27 +254,62 @@ describe('logout command', () => {
254254
},
255255
};
256256

257+
const mockSession = { save: vi.fn().mockResolvedValue(undefined) };
257258
const mockAuthClient = {
258259
logout: vi.fn().mockResolvedValue(undefined),
259260
};
261+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
260262

261263
mockShowModal.mockResolvedValue({ value: 'yes' });
262264
(getAuthClient as ReturnType<typeof vi.fn>).mockReturnValue(mockAuthClient);
263265
(saveConfig as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
264266

265267
const { logout } = await import('../../src/commands/logout.js');
266-
await logout({ config: mockConfig });
268+
await logout({ config: mockConfig, currentSession: mockSession as any });
267269

268270
expect(mockAuthClient.logout).toHaveBeenCalledWith('existing-token');
271+
expect(mockSession.save).toHaveBeenCalled();
269272
expect(saveConfig).toHaveBeenCalledWith(
270273
expect.objectContaining({
271274
auth: undefined,
272275
})
273276
);
274277
expect(consoleOutput.some((line) => line.includes('Successfully logged out'))).toBe(true);
278+
expect(exitSpy).toHaveBeenCalledWith(0);
279+
280+
exitSpy.mockRestore();
275281
});
276282

277-
it('clears local auth even if server logout fails', async () => {
283+
it('exits even without an active session', async () => {
284+
const mockConfig: LoadedConfig = {
285+
configPath: '/home/user/.autohand/config.json',
286+
auth: {
287+
token: 'existing-token',
288+
user: { id: 'user-1', email: 'test@example.com', name: 'Test User' },
289+
},
290+
};
291+
292+
const mockAuthClient = {
293+
logout: vi.fn().mockResolvedValue(undefined),
294+
};
295+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
296+
297+
mockShowModal.mockResolvedValue({ value: 'yes' });
298+
(getAuthClient as ReturnType<typeof vi.fn>).mockReturnValue(mockAuthClient);
299+
(saveConfig as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
300+
301+
const { logout } = await import('../../src/commands/logout.js');
302+
await logout({ config: mockConfig });
303+
304+
expect(saveConfig).toHaveBeenCalledWith(
305+
expect.objectContaining({ auth: undefined })
306+
);
307+
expect(exitSpy).toHaveBeenCalledWith(0);
308+
309+
exitSpy.mockRestore();
310+
});
311+
312+
it('clears local auth and exits even if server logout fails', async () => {
278313
const mockConfig: LoadedConfig = {
279314
configPath: '/home/user/.autohand/config.json',
280315
auth: {
@@ -286,6 +321,7 @@ describe('logout command', () => {
286321
const mockAuthClient = {
287322
logout: vi.fn().mockRejectedValue(new Error('Network error')),
288323
};
324+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
289325

290326
mockShowModal.mockResolvedValue({ value: 'yes' });
291327
(getAuthClient as ReturnType<typeof vi.fn>).mockReturnValue(mockAuthClient);
@@ -301,6 +337,9 @@ describe('logout command', () => {
301337
})
302338
);
303339
expect(consoleOutput.some((line) => line.includes('Successfully logged out'))).toBe(true);
340+
expect(exitSpy).toHaveBeenCalledWith(0);
341+
342+
exitSpy.mockRestore();
304343
});
305344
});
306345

0 commit comments

Comments
 (0)