switchman-dev 0.1.9 → 0.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchman-dev",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Project manager for AI coding assistants running safely on one codebase",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/cli/index.js CHANGED
@@ -6352,19 +6352,22 @@ Examples:
6352
6352
  const licence = await checkLicence();
6353
6353
 
6354
6354
  console.log(` ${chalk.green('✓')} Signed in as ${chalk.cyan(result.email ?? 'unknown')}`);
6355
-
6355
+
6356
6356
  if (licence.valid) {
6357
+ console.log(` ${chalk.green('✓')} Pro licence verified — all features unlocked`);
6357
6358
  console.log(` ${chalk.green('✓')} Switchman Pro active`);
6358
6359
  console.log(` ${chalk.dim('Plan:')} ${licence.plan ?? 'Pro'}`);
6359
6360
  console.log('');
6360
6361
  console.log(` ${chalk.dim('Credentials saved · valid 24h · 7-day offline grace')}`);
6361
6362
  console.log('');
6362
- console.log(` Run ${chalk.cyan('switchman setup --agents 10')} to use unlimited agents.`);
6363
+ console.log(` Run ${chalk.cyan('switchman setup --agents 10')} to start with unlimited agents.`);
6363
6364
  } else {
6364
- console.log(` ${chalk.yellow('⚠')} No active Pro licence found`);
6365
+ console.log(` ${chalk.yellow('⚠')} Signed in — no Pro licence found yet`);
6366
+ console.log('');
6367
+ console.log(` ${chalk.dim('If you just subscribed, Polar may take 30–60 seconds to activate.')}`);
6368
+ console.log(` ${chalk.dim('Check your status with:')} ${chalk.cyan('switchman login --status')}`);
6365
6369
  console.log('');
6366
- console.log(` If you just paid, it may take a moment to activate.`);
6367
- console.log(` ${chalk.dim('Upgrade at:')} ${chalk.cyan(PRO_PAGE_URL)}`);
6370
+ console.log(` ${chalk.dim('Not subscribed yet?')} ${chalk.cyan(PRO_PAGE_URL)}`);
6368
6371
  }
6369
6372
 
6370
6373
  console.log('');
