jobarbiter 0.3.11 → 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.
@@ -72,6 +72,24 @@ class Prompt {
72
72
  }
73
73
  });
74
74
  }
75
+ reset() {
76
+ // Recreate readline interface (needed after clack prompts take over stdin)
77
+ if (!this.closed) {
78
+ this.rl.removeAllListeners();
79
+ this.rl.close();
80
+ }
81
+ this.closed = false;
82
+ this.rl = readline.createInterface({
83
+ input: process.stdin,
84
+ output: process.stdout,
85
+ });
86
+ this.rl.on("close", () => {
87
+ if (!this.closed) {
88
+ console.log("\n\n" + c.dim("Onboarding cancelled. Run 'jobarbiter onboard' anytime to continue."));
89
+ process.exit(0);
90
+ }
91
+ });
92
+ }
75
93
  async question(prompt) {
76
94
  return new Promise((resolve) => {
77
95
  this.rl.question(prompt, (answer) => {
@@ -142,19 +160,68 @@ export async function runOnboardWizard(opts) {
142
160
  const userType = await selectUserType(prompt);
143
161
  state.userType = userType;
144
162
  // Step 2: Email & Verification
145
- const { email, apiKey, userId } = await handleEmailVerification(prompt, baseUrl, userType);
146
- state.email = email;
147
- state.apiKey = apiKey;
148
- 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;
149
172
  // Save config immediately after verification (with step progress)
150
173
  saveConfig({
151
- apiKey,
174
+ apiKey: verificationResult.apiKey,
152
175
  baseUrl,
153
- userType,
176
+ userType: effectiveUserType,
154
177
  onboardingStep: 1,
155
178
  onboardingComplete: false,
156
179
  });
157
- 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") {
158
225
  await runWorkerFlow(prompt, state);
159
226
  }
160
227
  else {
@@ -215,6 +282,7 @@ async function handleEmailVerification(prompt, baseUrl, userType) {
215
282
  }
216
283
  // Call register API
217
284
  console.log(c.dim("\nSending verification code..."));
285
+ let isReturningUser = false;
218
286
  try {
219
287
  await apiUnauthenticated(baseUrl, "POST", "/v1/auth/register", {
220
288
  email,
@@ -223,18 +291,34 @@ async function handleEmailVerification(prompt, baseUrl, userType) {
223
291
  }
224
292
  catch (err) {
225
293
  if (err instanceof ApiError && err.status === 409) {
226
- // Email already registered and verified
227
- 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
+ }
228
303
  }
229
- 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)}`);
230
313
  }
231
- console.log(`\n${sym.check} Verification code sent to ${c.highlight(email)}`);
232
314
  console.log(c.dim(" (Check your inbox and spam folder. Code expires in 15 minutes.)\n"));
233
315
  // Get verification code
234
316
  let apiKey;
235
317
  let userId;
318
+ let returnedUserType;
236
319
  let attempts = 0;
237
320
  const maxAttempts = 5;
321
+ const verifyEndpoint = isReturningUser ? "/v1/auth/verify-login" : "/v1/auth/verify";
238
322
  while (attempts < maxAttempts) {
239
323
  const code = await prompt.question(`Enter 6-digit code: `);
240
324
  if (!code || code.length !== 6) {
@@ -242,12 +326,15 @@ async function handleEmailVerification(prompt, baseUrl, userType) {
242
326
  continue;
243
327
  }
244
328
  try {
245
- const result = await apiUnauthenticated(baseUrl, "POST", "/v1/auth/verify", {
329
+ const result = await apiUnauthenticated(baseUrl, "POST", verifyEndpoint, {
246
330
  email,
247
331
  code: code.trim(),
248
332
  });
249
333
  apiKey = result.apiKey;
250
334
  userId = result.id;
335
+ if (isReturningUser) {
336
+ returnedUserType = result.userType;
337
+ }
251
338
  break;
252
339
  }
253
340
  catch (err) {
@@ -269,8 +356,105 @@ async function handleEmailVerification(prompt, baseUrl, userType) {
269
356
  if (!apiKey || !userId) {
270
357
  throw new Error("Verification failed. Please try again.");
271
358
  }
272
- console.log(`\n${sym.check} ${c.success("Email verified! Account created.")}\n`);
273
- 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
+ }
274
458
  }
275
459
  // ── Worker Flow ────────────────────────────────────────────────────────
276
460
  async function runWorkerFlow(prompt, state, startStep = 2) {
@@ -539,6 +723,8 @@ async function runConnectAIAccountsStep(prompt) {
539
723
  options: availableProviders,
540
724
  required: false,
541
725
  });
726
+ // Recreate readline after clack took over stdin
727
+ prompt.reset();
542
728
  if (clack.isCancel(selected) || !Array.isArray(selected) || selected.length === 0) {
543
729
  console.log(`\n${c.dim(" Skipped — you can connect providers later with 'jobarbiter tokens connect'")}\n`);
544
730
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jobarbiter",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "description": "CLI for JobArbiter — the first AI Proficiency Marketplace",
5
5
  "type": "module",
6
6
  "bin": {
@@ -96,6 +96,25 @@ class Prompt {
96
96
  });
97
97
  }
98
98
 
99
+ reset(): void {
100
+ // Recreate readline interface (needed after clack prompts take over stdin)
101
+ if (!this.closed) {
102
+ this.rl.removeAllListeners();
103
+ this.rl.close();
104
+ }
105
+ this.closed = false;
106
+ this.rl = readline.createInterface({
107
+ input: process.stdin,
108
+ output: process.stdout,
109
+ });
110
+ this.rl.on("close", () => {
111
+ if (!this.closed) {
112
+ console.log("\n\n" + c.dim("Onboarding cancelled. Run 'jobarbiter onboard' anytime to continue."));
113
+ process.exit(0);
114
+ }
115
+ });
116
+ }
117
+
99
118
  async question(prompt: string): Promise<string> {
100
119
  return new Promise((resolve) => {
101
120
  this.rl.question(prompt, (answer) => {
@@ -196,21 +215,71 @@ export async function runOnboardWizard(opts: { force?: boolean; baseUrl?: string
196
215
  state.userType = userType;
197
216
 
198
217
  // Step 2: Email & Verification
199
- const { email, apiKey, userId } = await handleEmailVerification(prompt, baseUrl, userType);
200
- state.email = email;
201
- state.apiKey = apiKey;
202
- 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;
203
228
 
204
229
  // Save config immediately after verification (with step progress)
205
230
  saveConfig({
206
- apiKey,
231
+ apiKey: verificationResult.apiKey,
207
232
  baseUrl,
208
- userType,
233
+ userType: effectiveUserType,
209
234
  onboardingStep: 1,
210
235
  onboardingComplete: false,
211
236
  });
212
237
 
213
- 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") {
214
283
  await runWorkerFlow(prompt, state as OnboardState);
215
284
  } else {
216
285
  await runEmployerFlow(prompt, state as OnboardState);
@@ -263,7 +332,7 @@ async function handleEmailVerification(
263
332
  prompt: Prompt,
264
333
  baseUrl: string,
265
334
  userType: "worker" | "employer"
266
- ): Promise<{ email: string; apiKey: string; userId: string }> {
335
+ ): Promise<{ email: string; apiKey: string; userId: string; isReturningUser: boolean; userType?: string }> {
267
336
  // Workers: 1) Account, 2) Tool Detection, 3) AI Accounts, 4) Domains, 5) GitHub, 6) LinkedIn, 7) Done
268
337
  // Employers: 1) Account, 2) (skip verification), 3) Company, 4) Domain, 5) What You Need, 6) Done (stays at 6)
269
338
  const totalSteps = userType === "employer" ? 6 : 7;
@@ -281,6 +350,8 @@ async function handleEmailVerification(
281
350
  // Call register API
282
351
  console.log(c.dim("\nSending verification code..."));
283
352
 
353
+ let isReturningUser = false;
354
+
284
355
  try {
285
356
  await apiUnauthenticated(baseUrl, "POST", "/v1/auth/register", {
286
357
  email,
@@ -288,37 +359,53 @@ async function handleEmailVerification(
288
359
  });
289
360
  } catch (err) {
290
361
  if (err instanceof ApiError && err.status === 409) {
291
- // Email already registered and verified
292
- 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;
293
372
  }
294
- throw err;
295
373
  }
296
374
 
297
- 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
+ }
298
380
  console.log(c.dim(" (Check your inbox and spam folder. Code expires in 15 minutes.)\n"));
299
381
 
300
382
  // Get verification code
301
383
  let apiKey: string | undefined;
302
384
  let userId: string | undefined;
385
+ let returnedUserType: string | undefined;
303
386
  let attempts = 0;
304
387
  const maxAttempts = 5;
388
+ const verifyEndpoint = isReturningUser ? "/v1/auth/verify-login" : "/v1/auth/verify";
305
389
 
306
390
  while (attempts < maxAttempts) {
307
391
  const code = await prompt.question(`Enter 6-digit code: `);
308
-
392
+
309
393
  if (!code || code.length !== 6) {
310
394
  console.log(c.error("Code must be 6 digits"));
311
395
  continue;
312
396
  }
313
397
 
314
398
  try {
315
- const result = await apiUnauthenticated(baseUrl, "POST", "/v1/auth/verify", {
399
+ const result = await apiUnauthenticated(baseUrl, "POST", verifyEndpoint, {
316
400
  email,
317
401
  code: code.trim(),
318
402
  });
319
403
 
320
404
  apiKey = result.apiKey as string;
321
405
  userId = result.id as string;
406
+ if (isReturningUser) {
407
+ returnedUserType = result.userType as string;
408
+ }
322
409
  break;
323
410
  } catch (err) {
324
411
  attempts++;
@@ -339,9 +426,125 @@ async function handleEmailVerification(
339
426
  throw new Error("Verification failed. Please try again.");
340
427
  }
341
428
 
342
- 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
+ }
343
434
 
344
- return { email, apiKey, userId };
435
+ return { email, apiKey, userId, isReturningUser, userType: returnedUserType };
436
+ }
437
+
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
+ }
345
548
  }
346
549
 
347
550
  // ── Worker Flow ────────────────────────────────────────────────────────
@@ -648,6 +851,9 @@ async function runConnectAIAccountsStep(prompt: Prompt): Promise<void> {
648
851
  required: false,
649
852
  });
650
853
 
854
+ // Recreate readline after clack took over stdin
855
+ prompt.reset();
856
+
651
857
  if (clack.isCancel(selected) || !Array.isArray(selected) || selected.length === 0) {
652
858
  console.log(`\n${c.dim(" Skipped — you can connect providers later with 'jobarbiter tokens connect'")}\n`);
653
859
  return;