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.
- package/dist/lib/onboard.js +200 -14
- package/package.json +1 -1
- package/src/lib/onboard.ts +222 -16
package/dist/lib/onboard.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
227
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
273
|
-
|
|
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
package/src/lib/onboard.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
292
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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;
|