@@ -223,129 +223,108 @@ async function refreshToken(refreshToken) {
223
223
  * Returns { success, email } or { success: false, error }
224
224
  */
225
225
  export async function loginWithGitHub() {
226
- // Use Supabase's PKCE/implicit flow by opening the browser
227
- // with a special device-style URL that redirects to a local callback
228
226
  const { default: open } = await import('open');
229
227
 
230
- // We use a simple approach: direct the user to the Pro page sign-in
231
- // which sets the session, then we poll Supabase for the session
232
- // using a one-time code approach via the CLI callback server
233
-
234
- // Start a tiny local HTTP server to catch the OAuth callback
235
- const { createServer } = await import('http');
236
-
237
- return new Promise((resolve) => {
238
- let server;
239
- const timeout = setTimeout(() => {
240
- server?.close();
241
- resolve({ success: false, error: 'timeout' });
242
- }, 5 * 60 * 1000); // 5 minute timeout
243
-
244
- server = createServer(async (req, res) => {
245
- const url = new URL(req.url, 'http://localhost:7429');
246
-
247
- if (url.pathname === '/callback') {
248
- const code = url.searchParams.get('code');
249
- const error = url.searchParams.get('error');
250
-
251
- res.writeHead(200, { 'Content-Type': 'text/html' });
252
- res.end(`
253
- <!DOCTYPE html>
254
- <html>
255
- <head><style>
256
- body { background: #0b1020; color: #e6eef8; font-family: monospace;
257
- display: flex; align-items: center; justify-content: center;
258
- min-height: 100vh; margin: 0; }
259
- .box { text-align: center; }
260
- .ok { color: #4ade80; font-size: 48px; }
261
- h2 { font-size: 24px; margin: 16px 0 8px; }
262
- p { color: #5f7189; }
263
- </style></head>
264
- <body>
265
- <div class="box">
266
- <div class="ok">${error ? '✕' : '✓'}</div>
267
- <h2>${error ? 'Sign in failed' : 'Signed in successfully'}</h2>
268
- <p>${error ? 'You can close this tab.' : 'You can close this tab and return to your terminal.'}</p>
269
- </div>
270
- </body>
271
- </html>
272
- `);
273
-
274
- clearTimeout(timeout);
275
- server.close();
276
-
277
- if (error || !code) {
278
- resolve({ success: false, error: error || 'no_code' });
279
- return;
280
- }
228
+ // ── Step 1: Generate a short human-readable code ──────────────────────────
229
+ const adjectives = ['SWIFT', 'CLEAR', 'SAFE', 'CLEAN', 'FAST', 'BOLD', 'CALM', 'KEEN'];
230
+ const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
231
+ const num = Math.floor(1000 + Math.random() * 9000);
232
+ const code = `${adj}-${num}`;
281
233
 
282
- // Exchange the code for a session via Supabase
283
- try {
284
- const tokenRes = await fetch(`${AUTH_URL}/token?grant_type=pkce`, {
285
- method: 'POST',
286
- headers: {
287
- 'Content-Type': 'application/json',
288
- 'apikey': SUPABASE_ANON,
289
- },
290
- body: JSON.stringify({ auth_code: code }),
291
- });
292
-
293
- if (!tokenRes.ok) {
294
- // Try the standard exchange endpoint
295
- const exchangeRes = await fetch(`${AUTH_URL}/token?grant_type=authorization_code`, {
296
- method: 'POST',
297
- headers: {
298
- 'Content-Type': 'application/json',
299
- 'apikey': SUPABASE_ANON,
300
- },
301
- body: JSON.stringify({ code }),
302
- });
303
-
304
- if (!exchangeRes.ok) {
305
- resolve({ success: false, error: 'token_exchange_failed' });
306
- return;
307
- }
308
-
309
- const session = await exchangeRes.json();
310
- saveSession(session);
311
- resolve({ success: true, email: session.user?.email });
312
- return;
313
- }
314
-
315
- const session = await tokenRes.json();
316
- saveSession(session);
317
- resolve({ success: true, email: session.user?.email });
318
- } catch (err) {
319
- resolve({ success: false, error: err.message });
320
- }
321
- }
234
+ // ── Step 2: Store the pending code in Supabase ────────────────────────────
235
+ try {
236
+ const insertRes = await fetch(`${SUPABASE_URL}/rest/v1/cli_auth_codes`, {
237
+ method: 'POST',
238
+ headers: {
239
+ 'Content-Type': 'application/json',
240
+ 'apikey': SUPABASE_ANON,
241
+ 'Prefer': 'return=minimal',
242
+ },
243
+ body: JSON.stringify({
244
+ code,
245
+ status: 'pending',
246
+ }),
322
247
  });
323
248
 
324
- server.listen(7429, 'localhost', () => {
325
- // Build the Supabase GitHub OAuth URL with our local callback
326
- const params = new URLSearchParams({
327
- provider: 'github',
328
- redirect_to: 'http://localhost:7429/callback',
329
- scopes: 'read:user user:email',
330
- });
249
+ if (!insertRes.ok) {
250
+ return { success: false, error: 'Could not create auth session. Please try again.' };
251
+ }
252
+ } catch {
253
+ return { success: false, error: 'Network error. Please check your connection.' };
254
+ }
331
255
 
332
- const loginUrl = `${AUTH_URL}/authorize?${params}`;
333
- console.log('');
334
- console.log(' Opening GitHub sign-in in your browser...');
335
- console.log(` If it doesn\'t open, visit: ${loginUrl}`);
336
- console.log('');
256
+ // ── Step 3: Open the activate page in the browser ─────────────────────────
257
+ const activateUrl = `https://switchman.dev/activate?code=${code}`;
337
258
 
338
- open(loginUrl).catch(() => {
339
- console.log(` Could not open browser automatically.`);
340
- console.log(` Please visit: ${loginUrl}`);
341
- });
342
- });
259
+ console.log('');
260
+ console.log(' Visit this URL to sign in:');
261
+ console.log(` ${activateUrl}`);
262
+ console.log('');
263
+ console.log(` Your code: ${code}`);
264
+ console.log('');
265
+ console.log(' Waiting for authorization...');
343
266
 
344
- server.on('error', (err) => {
345
- clearTimeout(timeout);
346
- resolve({ success: false, error: err.message });
347
- });
267
+ open(activateUrl).catch(() => {
268
+ // Browser didn't open — user can copy the URL manually
348
269
  });
270
+
271
+ // ── Step 4: Poll Supabase every 2 seconds for up to 10 minutes ────────────
272
+ const POLL_INTERVAL_MS = 2000;
273
+ const MAX_WAIT_MS = 10 * 60 * 1000;
274
+ const started = Date.now();
275
+
276
+ while (Date.now() - started < MAX_WAIT_MS) {
277
+ await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
278
+
279
+ try {
280
+ const pollRes = await fetch(
281
+ `${SUPABASE_URL}/rest/v1/cli_auth_codes?code=eq.${code}&select=status,access_token,refresh_token,user_email,user_id`,
282
+ {
283
+ headers: {
284
+ 'apikey': SUPABASE_ANON,
285
+ 'Accept': 'application/json',
286
+ },
287
+ }
288
+ );
289
+
290
+ if (!pollRes.ok) continue;
291
+
292
+ const rows = await pollRes.json();
293
+ const row = rows?.[0];
294
+
295
+ if (!row) continue;
296
+
297
+ if (row.status === 'expired') {
298
+ return { success: false, error: 'Code expired. Please run switchman login again.' };
299
+ }
300
+
301
+ if (row.status === 'authorized' && row.access_token) {
302
+ // Save the credentials
303
+ writeCredentials({
304
+ access_token: row.access_token,
305
+ refresh_token: row.refresh_token ?? null,
306
+ expires_at: Date.now() + 3600 * 1000,
307
+ email: row.user_email ?? null,
308
+ user_id: row.user_id ?? null,
309
+ });
310
+ clearLicenceCache();
311
+
312
+ // Clean up the code row
313
+ fetch(`${SUPABASE_URL}/rest/v1/cli_auth_codes?code=eq.${code}`, {
314
+ method: 'DELETE',
315
+ headers: { 'apikey': SUPABASE_ANON },
316
+ }).catch(() => {});
317
+
318
+ return { success: true, email: row.user_email };
319
+ }
320
+
321
+ // status === 'pending' — keep polling
322
+ } catch {
323
+ // Network blip — keep polling
324
+ }
325
+ }
326
+
327
+ return { success: false, error: 'timeout' };
349
328
  }
350
329
 
351
330
  function saveSession(session) {
package/src.zip DELETED
Binary file