switchman-dev 0.1.10 → 0.1.12

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.10",
3
+ "version": "0.1.12",
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('');
@@ -134,12 +134,13 @@ export async function checkLicence() {
134
134
 
135
135
  // Try live validation
136
136
  try {
137
- const res = await fetch(VALIDATE_URL, {
138
- headers: {
139
- 'Authorization': `Bearer ${creds.access_token}`,
140
- 'apikey': SUPABASE_ANON,
141
- },
142
- });
137
+ const res = await fetch(VALIDATE_URL, {
138
+ headers: {
139
+ 'Authorization': `Bearer ${SUPABASE_ANON}`,
140
+ 'apikey': SUPABASE_ANON,
141
+ 'x-user-token': creds.access_token,
142
+ },
143
+ });
143
144
 
144
145
  if (!res.ok) {
145
146
  // If token is expired, try to refresh
@@ -224,153 +225,107 @@ async function refreshToken(refreshToken) {
224
225
  */
225
226
  export async function loginWithGitHub() {
226
227
  const { default: open } = await import('open');
227
- const { createServer } = await import('http');
228
-
229
- return new Promise((resolve) => {
230
- let server;
231
- const timeout = setTimeout(() => {
232
- server?.close();
233
- resolve({ success: false, error: 'timeout' });
234
- }, 5 * 60 * 1000);
235
-
236
- server = createServer(async (req, res) => {
237
- const url = new URL(req.url, 'http://localhost:7429');
238
-
239
- if (url.pathname === '/callback') {
240
- const code = url.searchParams.get('code');
241
- const accessToken = url.searchParams.get('access_token');
242
- const refreshToken = url.searchParams.get('refresh_token');
243
- const error = url.searchParams.get('error');
244
-
245
- res.writeHead(200, { 'Content-Type': 'text/html' });
246
- res.end(`
247
- <!DOCTYPE html><html>
248
- <head><style>
249
- body { background:#0b1020; color:#e6eef8; font-family:monospace;
250
- display:flex; align-items:center; justify-content:center;
251
- min-height:100vh; margin:0; }
252
- .box { text-align:center; }
253
- .ok { color:#4ade80; font-size:48px; }
254
- .err { color:#f87171; font-size:48px; }
255
- h2 { font-size:24px; margin:16px 0 8px; }
256
- p { color:#5f7189; }
257
- </style></head>
258
- <body><div class="box">
259
- <div class="${error ? 'err' : 'ok'}">${error ? '✕' : '✓'}</div>
260
- <h2>${error ? 'Sign in failed' : 'Signed in successfully'}</h2>
261
- <p>${error ? 'You can close this tab.' : 'You can close this tab and return to your terminal.'}</p>
262
- </div></body></html>
263
- `);
264
-
265
- clearTimeout(timeout);
266
- server.close();
267
-
268
- if (error) {
269
- resolve({ success: false, error });
270
- return;
271
- }
272
228
 
273
- // If Supabase sent the token directly as query params
274
- if (accessToken) {
275
- saveSession({
276
- access_token: accessToken,
277
- refresh_token: refreshToken ?? null,
278
- expires_in: 3600,
279
- user: null, // will be fetched on next checkLicence
280
- });
281
- // Fetch the user email from Supabase
282
- try {
283
- const userRes = await fetch(`${AUTH_URL}/user`, {
284
- headers: {
285
- 'Authorization': `Bearer ${accessToken}`,
286
- 'apikey': SUPABASE_ANON,
287
- },
288
- });
289
- if (userRes.ok) {
290
- const user = await userRes.json();
291
- const creds = readCredentials() || {};
292
- writeCredentials({ ...creds, email: user.email, user_id: user.id });
293
- resolve({ success: true, email: user.email });
294
- } else {
295
- resolve({ success: true, email: null });
296
- }
297
- } catch {
298
- resolve({ success: true, email: null });
299
- }
300
- return;
301
- }
302
-
303
- // Exchange the code for a session
304
- if (code) {
305
- try {
306
- const tokenRes = await fetch(`${AUTH_URL}/token?grant_type=pkce`, {
307
- method: 'POST',
308
- headers: {
309
- 'Content-Type': 'application/json',
310
- 'apikey': SUPABASE_ANON,
311
- },
312
- body: JSON.stringify({ auth_code: code }),
313
- });
314
-
315
- if (tokenRes.ok) {
316
- const session = await tokenRes.json();
317
- saveSession(session);
318
- resolve({ success: true, email: session.user?.email ?? null });
319
- return;
320
- }
321
-
322
- // Fallback exchange
323
- const exchangeRes = await fetch(`${AUTH_URL}/token?grant_type=authorization_code`, {
324
- method: 'POST',
325
- headers: {
326
- 'Content-Type': 'application/json',
327
- 'apikey': SUPABASE_ANON,
328
- },
329
- body: JSON.stringify({ code }),
330
- });
331
-
332
- if (exchangeRes.ok) {
333
- const session = await exchangeRes.json();
334
- saveSession(session);
335
- resolve({ success: true, email: session.user?.email ?? null });
336
- return;
337
- }
338
-
339
- resolve({ success: false, error: 'token_exchange_failed' });
340
- } catch (err) {
341
- resolve({ success: false, error: err.message });
342
- }
343
- return;
344
- }
229
+ // ── Step 1: Generate a short human-readable code ──────────────────────────
230
+ const adjectives = ['SWIFT', 'CLEAR', 'SAFE', 'CLEAN', 'FAST', 'BOLD', 'CALM', 'KEEN'];
231
+ const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
232
+ const num = Math.floor(1000 + Math.random() * 9000);
233
+ const code = `${adj}-${num}`;
345
234
 
346
- resolve({ success: false, error: 'no_code' });
347
- }
235
+ // ── Step 2: Store the pending code in Supabase ────────────────────────────
236
+ try {
237
+ const insertRes = await fetch(`${SUPABASE_URL}/rest/v1/cli_auth_codes`, {
238
+ method: 'POST',
239
+ headers: {
240
+ 'Content-Type': 'application/json',
241
+ 'apikey': SUPABASE_ANON,
242
+ 'Prefer': 'return=minimal',
243
+ },
244
+ body: JSON.stringify({
245
+ code,
246
+ status: 'pending',
247
+ }),
348
248
  });
349
249
 
350
- server.listen(7429, 'localhost', () => {
351
- const params = new URLSearchParams({
352
- provider: 'github',
353
- redirect_to: 'http://localhost:7429/callback',
354
- scopes: 'read:user user:email',
355
- });
250
+ if (!insertRes.ok) {
251
+ return { success: false, error: 'Could not create auth session. Please try again.' };
252
+ }
253
+ } catch {
254
+ return { success: false, error: 'Network error. Please check your connection.' };
255
+ }
356
256
 
357
- const loginUrl = `${AUTH_URL}/authorize?${params}`;
358
- console.log('');
359
- console.log(' Opening GitHub sign-in in your browser...');
360
- console.log(` If it doesn\'t open, visit: ${loginUrl}`);
361
- console.log('');
257
+ // ── Step 3: Open the activate page in the browser ─────────────────────────
258
+ const activateUrl = `https://switchman.dev/activate?code=${code}`;
362
259
 
363
- open(loginUrl).catch(() => {
364
- console.log(' Could not open browser automatically.');
365
- console.log(` Please visit: ${loginUrl}`);
366
- });
367
- });
260
+ console.log('');
261
+ console.log(' Visit this URL to sign in:');
262
+ console.log(` ${activateUrl}`);
263
+ console.log('');
264
+ console.log(` Your code: ${code}`);
265
+ console.log('');
266
+ console.log(' Waiting for authorization...');
368
267
 
369
- server.on('error', (err) => {
370
- clearTimeout(timeout);
371
- resolve({ success: false, error: err.message });
372
- });
268
+ open(activateUrl).catch(() => {
269
+ // Browser didn't open — user can copy the URL manually
373
270
  });
271
+
272
+ // ── Step 4: Poll Supabase every 2 seconds for up to 10 minutes ────────────
273
+ const POLL_INTERVAL_MS = 2000;
274
+ const MAX_WAIT_MS = 10 * 60 * 1000;
275
+ const started = Date.now();
276
+
277
+ while (Date.now() - started < MAX_WAIT_MS) {
278
+ await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
279
+
280
+ try {
281
+ const pollRes = await fetch(
282
+ `${SUPABASE_URL}/rest/v1/cli_auth_codes?code=eq.${code}&select=status,access_token,refresh_token,user_email,user_id`,
283
+ {
284
+ headers: {
285
+ 'apikey': SUPABASE_ANON,
286
+ 'Accept': 'application/json',
287
+ },
288
+ }
289
+ );
290
+
291
+ if (!pollRes.ok) continue;
292
+
293
+ const rows = await pollRes.json();
294
+ const row = rows?.[0];
295
+
296
+ if (!row) continue;
297
+
298
+ if (row.status === 'expired') {
299
+ return { success: false, error: 'Code expired. Please run switchman login again.' };
300
+ }
301
+
302
+ if (row.status === 'authorized' && row.access_token) {
303
+ // Save the credentials
304
+ writeCredentials({
305
+ access_token: row.access_token,
306
+ refresh_token: row.refresh_token ?? null,
307
+ expires_at: Date.now() + 3600 * 1000,
308
+ email: row.user_email ?? null,
309
+ user_id: row.user_id ?? null,
310
+ });
311
+ clearLicenceCache();
312
+
313
+ // Clean up the code row
314
+ fetch(`${SUPABASE_URL}/rest/v1/cli_auth_codes?code=eq.${code}`, {
315
+ method: 'DELETE',
316
+ headers: { 'apikey': SUPABASE_ANON },
317
+ }).catch(() => {});
318
+
319
+ return { success: true, email: row.user_email };
320
+ }
321
+
322
+ // status === 'pending' — keep polling
323
+ } catch {
324
+ // Network blip — keep polling
325
+ }
326
+ }
327
+
328
+ return { success: false, error: 'timeout' };
374
329
  }
375
330
 
376
331
  function saveSession(session) {
package/src.zip DELETED
Binary file