Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/devx-314-cli-onboarding.md
Original file line number Diff line number Diff line change
@@ -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
138 changes: 134 additions & 4 deletions src/cli/commands/auth.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -7,9 +8,25 @@ 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
// ---------------------------------------------------------------------------
Expand All @@ -31,6 +48,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" },
Expand Down Expand Up @@ -69,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 =
Expand All @@ -82,19 +117,114 @@ const authLogin = Command.make(
.filter((t) => t.length > 0),
)
: undefined;

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.catchAll((err) => {
onboardingError = err.message;
return Effect.succeed(null);
}),
);

// New user (PENDING) — collect agreement acceptance then complete onboarding
if (onboardingStatus?.status === "PENDING") {
// 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<void>((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);
}),
);

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",
{
authenticated: loginResult.success,
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,
);
Expand Down
1 change: 1 addition & 0 deletions src/core/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface AuthResult {
success: boolean;
accessToken?: string;
expiresAt?: Date;
onboardingPending?: boolean;
}

export interface AuthStatus {
Expand Down
15 changes: 15 additions & 0 deletions src/core/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Environment, string> = {
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
*/
Expand Down
Loading