jobarbiter 0.3.12 → 0.3.13

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.
@@ -160,19 +160,68 @@ export async function runOnboardWizard(opts) {
160
160
  const userType = await selectUserType(prompt);
161
161
  state.userType = userType;
162
162
  // Step 2: Email & Verification
163
- const { email, apiKey, userId } = await handleEmailVerification(prompt, baseUrl, userType);
164
- state.email = email;
165
- state.apiKey = apiKey;
166
- state.userId = userId;
163
+ const verificationResult = await handleEmailVerification(prompt, baseUrl, userType);
164
+ state.email = verificationResult.email;
165
+ state.apiKey = verificationResult.apiKey;
166
+ state.userId = verificationResult.userId;
167
+ // If returning user, override userType from backend
168
+ const effectiveUserType = verificationResult.isReturningUser && verificationResult.userType
169
+ ? verificationResult.userType
170
+ : userType;
171
+ state.userType = effectiveUserType;
167
172
  // Save config immediately after verification (with step progress)
168
173
  saveConfig({
169
- apiKey,
174
+ apiKey: verificationResult.apiKey,
170
175
  baseUrl,
171
- userType,
176
+ userType: effectiveUserType,
172
177
  onboardingStep: 1,
173
178
  onboardingComplete: false,
174
179
  });
175
- if (userType === "worker") {
180
+ // Returning user: show progress and resume from first incomplete step
181
+ if (verificationResult.isReturningUser) {
182
+ const config = {
183
+ apiKey: verificationResult.apiKey,
184
+ baseUrl,
185
+ userType: effectiveUserType,
186
+ };
187
+ const progress = await fetchOnboardingProgress(config);
188
+ const { firstIncomplete } = showReturningUserProgress(progress);
189
+ if (firstIncomplete > (effectiveUserType === "worker" ? 6 : 5)) {
190
+ // Everything done except completion step
191
+ const continueAnyway = await prompt.confirm(`All steps look complete. Re-run onboarding anyway?`, false);
192
+ if (!continueAnyway) {
193
+ saveConfig({ ...config, onboardingComplete: true, onboardingStep: effectiveUserType === "worker" ? 7 : 6 });
194
+ console.log(`\n${sym.check} ${c.success("You're all set!")} Run ${c.highlight("jobarbiter status")} to check your account.\n`);
195
+ prompt.close();
196
+ return;
197
+ }
198
+ }
199
+ else {
200
+ const continueFromStep = await prompt.confirm(`Continue from step ${firstIncomplete}?`);
201
+ if (!continueFromStep) {
202
+ // Let them start from step 2
203
+ console.log(c.dim("Starting from the beginning...\n"));
204
+ if (effectiveUserType === "worker") {
205
+ await runWorkerFlow(prompt, state, 2);
206
+ }
207
+ else {
208
+ await runEmployerFlow(prompt, state);
209
+ }
210
+ prompt.close();
211
+ return;
212
+ }
213
+ }
214
+ // Resume from firstIncomplete
215
+ if (effectiveUserType === "worker") {
216
+ await runWorkerFlow(prompt, state, firstIncomplete);
217
+ }
218
+ else {
219
+ await runEmployerFlow(prompt, state);
220
+ }
221
+ prompt.close();
222
+ return;
223
+ }
224
+ if (effectiveUserType === "worker") {
176
225
  await runWorkerFlow(prompt, state);
177
226
  }
178
227
  else {
@@ -233,6 +282,7 @@ async function handleEmailVerification(prompt, baseUrl, userType) {
233
282
  }
234
283
  // Call register API
235
284
  console.log(c.dim("\nSending verification code..."));
285
+ let isReturningUser = false;
236
286
  try {
237
287
  await apiUnauthenticated(baseUrl, "POST", "/v1/auth/register", {
238
288
  email,
@@ -241,18 +291,34 @@ async function handleEmailVerification(prompt, baseUrl, userType) {
241
291
  }
242
292
  catch (err) {
243
293
  if (err instanceof ApiError && err.status === 409) {
244
- // Email already registered and verified
245
- throw new Error(`This email is already registered. Run 'jobarbiter verify-email --email ${email}' if you need to re-verify, or use a different email.`);
294
+ // Email already registered switch to login flow
295
+ isReturningUser = true;
296
+ console.log(`\n${sym.rocket} ${c.bold("Welcome back!")} Sending verification code...`);
297
+ try {
298
+ await apiUnauthenticated(baseUrl, "POST", "/v1/auth/login", { email });
299
+ }
300
+ catch (loginErr) {
301
+ throw new Error("Could not send login verification code. Please try again later.");
302
+ }
246
303
  }
247
- throw err;
304
+ else {
305
+ throw err;
306
+ }
307
+ }
308
+ if (!isReturningUser) {
309
+ console.log(`\n${sym.check} Verification code sent to ${c.highlight(email)}`);
310
+ }
311
+ else {
312
+ console.log(`${sym.check} Verification code sent to ${c.highlight(email)}`);
248
313
  }
249
- console.log(`\n${sym.check} Verification code sent to ${c.highlight(email)}`);
250
314
  console.log(c.dim(" (Check your inbox and spam folder. Code expires in 15 minutes.)\n"));
251
315
  // Get verification code
252
316
  let apiKey;
253
317
  let userId;
318
+ let returnedUserType;
254
319
  let attempts = 0;
255
320
  const maxAttempts = 5;
321
+ const verifyEndpoint = isReturningUser ? "/v1/auth/verify-login" : "/v1/auth/verify";
256
322
  while (attempts < maxAttempts) {
257
323
  const code = await prompt.question(`Enter 6-digit code: `);
258
324
  if (!code || code.length !== 6) {
@@ -260,12 +326,15 @@ async function handleEmailVerification(prompt, baseUrl, userType) {
260
326
  continue;
261
327
  }
262
328
  try {
263
- const result = await apiUnauthenticated(baseUrl, "POST", "/v1/auth/verify", {
329
+ const result = await apiUnauthenticated(baseUrl, "POST", verifyEndpoint, {
264
330
  email,
265
331
  code: code.trim(),
266
332
  });
267
333
  apiKey = result.apiKey;
268
334
  userId = result.id;
335
+ if (isReturningUser) {
336
+ returnedUserType = result.userType;
337
+ }
269
338
  break;
270
339
  }
271
340
  catch (err) {
@@ -287,8 +356,105 @@ async function handleEmailVerification(prompt, baseUrl, userType) {
287
356
  if (!apiKey || !userId) {
288
357
  throw new Error("Verification failed. Please try again.");
289
358
  }
290
- console.log(`\n${sym.check} ${c.success("Email verified! Account created.")}\n`);
291
- return { email, apiKey, userId };
359
+ if (isReturningUser) {
360
+ console.log(`\n${sym.check} ${c.success("Welcome back! Logged in successfully.")}\n`);
361
+ }
362
+ else {
363
+ console.log(`\n${sym.check} ${c.success("Email verified! Account created.")}\n`);
364
+ }
365
+ return { email, apiKey, userId, isReturningUser, userType: returnedUserType };
366
+ }
367
+ async function fetchOnboardingProgress(config) {
368
+ const progress = {
369
+ accountCreated: true,
370
+ userType: config.userType,
371
+ toolsDetected: [],
372
+ aiAccountsConnected: false,
373
+ domainsSet: [],
374
+ githubConnected: false,
375
+ linkedinConnected: false,
376
+ };
377
+ // Fetch profile
378
+ try {
379
+ const profile = await api(config, "GET", "/v1/profile");
380
+ if (profile.domains && Array.isArray(profile.domains) && profile.domains.length > 0) {
381
+ progress.domainsSet = profile.domains;
382
+ }
383
+ if (profile.tools && typeof profile.tools === "object") {
384
+ const tools = profile.tools;
385
+ if (tools.primary && Array.isArray(tools.primary) && tools.primary.length > 0) {
386
+ progress.toolsDetected = tools.primary;
387
+ }
388
+ }
389
+ if (profile.bio) {
390
+ progress.bio = profile.bio;
391
+ }
392
+ if (profile.githubUsername) {
393
+ progress.githubConnected = true;
394
+ progress.githubUsername = profile.githubUsername;
395
+ }
396
+ }
397
+ catch {
398
+ // Profile doesn't exist yet — that's fine
399
+ }
400
+ // Check AI accounts
401
+ const existingProviders = loadProviderKeys();
402
+ progress.aiAccountsConnected = existingProviders.length > 0;
403
+ // Check verification status (LinkedIn etc.)
404
+ try {
405
+ const verificationStatus = await api(config, "GET", "/v1/verification/status");
406
+ if (verificationStatus.linkedin) {
407
+ progress.linkedinConnected = true;
408
+ }
409
+ }
410
+ catch {
411
+ // Endpoint may not exist — gracefully ignore
412
+ }
413
+ return progress;
414
+ }
415
+ function showReturningUserProgress(progress) {
416
+ const isWorker = progress.userType === "worker";
417
+ const totalSteps = isWorker ? 7 : 6;
418
+ console.log(`${c.bold("Here's your onboarding progress:")}\n`);
419
+ if (isWorker) {
420
+ const steps = [
421
+ { done: true, label: `Account created (${progress.userType})` },
422
+ { done: progress.toolsDetected.length > 0, label: progress.toolsDetected.length > 0
423
+ ? `AI Tools detected (${progress.toolsDetected.join(", ")})`
424
+ : "AI Tools not detected" },
425
+ { done: progress.aiAccountsConnected, label: progress.aiAccountsConnected
426
+ ? "AI Accounts connected"
427
+ : "AI Accounts not connected" },
428
+ { done: progress.domainsSet.length > 0, label: progress.domainsSet.length > 0
429
+ ? `Domains set (${progress.domainsSet.join(", ")})`
430
+ : "Domains not set" },
431
+ { done: progress.githubConnected, label: progress.githubConnected
432
+ ? `GitHub connected (${progress.githubUsername})`
433
+ : "GitHub not connected" },
434
+ { done: progress.linkedinConnected, label: progress.linkedinConnected
435
+ ? "LinkedIn connected"
436
+ : "LinkedIn not connected" },
437
+ { done: false, label: "Completion" },
438
+ ];
439
+ let firstIncomplete = totalSteps; // default to last
440
+ for (let i = 0; i < steps.length; i++) {
441
+ const step = steps[i];
442
+ const icon = step.done ? sym.check : sym.cross;
443
+ console.log(` ${icon} Step ${i + 1}/${totalSteps} — ${step.label}`);
444
+ if (!step.done && firstIncomplete === totalSteps) {
445
+ firstIncomplete = i + 1;
446
+ }
447
+ }
448
+ console.log();
449
+ return { firstIncomplete };
450
+ }
451
+ else {
452
+ // Employer — simpler progress
453
+ console.log(` ${sym.check} Step 1/6 — Account created (employer)`);
454
+ console.log(` ${sym.cross} Step 2/6 — Remaining setup`);
455
+ console.log();
456
+ return { firstIncomplete: 2 };
457
+ }
292
458
  }
293
459
  // ── Worker Flow ────────────────────────────────────────────────────────
294
460
  async function runWorkerFlow(prompt, state, startStep = 2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jobarbiter",
3
- "version": "0.3.12",
3
+ "version": "0.3.13",
4
4
  "description": "CLI for JobArbiter — the first AI Proficiency Marketplace",
5
5
  "type": "module",
6
6
  "bin": {
@@ -215,21 +215,71 @@ export async function runOnboardWizard(opts: { force?: boolean; baseUrl?: string
215
215
  state.userType = userType;
216
216
 
217
217
  // Step 2: Email & Verification
218
- const { email, apiKey, userId } = await handleEmailVerification(prompt, baseUrl, userType);
219
- state.email = email;
220
- state.apiKey = apiKey;
221
- state.userId = userId;
218
+ const verificationResult = await handleEmailVerification(prompt, baseUrl, userType);
219
+ state.email = verificationResult.email;
220
+ state.apiKey = verificationResult.apiKey;
221
+ state.userId = verificationResult.userId;
222
+
223
+ // If returning user, override userType from backend
224
+ const effectiveUserType = verificationResult.isReturningUser && verificationResult.userType
225
+ ? (verificationResult.userType as "worker" | "employer")
226
+ : userType;
227
+ state.userType = effectiveUserType;
222
228
 
223
229
  // Save config immediately after verification (with step progress)
224
230
  saveConfig({
225
- apiKey,
231
+ apiKey: verificationResult.apiKey,
226
232
  baseUrl,
227
- userType,
233
+ userType: effectiveUserType,
228
234
  onboardingStep: 1,
229
235
  onboardingComplete: false,
230
236
  });
231
237
 
232
- if (userType === "worker") {
238
+ // Returning user: show progress and resume from first incomplete step
239
+ if (verificationResult.isReturningUser) {
240
+ const config: Config = {
241
+ apiKey: verificationResult.apiKey,
242
+ baseUrl,
243
+ userType: effectiveUserType,
244
+ };
245
+ const progress = await fetchOnboardingProgress(config);
246
+ const { firstIncomplete } = showReturningUserProgress(progress);
247
+
248
+ if (firstIncomplete > (effectiveUserType === "worker" ? 6 : 5)) {
249
+ // Everything done except completion step
250
+ const continueAnyway = await prompt.confirm(`All steps look complete. Re-run onboarding anyway?`, false);
251
+ if (!continueAnyway) {
252
+ saveConfig({ ...config, onboardingComplete: true, onboardingStep: effectiveUserType === "worker" ? 7 : 6 });
253
+ console.log(`\n${sym.check} ${c.success("You're all set!")} Run ${c.highlight("jobarbiter status")} to check your account.\n`);
254
+ prompt.close();
255
+ return;
256
+ }
257
+ } else {
258
+ const continueFromStep = await prompt.confirm(`Continue from step ${firstIncomplete}?`);
259
+ if (!continueFromStep) {
260
+ // Let them start from step 2
261
+ console.log(c.dim("Starting from the beginning...\n"));
262
+ if (effectiveUserType === "worker") {
263
+ await runWorkerFlow(prompt, state as OnboardState, 2);
264
+ } else {
265
+ await runEmployerFlow(prompt, state as OnboardState);
266
+ }
267
+ prompt.close();
268
+ return;
269
+ }
270
+ }
271
+
272
+ // Resume from firstIncomplete
273
+ if (effectiveUserType === "worker") {
274
+ await runWorkerFlow(prompt, state as OnboardState, firstIncomplete);
275
+ } else {
276
+ await runEmployerFlow(prompt, state as OnboardState);
277
+ }
278
+ prompt.close();
279
+ return;
280
+ }
281
+
282
+ if (effectiveUserType === "worker") {
233
283
  await runWorkerFlow(prompt, state as OnboardState);
234
284
  } else {
235
285
  await runEmployerFlow(prompt, state as OnboardState);
@@ -282,7 +332,7 @@ async function handleEmailVerification(
282
332
  prompt: Prompt,
283
333
  baseUrl: string,
284
334
  userType: "worker" | "employer"
285
- ): Promise<{ email: string; apiKey: string; userId: string }> {
335
+ ): Promise<{ email: string; apiKey: string; userId: string; isReturningUser: boolean; userType?: string }> {
286
336
  // Workers: 1) Account, 2) Tool Detection, 3) AI Accounts, 4) Domains, 5) GitHub, 6) LinkedIn, 7) Done
287
337
  // Employers: 1) Account, 2) (skip verification), 3) Company, 4) Domain, 5) What You Need, 6) Done (stays at 6)
288
338
  const totalSteps = userType === "employer" ? 6 : 7;
@@ -300,6 +350,8 @@ async function handleEmailVerification(
300
350
  // Call register API
301
351
  console.log(c.dim("\nSending verification code..."));
302
352
 
353
+ let isReturningUser = false;
354
+
303
355
  try {
304
356
  await apiUnauthenticated(baseUrl, "POST", "/v1/auth/register", {
305
357
  email,
@@ -307,37 +359,53 @@ async function handleEmailVerification(
307
359
  });
308
360
  } catch (err) {
309
361
  if (err instanceof ApiError && err.status === 409) {
310
- // Email already registered and verified
311
- throw new Error(`This email is already registered. Run 'jobarbiter verify-email --email ${email}' if you need to re-verify, or use a different email.`);
362
+ // Email already registered switch to login flow
363
+ isReturningUser = true;
364
+ console.log(`\n${sym.rocket} ${c.bold("Welcome back!")} Sending verification code...`);
365
+ try {
366
+ await apiUnauthenticated(baseUrl, "POST", "/v1/auth/login", { email });
367
+ } catch (loginErr) {
368
+ throw new Error("Could not send login verification code. Please try again later.");
369
+ }
370
+ } else {
371
+ throw err;
312
372
  }
313
- throw err;
314
373
  }
315
374
 
316
- console.log(`\n${sym.check} Verification code sent to ${c.highlight(email)}`);
375
+ if (!isReturningUser) {
376
+ console.log(`\n${sym.check} Verification code sent to ${c.highlight(email)}`);
377
+ } else {
378
+ console.log(`${sym.check} Verification code sent to ${c.highlight(email)}`);
379
+ }
317
380
  console.log(c.dim(" (Check your inbox and spam folder. Code expires in 15 minutes.)\n"));
318
381
 
319
382
  // Get verification code
320
383
  let apiKey: string | undefined;
321
384
  let userId: string | undefined;
385
+ let returnedUserType: string | undefined;
322
386
  let attempts = 0;
323
387
  const maxAttempts = 5;
388
+ const verifyEndpoint = isReturningUser ? "/v1/auth/verify-login" : "/v1/auth/verify";
324
389
 
325
390
  while (attempts < maxAttempts) {
326
391
  const code = await prompt.question(`Enter 6-digit code: `);
327
-
392
+
328
393
  if (!code || code.length !== 6) {
329
394
  console.log(c.error("Code must be 6 digits"));
330
395
  continue;
331
396
  }
332
397
 
333
398
  try {
334
- const result = await apiUnauthenticated(baseUrl, "POST", "/v1/auth/verify", {
399
+ const result = await apiUnauthenticated(baseUrl, "POST", verifyEndpoint, {
335
400
  email,
336
401
  code: code.trim(),
337
402
  });
338
403
 
339
404
  apiKey = result.apiKey as string;
340
405
  userId = result.id as string;
406
+ if (isReturningUser) {
407
+ returnedUserType = result.userType as string;
408
+ }
341
409
  break;
342
410
  } catch (err) {
343
411
  attempts++;
@@ -358,9 +426,125 @@ async function handleEmailVerification(
358
426
  throw new Error("Verification failed. Please try again.");
359
427
  }
360
428
 
361
- console.log(`\n${sym.check} ${c.success("Email verified! Account created.")}\n`);
429
+ if (isReturningUser) {
430
+ console.log(`\n${sym.check} ${c.success("Welcome back! Logged in successfully.")}\n`);
431
+ } else {
432
+ console.log(`\n${sym.check} ${c.success("Email verified! Account created.")}\n`);
433
+ }
434
+
435
+ return { email, apiKey, userId, isReturningUser, userType: returnedUserType };
436
+ }
362
437
 
363
- return { email, apiKey, userId };
438
+ // ── Returning User Progress ─────────────────────────────────────────────
439
+
440
+ interface OnboardProgress {
441
+ accountCreated: boolean;
442
+ userType: "worker" | "employer";
443
+ toolsDetected: string[];
444
+ aiAccountsConnected: boolean;
445
+ domainsSet: string[];
446
+ githubConnected: boolean;
447
+ linkedinConnected: boolean;
448
+ bio?: string;
449
+ githubUsername?: string;
450
+ }
451
+
452
+ async function fetchOnboardingProgress(config: Config): Promise<OnboardProgress> {
453
+ const progress: OnboardProgress = {
454
+ accountCreated: true,
455
+ userType: config.userType as "worker" | "employer",
456
+ toolsDetected: [],
457
+ aiAccountsConnected: false,
458
+ domainsSet: [],
459
+ githubConnected: false,
460
+ linkedinConnected: false,
461
+ };
462
+
463
+ // Fetch profile
464
+ try {
465
+ const profile = await api(config, "GET", "/v1/profile");
466
+ if (profile.domains && Array.isArray(profile.domains) && (profile.domains as string[]).length > 0) {
467
+ progress.domainsSet = profile.domains as string[];
468
+ }
469
+ if (profile.tools && typeof profile.tools === "object") {
470
+ const tools = profile.tools as Record<string, unknown>;
471
+ if (tools.primary && Array.isArray(tools.primary) && (tools.primary as string[]).length > 0) {
472
+ progress.toolsDetected = tools.primary as string[];
473
+ }
474
+ }
475
+ if (profile.bio) {
476
+ progress.bio = profile.bio as string;
477
+ }
478
+ if (profile.githubUsername) {
479
+ progress.githubConnected = true;
480
+ progress.githubUsername = profile.githubUsername as string;
481
+ }
482
+ } catch {
483
+ // Profile doesn't exist yet — that's fine
484
+ }
485
+
486
+ // Check AI accounts
487
+ const existingProviders = loadProviderKeys();
488
+ progress.aiAccountsConnected = existingProviders.length > 0;
489
+
490
+ // Check verification status (LinkedIn etc.)
491
+ try {
492
+ const verificationStatus = await api(config, "GET", "/v1/verification/status");
493
+ if (verificationStatus.linkedin) {
494
+ progress.linkedinConnected = true;
495
+ }
496
+ } catch {
497
+ // Endpoint may not exist — gracefully ignore
498
+ }
499
+
500
+ return progress;
501
+ }
502
+
503
+ function showReturningUserProgress(progress: OnboardProgress): { firstIncomplete: number } {
504
+ const isWorker = progress.userType === "worker";
505
+ const totalSteps = isWorker ? 7 : 6;
506
+
507
+ console.log(`${c.bold("Here's your onboarding progress:")}\n`);
508
+
509
+ if (isWorker) {
510
+ const steps: Array<{ done: boolean; label: string }> = [
511
+ { done: true, label: `Account created (${progress.userType})` },
512
+ { done: progress.toolsDetected.length > 0, label: progress.toolsDetected.length > 0
513
+ ? `AI Tools detected (${progress.toolsDetected.join(", ")})`
514
+ : "AI Tools not detected" },
515
+ { done: progress.aiAccountsConnected, label: progress.aiAccountsConnected
516
+ ? "AI Accounts connected"
517
+ : "AI Accounts not connected" },
518
+ { done: progress.domainsSet.length > 0, label: progress.domainsSet.length > 0
519
+ ? `Domains set (${progress.domainsSet.join(", ")})`
520
+ : "Domains not set" },
521
+ { done: progress.githubConnected, label: progress.githubConnected
522
+ ? `GitHub connected (${progress.githubUsername})`
523
+ : "GitHub not connected" },
524
+ { done: progress.linkedinConnected, label: progress.linkedinConnected
525
+ ? "LinkedIn connected"
526
+ : "LinkedIn not connected" },
527
+ { done: false, label: "Completion" },
528
+ ];
529
+
530
+ let firstIncomplete = totalSteps; // default to last
531
+ for (let i = 0; i < steps.length; i++) {
532
+ const step = steps[i];
533
+ const icon = step.done ? sym.check : sym.cross;
534
+ console.log(` ${icon} Step ${i + 1}/${totalSteps} — ${step.label}`);
535
+ if (!step.done && firstIncomplete === totalSteps) {
536
+ firstIncomplete = i + 1;
537
+ }
538
+ }
539
+ console.log();
540
+ return { firstIncomplete };
541
+ } else {
542
+ // Employer — simpler progress
543
+ console.log(` ${sym.check} Step 1/6 — Account created (employer)`);
544
+ console.log(` ${sym.cross} Step 2/6 — Remaining setup`);
545
+ console.log();
546
+ return { firstIncomplete: 2 };
547
+ }
364
548
  }
365
549
 
366
550
  // ── Worker Flow ────────────────────────────────────────────────────────