From 7b5d222b61d4cd700f1054f74e3b8923f5b44164 Mon Sep 17 00:00:00 2001 From: "Sanket M." Date: Tue, 16 Jun 2026 21:06:17 +0530 Subject: [PATCH 1/4] feat(auth): enhance onboarding process and add agreement prompts - Introduced onboarding status checks and completion via new API calls. - Added agreement URLs for terms of service, privacy policy, and developer agreement. - Updated authentication result to include onboarding status. - Enhanced CLI interaction to prompt users for agreement acceptance before proceeding with authentication. --- src/cli/commands/auth.ts | 113 +++++++++++++++++++++++- src/core/auth.ts | 1 + src/core/environment.ts | 15 ++++ src/core/onboarding.ts | 181 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 src/core/onboarding.ts diff --git a/src/cli/commands/auth.ts b/src/cli/commands/auth.ts index 7eb53fc..746b935 100644 --- a/src/cli/commands/auth.ts +++ b/src/cli/commands/auth.ts @@ -1,3 +1,4 @@ +import readline from "node:readline"; import * as Command from "@effect/cli/Command"; import * as Options from "@effect/cli/Options"; import * as Effect from "effect/Effect"; @@ -7,9 +8,23 @@ import { authStatusEffect, } from "../../core/auth"; import { envGetEffect } from "../../core/environment"; +import { + checkOnboardingStatusEffect, + completeOnboardingEffect, +} from "../../core/onboarding"; import type { NextAction } from "../agent/types"; import { EnvelopeWriter } from "../services/envelope-writer"; +// --------------------------------------------------------------------------- +// Agreement URLs +// --------------------------------------------------------------------------- + +const AGREEMENT_URLS = { + tos: "https://developer.commerce.godaddy.com/legal/agreements/terms-of-use", + privacy: "https://developer.commerce.godaddy.com/legal/agreements/privacy-policy", + developer: "https://developer.commerce.godaddy.com/legal/agreements/developer-agreement", +}; + // --------------------------------------------------------------------------- // Colocated next_actions // --------------------------------------------------------------------------- @@ -31,6 +46,18 @@ const authLoginActions: NextAction[] = [ { command: "godaddy auth logout", description: "Logout" }, ]; +const authLoginOnboardingActions: NextAction[] = [ + { + command: "godaddy application init", + description: "Create your first application", + }, + { + command: "godaddy auth status", + description: "Verify current authentication status", + }, + { command: "godaddy auth logout", description: "Logout" }, +]; + const authLogoutActions: NextAction[] = [ { command: "godaddy auth login", description: "Authenticate again" }, { command: "godaddy auth status", description: "Check auth status" }, @@ -82,11 +109,86 @@ const authLogin = Command.make( .filter((t) => t.length > 0), ) : undefined; + + // Show agreement links before SSO — skip in non-interactive/CI environments + if (process.stdin.isTTY) { + yield* Effect.promise( + () => + new Promise((resolve) => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + const prompt = [ + "", + "By continuing, you agree to the GoDaddy Developer terms:", + "", + ` Terms of Service: ${AGREEMENT_URLS.tos}`, + ` Privacy Policy: ${AGREEMENT_URLS.privacy}`, + ` Developer Agreement: ${AGREEMENT_URLS.developer}`, + "", + "Press Enter to accept and continue...", + ].join("\n"); + rl.question(prompt, () => { + rl.close(); + resolve(); + }); + rl.on("error", () => { + rl.close(); + resolve(); + }); + }), + ); + } + const loginResult = yield* authLoginEffect({ additionalScopes }); - const environment = yield* envGetEffect().pipe( - Effect.map(String), - Effect.orElseSucceed(() => "unknown"), + const env = yield* envGetEffect().pipe( + Effect.orElseSucceed(() => "ote" as const), ); + const environment = String(env); + + // Check onboarding status — non-fatal if the call fails + let onboardingError: string | undefined; + const onboardingStatus = yield* checkOnboardingStatusEffect().pipe( + Effect.tap((status) => + Effect.sync(() => + console.error("[DEBUG] onboarding status response:", JSON.stringify(status)), + ), + ), + Effect.catchAll((err) => { + console.error("[DEBUG] onboarding status error:", err.message); + onboardingError = err.message; + return Effect.succeed(null); + }), + ); + + // New user (PENDING) — complete onboarding via single API call + if (onboardingStatus?.status === "PENDING") { + let onboardingResult: { organizationId: string } | null = null; + onboardingResult = yield* completeOnboardingEffect().pipe( + Effect.catchAll((err) => { + onboardingError = err.message; + return Effect.succeed(null); + }), + ); + + yield* writer.emitSuccess( + "godaddy auth login", + { + authenticated: loginResult.success, + environment, + expires_at: loginResult.expiresAt?.toISOString(), + scopes_requested: additionalScopes, + onboarding: onboardingResult ? "complete" : "failed", + org_id: onboardingResult?.organizationId, + ...(onboardingError + ? { note: `Onboarding error: ${onboardingError}` } + : {}), + }, + authLoginOnboardingActions, + ); + return; + } yield* writer.emitSuccess( "godaddy auth login", @@ -95,6 +197,11 @@ const authLogin = Command.make( environment, expires_at: loginResult.expiresAt?.toISOString(), scopes_requested: additionalScopes, + onboarding: onboardingStatus?.status === "ACTIVE" ? "complete" : undefined, + org_id: onboardingStatus?.orgId, + ...(onboardingStatus === null + ? { note: `Could not verify onboarding status: ${onboardingError}` } + : {}), }, authLoginActions, ); diff --git a/src/core/auth.ts b/src/core/auth.ts index 43de795..a2377c7 100644 --- a/src/core/auth.ts +++ b/src/core/auth.ts @@ -44,6 +44,7 @@ export interface AuthResult { success: boolean; accessToken?: string; expiresAt?: Date; + onboardingPending?: boolean; } export interface AuthStatus { diff --git a/src/core/environment.ts b/src/core/environment.ts index 6975bab..eea59d9 100644 --- a/src/core/environment.ts +++ b/src/core/environment.ts @@ -268,6 +268,21 @@ export function getClientId(env: Environment): string { return clientIds[env]; } +/** + * Get the devx-core API base URL for the given environment. + * Can be overridden with DEVX_CORE_URL environment variable. + */ +export function getDevxCoreUrl(env: Environment): string { + if (process.env.DEVX_CORE_URL) return process.env.DEVX_CORE_URL; + + const urls: Record = { + ote: "https://api.developer.commerce.ote-godaddy.com", + prod: "https://api.developer.commerce.godaddy.com", + }; + + return urls[env]; +} + /** * Check if an action requires confirmation in the current environment */ diff --git a/src/core/onboarding.ts b/src/core/onboarding.ts new file mode 100644 index 0000000..c5fcfb8 --- /dev/null +++ b/src/core/onboarding.ts @@ -0,0 +1,181 @@ +import { FileSystem } from "@effect/platform/FileSystem"; +import * as Effect from "effect/Effect"; +import { AuthenticationError, ConfigurationError } from "../effect/errors"; +import type { Keychain } from "../effect/services/keychain"; +import { getTokenInfoEffect } from "./auth"; +import { envGetEffect, getDevxCoreUrl } from "./environment"; + +export interface OnboardingStatus { + orgId: string; + status: string; +} + +/** + * Check onboarding status for the authenticated user via devx-core. + * Auto-creates a PENDING org if none exists yet. + */ +export function checkOnboardingStatusEffect(): Effect.Effect< + OnboardingStatus, + ConfigurationError | AuthenticationError, + FileSystem | Keychain +> { + return Effect.gen(function* () { + const tokenInfo = yield* getTokenInfoEffect().pipe( + Effect.mapError( + (err) => + new ConfigurationError({ + message: `Failed to get token: ${err.message}`, + userMessage: "Could not check onboarding status.", + }), + ), + ); + + if (!tokenInfo) { + return yield* Effect.fail( + new AuthenticationError({ + message: "No token available for onboarding status check", + userMessage: "Not authenticated.", + }), + ); + } + + const env = yield* envGetEffect().pipe( + Effect.orElseSucceed(() => "ote" as const), + ); + const baseUrl = getDevxCoreUrl(env); + + console.error("[DEBUG] onboarding/status url:", `${baseUrl}/api/v1/onboarding/status`); + const response = yield* Effect.tryPromise({ + try: () => + fetch(`${baseUrl}/api/v1/onboarding/status`, { + method: "POST", + headers: { + Authorization: `Bearer ${tokenInfo.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }).then(async (res) => { + console.error("[DEBUG] onboarding/status HTTP status:", res.status); + if (res.status === 401) { + throw new AuthenticationError({ + message: "Onboarding status check: unauthorized (401)", + userMessage: "Session expired. Run 'godaddy auth login' again.", + }); + } + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new ConfigurationError({ + message: `Onboarding status check failed: HTTP ${res.status} ${body}`, + userMessage: "Could not check onboarding status.", + }); + } + return res.json(); + }), + catch: (err) => { + if (err instanceof AuthenticationError || err instanceof ConfigurationError) return err; + return new ConfigurationError({ + message: `Onboarding status check failed: ${err}`, + userMessage: "Could not check onboarding status.", + }); + }, + }); + + const envelope = response as { success?: boolean; data?: { id?: string; status?: string } }; + const data = envelope.data ?? (response as { id?: string; status?: string }); + if (!data?.id || !data?.status) { + return yield* Effect.fail( + new ConfigurationError({ + message: "Unexpected onboarding status response shape", + userMessage: "Could not check onboarding status.", + }), + ); + } + + return { orgId: data.id, status: data.status }; + }); +} + +/** + * Complete CLI onboarding in one call — get/create org, accept all agreements, submit. + * Returns the organizationId and whether the org was already active. + */ +export function completeOnboardingEffect(): Effect.Effect< + { organizationId: string; alreadyActive: boolean }, + AuthenticationError | ConfigurationError, + FileSystem | Keychain +> { + return Effect.gen(function* () { + const tokenInfo = yield* getTokenInfoEffect().pipe( + Effect.mapError( + (err) => + new ConfigurationError({ + message: `Failed to get token: ${err.message}`, + userMessage: "Could not complete onboarding.", + }), + ), + ); + + if (!tokenInfo) { + return yield* Effect.fail( + new AuthenticationError({ + message: "No token available for onboarding", + userMessage: "Not authenticated.", + }), + ); + } + + const env = yield* envGetEffect().pipe( + Effect.orElseSucceed(() => "ote" as const), + ); + const baseUrl = getDevxCoreUrl(env); + + const response = yield* Effect.tryPromise({ + try: () => + fetch(`${baseUrl}/api/v1/onboarding/cli`, { + method: "POST", + headers: { + Authorization: `Bearer ${tokenInfo.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }).then(async (res) => { + if (res.status === 401) { + throw new AuthenticationError({ + message: "CLI onboarding: unauthorized (401)", + userMessage: "Session expired. Run 'godaddy auth login' again.", + }); + } + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new ConfigurationError({ + message: `CLI onboarding failed: HTTP ${res.status} ${body}`, + userMessage: "Could not complete onboarding.", + }); + } + return res.json(); + }), + catch: (err) => { + if (err instanceof AuthenticationError || err instanceof ConfigurationError) return err; + return new ConfigurationError({ + message: `CLI onboarding failed: ${err}`, + userMessage: "Could not complete onboarding.", + }); + }, + }); + + const data = (response as { data?: { organizationId?: string; status?: string } }).data; + if (!data?.organizationId) { + return yield* Effect.fail( + new ConfigurationError({ + message: "Unexpected CLI onboarding response", + userMessage: "Could not complete onboarding.", + }), + ); + } + + return { + organizationId: data.organizationId, + alreadyActive: data.status === "ACTIVE", + }; + }); +} From 34b5eaa4fd62e890b590dc8c66251b94374d0478 Mon Sep 17 00:00:00 2001 From: "Sanket M." Date: Fri, 19 Jun 2026 10:46:55 +0530 Subject: [PATCH 2/4] refactor(auth, onboarding): clean up code formatting and improve error handling - Reformatted code for better readability, including consistent indentation and spacing. - Enhanced error handling in onboarding status checks to ensure clearer messaging. - Updated import statements to use type imports for better clarity and performance. --- src/cli/commands/auth.ts | 15 +- src/core/onboarding.ts | 306 ++++++++++++++++++++------------------- 2 files changed, 165 insertions(+), 156 deletions(-) diff --git a/src/cli/commands/auth.ts b/src/cli/commands/auth.ts index 746b935..bd19687 100644 --- a/src/cli/commands/auth.ts +++ b/src/cli/commands/auth.ts @@ -21,8 +21,10 @@ import { EnvelopeWriter } from "../services/envelope-writer"; const AGREEMENT_URLS = { tos: "https://developer.commerce.godaddy.com/legal/agreements/terms-of-use", - privacy: "https://developer.commerce.godaddy.com/legal/agreements/privacy-policy", - developer: "https://developer.commerce.godaddy.com/legal/agreements/developer-agreement", + privacy: + "https://developer.commerce.godaddy.com/legal/agreements/privacy-policy", + developer: + "https://developer.commerce.godaddy.com/legal/agreements/developer-agreement", }; // --------------------------------------------------------------------------- @@ -150,13 +152,7 @@ const authLogin = Command.make( // Check onboarding status — non-fatal if the call fails let onboardingError: string | undefined; const onboardingStatus = yield* checkOnboardingStatusEffect().pipe( - Effect.tap((status) => - Effect.sync(() => - console.error("[DEBUG] onboarding status response:", JSON.stringify(status)), - ), - ), Effect.catchAll((err) => { - console.error("[DEBUG] onboarding status error:", err.message); onboardingError = err.message; return Effect.succeed(null); }), @@ -197,7 +193,8 @@ const authLogin = Command.make( environment, expires_at: loginResult.expiresAt?.toISOString(), scopes_requested: additionalScopes, - onboarding: onboardingStatus?.status === "ACTIVE" ? "complete" : undefined, + onboarding: + onboardingStatus?.status === "ACTIVE" ? "complete" : undefined, org_id: onboardingStatus?.orgId, ...(onboardingStatus === null ? { note: `Could not verify onboarding status: ${onboardingError}` } diff --git a/src/core/onboarding.ts b/src/core/onboarding.ts index c5fcfb8..539708a 100644 --- a/src/core/onboarding.ts +++ b/src/core/onboarding.ts @@ -1,4 +1,4 @@ -import { FileSystem } from "@effect/platform/FileSystem"; +import type { FileSystem } from "@effect/platform/FileSystem"; import * as Effect from "effect/Effect"; import { AuthenticationError, ConfigurationError } from "../effect/errors"; import type { Keychain } from "../effect/services/keychain"; @@ -6,8 +6,8 @@ import { getTokenInfoEffect } from "./auth"; import { envGetEffect, getDevxCoreUrl } from "./environment"; export interface OnboardingStatus { - orgId: string; - status: string; + orgId: string; + status: string; } /** @@ -15,84 +15,90 @@ export interface OnboardingStatus { * Auto-creates a PENDING org if none exists yet. */ export function checkOnboardingStatusEffect(): Effect.Effect< - OnboardingStatus, - ConfigurationError | AuthenticationError, - FileSystem | Keychain + OnboardingStatus, + ConfigurationError | AuthenticationError, + FileSystem | Keychain > { - return Effect.gen(function* () { - const tokenInfo = yield* getTokenInfoEffect().pipe( - Effect.mapError( - (err) => - new ConfigurationError({ - message: `Failed to get token: ${err.message}`, - userMessage: "Could not check onboarding status.", - }), - ), - ); + return Effect.gen(function* () { + const tokenInfo = yield* getTokenInfoEffect().pipe( + Effect.mapError( + (err) => + new ConfigurationError({ + message: `Failed to get token: ${err.message}`, + userMessage: "Could not check onboarding status.", + }), + ), + ); - if (!tokenInfo) { - return yield* Effect.fail( - new AuthenticationError({ - message: "No token available for onboarding status check", - userMessage: "Not authenticated.", - }), - ); - } + if (!tokenInfo) { + return yield* Effect.fail( + new AuthenticationError({ + message: "No token available for onboarding status check", + userMessage: "Not authenticated.", + }), + ); + } - const env = yield* envGetEffect().pipe( - Effect.orElseSucceed(() => "ote" as const), - ); - const baseUrl = getDevxCoreUrl(env); + const env = yield* envGetEffect().pipe( + Effect.orElseSucceed(() => "ote" as const), + ); + const baseUrl = getDevxCoreUrl(env); - console.error("[DEBUG] onboarding/status url:", `${baseUrl}/api/v1/onboarding/status`); - const response = yield* Effect.tryPromise({ - try: () => - fetch(`${baseUrl}/api/v1/onboarding/status`, { - method: "POST", - headers: { - Authorization: `Bearer ${tokenInfo.accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - }).then(async (res) => { - console.error("[DEBUG] onboarding/status HTTP status:", res.status); - if (res.status === 401) { - throw new AuthenticationError({ - message: "Onboarding status check: unauthorized (401)", - userMessage: "Session expired. Run 'godaddy auth login' again.", - }); - } - if (!res.ok) { - const body = await res.text().catch(() => ""); - throw new ConfigurationError({ - message: `Onboarding status check failed: HTTP ${res.status} ${body}`, - userMessage: "Could not check onboarding status.", - }); - } - return res.json(); - }), - catch: (err) => { - if (err instanceof AuthenticationError || err instanceof ConfigurationError) return err; - return new ConfigurationError({ - message: `Onboarding status check failed: ${err}`, - userMessage: "Could not check onboarding status.", - }); - }, - }); + const response = yield* Effect.tryPromise({ + try: () => + fetch(`${baseUrl}/api/v1/onboarding/status`, { + method: "POST", + headers: { + Authorization: `Bearer ${tokenInfo.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }).then(async (res) => { + if (res.status === 401) { + throw new AuthenticationError({ + message: "Onboarding status check: unauthorized (401)", + userMessage: "Session expired. Run 'godaddy auth login' again.", + }); + } + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new ConfigurationError({ + message: `Onboarding status check failed: HTTP ${res.status} ${body}`, + userMessage: "Could not check onboarding status.", + }); + } + return res.json(); + }), + catch: (err) => { + if ( + err instanceof AuthenticationError || + err instanceof ConfigurationError + ) + return err; + return new ConfigurationError({ + message: `Onboarding status check failed: ${err}`, + userMessage: "Could not check onboarding status.", + }); + }, + }); - const envelope = response as { success?: boolean; data?: { id?: string; status?: string } }; - const data = envelope.data ?? (response as { id?: string; status?: string }); - if (!data?.id || !data?.status) { - return yield* Effect.fail( - new ConfigurationError({ - message: "Unexpected onboarding status response shape", - userMessage: "Could not check onboarding status.", - }), - ); - } + const envelope = response as { + success?: boolean; + data?: { id?: string; status?: string }; + }; + const data = + envelope.data ?? (response as { id?: string; status?: string }); + if (!data?.id || !data?.status) { + return yield* Effect.fail( + new ConfigurationError({ + message: "Unexpected onboarding status response shape", + userMessage: "Could not check onboarding status.", + }), + ); + } - return { orgId: data.id, status: data.status }; - }); + return { orgId: data.id, status: data.status }; + }); } /** @@ -100,82 +106,88 @@ export function checkOnboardingStatusEffect(): Effect.Effect< * Returns the organizationId and whether the org was already active. */ export function completeOnboardingEffect(): Effect.Effect< - { organizationId: string; alreadyActive: boolean }, - AuthenticationError | ConfigurationError, - FileSystem | Keychain + { organizationId: string; alreadyActive: boolean }, + AuthenticationError | ConfigurationError, + FileSystem | Keychain > { - return Effect.gen(function* () { - const tokenInfo = yield* getTokenInfoEffect().pipe( - Effect.mapError( - (err) => - new ConfigurationError({ - message: `Failed to get token: ${err.message}`, - userMessage: "Could not complete onboarding.", - }), - ), - ); + return Effect.gen(function* () { + const tokenInfo = yield* getTokenInfoEffect().pipe( + Effect.mapError( + (err) => + new ConfigurationError({ + message: `Failed to get token: ${err.message}`, + userMessage: "Could not complete onboarding.", + }), + ), + ); - if (!tokenInfo) { - return yield* Effect.fail( - new AuthenticationError({ - message: "No token available for onboarding", - userMessage: "Not authenticated.", - }), - ); - } + if (!tokenInfo) { + return yield* Effect.fail( + new AuthenticationError({ + message: "No token available for onboarding", + userMessage: "Not authenticated.", + }), + ); + } - const env = yield* envGetEffect().pipe( - Effect.orElseSucceed(() => "ote" as const), - ); - const baseUrl = getDevxCoreUrl(env); + const env = yield* envGetEffect().pipe( + Effect.orElseSucceed(() => "ote" as const), + ); + const baseUrl = getDevxCoreUrl(env); - const response = yield* Effect.tryPromise({ - try: () => - fetch(`${baseUrl}/api/v1/onboarding/cli`, { - method: "POST", - headers: { - Authorization: `Bearer ${tokenInfo.accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - }).then(async (res) => { - if (res.status === 401) { - throw new AuthenticationError({ - message: "CLI onboarding: unauthorized (401)", - userMessage: "Session expired. Run 'godaddy auth login' again.", - }); - } - if (!res.ok) { - const body = await res.text().catch(() => ""); - throw new ConfigurationError({ - message: `CLI onboarding failed: HTTP ${res.status} ${body}`, - userMessage: "Could not complete onboarding.", - }); - } - return res.json(); - }), - catch: (err) => { - if (err instanceof AuthenticationError || err instanceof ConfigurationError) return err; - return new ConfigurationError({ - message: `CLI onboarding failed: ${err}`, - userMessage: "Could not complete onboarding.", - }); - }, - }); + const response = yield* Effect.tryPromise({ + try: () => + fetch(`${baseUrl}/api/v1/onboarding/cli`, { + method: "POST", + headers: { + Authorization: `Bearer ${tokenInfo.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }).then(async (res) => { + if (res.status === 401) { + throw new AuthenticationError({ + message: "CLI onboarding: unauthorized (401)", + userMessage: "Session expired. Run 'godaddy auth login' again.", + }); + } + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new ConfigurationError({ + message: `CLI onboarding failed: HTTP ${res.status} ${body}`, + userMessage: "Could not complete onboarding.", + }); + } + return res.json(); + }), + catch: (err) => { + if ( + err instanceof AuthenticationError || + err instanceof ConfigurationError + ) + return err; + return new ConfigurationError({ + message: `CLI onboarding failed: ${err}`, + userMessage: "Could not complete onboarding.", + }); + }, + }); - const data = (response as { data?: { organizationId?: string; status?: string } }).data; - if (!data?.organizationId) { - return yield* Effect.fail( - new ConfigurationError({ - message: "Unexpected CLI onboarding response", - userMessage: "Could not complete onboarding.", - }), - ); - } + const data = ( + response as { data?: { organizationId?: string; status?: string } } + ).data; + if (!data?.organizationId) { + return yield* Effect.fail( + new ConfigurationError({ + message: "Unexpected CLI onboarding response", + userMessage: "Could not complete onboarding.", + }), + ); + } - return { - organizationId: data.organizationId, - alreadyActive: data.status === "ACTIVE", - }; - }); + return { + organizationId: data.organizationId, + alreadyActive: data.status === "ACTIVE", + }; + }); } From 0dc35d4873d58961e65903c1baa32289a9f83c4d Mon Sep 17 00:00:00 2001 From: "Sanket M." Date: Thu, 25 Jun 2026 16:29:32 +0530 Subject: [PATCH 3/4] fix(auth): move agreement prompt inside PENDING branch, add --accept-agreements flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Agreement prompt now only shown to new users (PENDING onboarding) — existing ACTIVE users no longer see it on every login - Prompt output moved to stderr to keep stdout envelope-only - Added --accept-agreements flag for non-interactive/CI acceptance - Non-TTY runs without the flag fail with a structured AGREEMENTS_REQUIRED error Co-Authored-By: Claude Sonnet 4.6 --- src/cli/commands/auth.ts | 96 +++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 35 deletions(-) diff --git a/src/cli/commands/auth.ts b/src/cli/commands/auth.ts index bd19687..e4d8fcc 100644 --- a/src/cli/commands/auth.ts +++ b/src/cli/commands/auth.ts @@ -98,8 +98,14 @@ const authLogin = Command.make( ), Options.repeated, ), + acceptAgreements: Options.boolean("accept-agreements").pipe( + Options.withDescription( + "Accept GoDaddy Developer agreements non-interactively (required for CI/scripts)", + ), + Options.withDefault(false), + ), }, - ({ scope }) => + ({ scope, acceptAgreements }) => Effect.gen(function* () { const writer = yield* EnvelopeWriter; const additionalScopes = @@ -112,37 +118,6 @@ const authLogin = Command.make( ) : undefined; - // Show agreement links before SSO — skip in non-interactive/CI environments - if (process.stdin.isTTY) { - yield* Effect.promise( - () => - new Promise((resolve) => { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - const prompt = [ - "", - "By continuing, you agree to the GoDaddy Developer terms:", - "", - ` Terms of Service: ${AGREEMENT_URLS.tos}`, - ` Privacy Policy: ${AGREEMENT_URLS.privacy}`, - ` Developer Agreement: ${AGREEMENT_URLS.developer}`, - "", - "Press Enter to accept and continue...", - ].join("\n"); - rl.question(prompt, () => { - rl.close(); - resolve(); - }); - rl.on("error", () => { - rl.close(); - resolve(); - }); - }), - ); - } - const loginResult = yield* authLoginEffect({ additionalScopes }); const env = yield* envGetEffect().pipe( Effect.orElseSucceed(() => "ote" as const), @@ -158,10 +133,61 @@ const authLogin = Command.make( }), ); - // New user (PENDING) — complete onboarding via single API call + // New user (PENDING) — collect agreement acceptance then complete onboarding if (onboardingStatus?.status === "PENDING") { - let onboardingResult: { organizationId: string } | null = null; - onboardingResult = yield* completeOnboardingEffect().pipe( + // Non-interactive without explicit flag: refuse to accept agreements silently + if (!process.stdin.isTTY && !acceptAgreements) { + yield* writer.emitError( + "godaddy auth login", + { + code: "AGREEMENTS_REQUIRED", + message: + "Agreement acceptance is required to complete onboarding. Re-run interactively or pass --accept-agreements to confirm acceptance of the GoDaddy Developer terms.", + }, + "Run: godaddy auth login --accept-agreements", + [ + { + command: "godaddy auth login --accept-agreements", + description: + "Accept GoDaddy Developer agreements non-interactively", + }, + ], + ); + return; + } + + // TTY: show agreement prompt on stderr before recording acceptance + if (process.stdin.isTTY) { + yield* Effect.promise( + () => + new Promise((resolve) => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stderr, + }); + const prompt = [ + "", + "By continuing, you agree to the GoDaddy Developer terms:", + "", + ` Terms of Service: ${AGREEMENT_URLS.tos}`, + ` Privacy Policy: ${AGREEMENT_URLS.privacy}`, + ` Developer Agreement: ${AGREEMENT_URLS.developer}`, + "", + "Press Enter to accept and continue...", + ].join("\n"); + rl.question(prompt, () => { + rl.close(); + resolve(); + }); + rl.on("error", () => { + rl.close(); + resolve(); + }); + }), + ); + } + + const onboardingResult = yield* completeOnboardingEffect().pipe( Effect.catchAll((err) => { onboardingError = err.message; return Effect.succeed(null); From 691c34ce7bfad4d0544cb69e27a3ed7e8626c240 Mon Sep 17 00:00:00 2001 From: "Sanket M." Date: Thu, 2 Jul 2026 23:47:21 +0530 Subject: [PATCH 4/4] chore: add changeset for DEVX-314 cli onboarding Co-Authored-By: Claude Sonnet 4.6 --- .changeset/devx-314-cli-onboarding.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/devx-314-cli-onboarding.md diff --git a/.changeset/devx-314-cli-onboarding.md b/.changeset/devx-314-cli-onboarding.md new file mode 100644 index 0000000..77813a7 --- /dev/null +++ b/.changeset/devx-314-cli-onboarding.md @@ -0,0 +1,10 @@ +--- +"@godaddy/cli": minor +--- + +Add terminal-based agreement acceptance and automated onboarding to `auth login` + +- Agreement prompt (ToS, Privacy Policy, Developer Agreement) is shown on stderr before completing onboarding — only for new users whose org is PENDING, never for returning users +- Onboarding completes automatically after OAuth via a single `POST /api/v1/onboarding/cli` call; no browser redirect to the portal required +- Added `--accept-agreements` flag for non-interactive/CI use; without it, non-TTY runs with a PENDING org emit a structured `AGREEMENTS_REQUIRED` error (`ok: false`, exit 1) +- `org_id` and `onboarding` status are included in the login result envelope