@gramatr/client 0.6.5 → 0.6.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/bin/gmtr-login.ts +118 -56
- package/package.json +1 -1
package/bin/gmtr-login.ts
CHANGED
|
@@ -48,76 +48,125 @@ const DASHBOARD_BASE = process.env.GMTR_DASHBOARD_URL || (() => {
|
|
|
48
48
|
|
|
49
49
|
// ── HTML Templates ──
|
|
50
50
|
|
|
51
|
+
// Brand tokens lifted directly from staging.gramatr.com BaseLayout.css.
|
|
52
|
+
// DO NOT introduce gradients, glassmorphism, or cyan accents — the real
|
|
53
|
+
// brand is flat near-black surfaces, #3B82F6 blue, Inter + Outfit.
|
|
51
54
|
const BRAND_CSS = `
|
|
55
|
+
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@600&family=Inter:wght@400;500;600;700&display=swap');
|
|
52
56
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
57
|
+
:root {
|
|
58
|
+
--gmtr-bg: #0E0E0E;
|
|
59
|
+
--gmtr-surface: #1A1A1A;
|
|
60
|
+
--gmtr-border: #333333;
|
|
61
|
+
--gmtr-text: #ECECEC;
|
|
62
|
+
--gmtr-text-secondary: #A0A0A0;
|
|
63
|
+
--gmtr-text-muted: #707070;
|
|
64
|
+
--gmtr-text-faint: #4A4A4A;
|
|
65
|
+
--gmtr-primary: #3B82F6;
|
|
66
|
+
--gmtr-accent: #60A5FA;
|
|
67
|
+
--gmtr-success: #4ADE80;
|
|
68
|
+
--gmtr-error: #F87171;
|
|
69
|
+
--font-wordmark: 'Outfit', -apple-system, sans-serif;
|
|
70
|
+
--font-heading: 'Inter', -apple-system, sans-serif;
|
|
71
|
+
--font-body: 'Inter', -apple-system, sans-serif;
|
|
72
|
+
--font-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
|
|
73
|
+
}
|
|
74
|
+
html, body { height: 100%; }
|
|
53
75
|
body {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
background: var(--gmtr-bg);
|
|
77
|
+
color: var(--gmtr-text);
|
|
78
|
+
font-family: var(--font-body);
|
|
79
|
+
font-weight: 400;
|
|
80
|
+
line-height: 1.7;
|
|
57
81
|
min-height: 100vh;
|
|
58
82
|
display: flex;
|
|
59
83
|
align-items: center;
|
|
60
84
|
justify-content: center;
|
|
85
|
+
padding: 24px;
|
|
86
|
+
-webkit-font-smoothing: antialiased;
|
|
87
|
+
-moz-osx-font-smoothing: grayscale;
|
|
61
88
|
}
|
|
62
89
|
.card {
|
|
63
|
-
background:
|
|
64
|
-
border: 1px solid
|
|
65
|
-
border-radius:
|
|
66
|
-
padding:
|
|
67
|
-
max-width:
|
|
68
|
-
width:
|
|
90
|
+
background: var(--gmtr-surface);
|
|
91
|
+
border: 1px solid var(--gmtr-border);
|
|
92
|
+
border-radius: 12px;
|
|
93
|
+
padding: 2.5rem;
|
|
94
|
+
max-width: 480px;
|
|
95
|
+
width: 100%;
|
|
69
96
|
text-align: center;
|
|
70
|
-
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
71
97
|
}
|
|
72
|
-
.
|
|
73
|
-
font-
|
|
74
|
-
font-weight:
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
.wordmark {
|
|
99
|
+
font-family: var(--font-wordmark);
|
|
100
|
+
font-weight: 600;
|
|
101
|
+
font-size: 1.5rem;
|
|
102
|
+
color: var(--gmtr-text);
|
|
103
|
+
letter-spacing: -0.02em;
|
|
104
|
+
margin-bottom: 0.25rem;
|
|
77
105
|
}
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
106
|
+
.tagline {
|
|
107
|
+
color: var(--gmtr-text-muted);
|
|
108
|
+
font-size: 0.75rem;
|
|
109
|
+
font-weight: 500;
|
|
110
|
+
text-transform: uppercase;
|
|
111
|
+
letter-spacing: 0.1em;
|
|
112
|
+
margin-bottom: 2rem;
|
|
84
113
|
}
|
|
85
|
-
.status {
|
|
86
|
-
|
|
87
|
-
|
|
114
|
+
.status-icon {
|
|
115
|
+
width: 56px;
|
|
116
|
+
height: 56px;
|
|
117
|
+
margin: 0 auto 1.25rem;
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
justify-content: center;
|
|
88
121
|
}
|
|
122
|
+
.status-icon svg { width: 56px; height: 56px; }
|
|
89
123
|
h2 {
|
|
90
|
-
font-
|
|
91
|
-
font-
|
|
92
|
-
|
|
124
|
+
font-family: var(--font-heading);
|
|
125
|
+
font-size: 1.5rem;
|
|
126
|
+
font-weight: 700;
|
|
127
|
+
line-height: 1.2;
|
|
128
|
+
color: var(--gmtr-text);
|
|
129
|
+
margin: 0 0 0.5rem;
|
|
93
130
|
}
|
|
94
|
-
h2.success { color: #00b4d8; }
|
|
95
|
-
h2.error { color: #e74c3c; }
|
|
96
131
|
p {
|
|
97
|
-
color:
|
|
98
|
-
font-size:
|
|
132
|
+
color: var(--gmtr-text-secondary);
|
|
133
|
+
font-size: 0.9375rem;
|
|
99
134
|
line-height: 1.6;
|
|
135
|
+
margin: 0;
|
|
100
136
|
}
|
|
101
137
|
.hint {
|
|
102
|
-
margin-top:
|
|
103
|
-
padding-top:
|
|
104
|
-
border-top: 1px solid
|
|
105
|
-
font-size:
|
|
106
|
-
color:
|
|
138
|
+
margin-top: 1.75rem;
|
|
139
|
+
padding-top: 1.25rem;
|
|
140
|
+
border-top: 1px solid var(--gmtr-border);
|
|
141
|
+
font-size: 0.8125rem;
|
|
142
|
+
color: var(--gmtr-text-muted);
|
|
143
|
+
}
|
|
144
|
+
.hint code {
|
|
145
|
+
font-family: var(--font-mono);
|
|
146
|
+
font-size: 0.8125rem;
|
|
147
|
+
background: var(--gmtr-bg);
|
|
148
|
+
border: 1px solid var(--gmtr-border);
|
|
149
|
+
color: var(--gmtr-accent);
|
|
150
|
+
padding: 0.15em 0.4em;
|
|
151
|
+
border-radius: 4px;
|
|
107
152
|
}
|
|
108
153
|
`;
|
|
109
154
|
|
|
155
|
+
// Flat, no pulse, no glow — matches the rest of gramatr.com.
|
|
156
|
+
const CHECK_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="#4ADE80" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="16 10 11 15 8 12"></polyline></svg>`;
|
|
157
|
+
const X_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="#F87171" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>`;
|
|
158
|
+
|
|
110
159
|
function htmlPage(title: string, body: string): string {
|
|
111
160
|
return `<!DOCTYPE html>
|
|
112
161
|
<html lang="en"><head>
|
|
113
162
|
<meta charset="utf-8">
|
|
114
163
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
115
|
-
<title>${title} —
|
|
164
|
+
<title>${title} — grāmatr</title>
|
|
116
165
|
<style>${BRAND_CSS}</style>
|
|
117
166
|
</head><body>
|
|
118
167
|
<div class="card">
|
|
119
|
-
<div class="
|
|
120
|
-
<div class="
|
|
168
|
+
<div class="wordmark">grāmatr</div>
|
|
169
|
+
<div class="tagline">Your AI gets smarter</div>
|
|
121
170
|
${body}
|
|
122
171
|
</div>
|
|
123
172
|
</body></html>`;
|
|
@@ -125,19 +174,19 @@ function htmlPage(title: string, body: string): string {
|
|
|
125
174
|
|
|
126
175
|
function successPage(): string {
|
|
127
176
|
return htmlPage('Authenticated', `
|
|
128
|
-
<div class="status"
|
|
129
|
-
<h2
|
|
130
|
-
<p>
|
|
131
|
-
<div class="hint">
|
|
177
|
+
<div class="status-icon">${CHECK_SVG}</div>
|
|
178
|
+
<h2>You're signed in</h2>
|
|
179
|
+
<p>Your token is saved on this machine. You can close this tab and return to your terminal.</p>
|
|
180
|
+
<div class="hint">grāmatr intelligence is now active across every AI tool you use.</div>
|
|
132
181
|
`);
|
|
133
182
|
}
|
|
134
183
|
|
|
135
184
|
function errorPage(title: string, detail: string): string {
|
|
136
185
|
return htmlPage('Error', `
|
|
137
|
-
<div class="status"
|
|
138
|
-
<h2
|
|
186
|
+
<div class="status-icon">${X_SVG}</div>
|
|
187
|
+
<h2>${title}</h2>
|
|
139
188
|
<p>${detail}</p>
|
|
140
|
-
<div class="hint">Return to your terminal and try again, or
|
|
189
|
+
<div class="hint">Return to your terminal and try again, or run <code>gmtr-login --token</code> to paste a token directly.</div>
|
|
141
190
|
`);
|
|
142
191
|
}
|
|
143
192
|
|
|
@@ -148,10 +197,11 @@ function isHeadless(forceFlag: boolean = false): boolean {
|
|
|
148
197
|
if (forceFlag) return true;
|
|
149
198
|
if (process.env.GRAMATR_LOGIN_HEADLESS === '1') return true;
|
|
150
199
|
|
|
151
|
-
// SSH session
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
200
|
+
// SSH session — always go headless. Even on macOS (which has a local
|
|
201
|
+
// display), `open` would launch Safari on the Mac's *physical* screen,
|
|
202
|
+
// not on the remote terminal where the user actually is. Device flow
|
|
203
|
+
// lets them paste the code on whichever machine they're sitting at.
|
|
204
|
+
if (process.env.SSH_CONNECTION || process.env.SSH_TTY) return true;
|
|
155
205
|
// Docker / CI / no TTY
|
|
156
206
|
if (process.env.CI || process.env.DOCKER) return true;
|
|
157
207
|
// Linux without display
|
|
@@ -493,23 +543,35 @@ async function loginBrowser(opts: { forceHeadless?: boolean } = {}): Promise<voi
|
|
|
493
543
|
const code = url.searchParams.get('code');
|
|
494
544
|
const returnedState = url.searchParams.get('state');
|
|
495
545
|
const error = url.searchParams.get('error');
|
|
546
|
+
// v0.6.6: `server.close()` alone does not terminate keep-alive
|
|
547
|
+
// sockets — the browser holds the connection open and the Node
|
|
548
|
+
// event loop never exits, so the CLI prints "Authenticated" and
|
|
549
|
+
// then hangs until Ctrl+C. Send `Connection: close` on the
|
|
550
|
+
// response and call `closeAllConnections()` to forcibly drop
|
|
551
|
+
// lingering sockets after we respond.
|
|
552
|
+
const shutdown = () => {
|
|
553
|
+
callbackServer.close();
|
|
554
|
+
if (typeof (callbackServer as any).closeAllConnections === 'function') {
|
|
555
|
+
(callbackServer as any).closeAllConnections();
|
|
556
|
+
}
|
|
557
|
+
};
|
|
496
558
|
if (error) {
|
|
497
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
559
|
+
res.writeHead(200, { 'Content-Type': 'text/html', Connection: 'close' });
|
|
498
560
|
res.end(errorPage('Authentication Failed', error));
|
|
499
|
-
|
|
561
|
+
shutdown();
|
|
500
562
|
reject(new Error(`OAuth error: ${error}`));
|
|
501
563
|
return;
|
|
502
564
|
}
|
|
503
565
|
if (!code || returnedState !== state) {
|
|
504
|
-
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
566
|
+
res.writeHead(400, { 'Content-Type': 'text/html', Connection: 'close' });
|
|
505
567
|
res.end(errorPage('Invalid Callback', 'Missing code or state mismatch. Please try again.'));
|
|
506
|
-
|
|
568
|
+
shutdown();
|
|
507
569
|
reject(new Error('Invalid callback'));
|
|
508
570
|
return;
|
|
509
571
|
}
|
|
510
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
572
|
+
res.writeHead(200, { 'Content-Type': 'text/html', Connection: 'close' });
|
|
511
573
|
res.end(successPage());
|
|
512
|
-
|
|
574
|
+
shutdown();
|
|
513
575
|
resolve(code);
|
|
514
576
|
});
|
|
515
577
|
codeTimeoutHandle = setTimeout(() => {
|