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
9,278 changes: 4,155 additions & 5,123 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions src/commands/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ export default (cmd: Command): Command => {
.command('login')
.description(t('cmdAuthLoginDescription'))
.option('-e, --email', 'Login via email verification (no browser required)')
.option('--auth-url <url>', 'Override auth service URL (e.g., http://localhost:3001)')
.option(
'--auth-url <url>',
'Override auth service URL (e.g., http://localhost:3001)',
)
.action((options) => {
// If --email flag is provided, use email-based login (no browser needed)
if (options.email) {
// Use europlots ingress URL which has valid SSL (*.europlots.com wildcard cert)
// Custom domain auth.alternatefutures.ai has cert mismatch with Akash provider
const authApiUrl = options.authUrl || getDefined('AUTH__API_URL') || 'https://ubsm31q4ol97b1pi5l06iognug.ingress.europlots.com';
const authApiUrl =
options.authUrl ||
getDefined('AUTH__API_URL') ||
'https://ubsm31q4ol97b1pi5l06iognug.ingress.europlots.com';
return emailLoginActionHandler({ authApiUrl });
}

Expand All @@ -41,7 +47,8 @@ export default (cmd: Command): Command => {
.command('signup')
.description('Create a new account using email verification')
.action(() => {
const authApiUrl = getDefined('AUTH__API_URL') || 'https://auth.alternatefutures.ai';
const authApiUrl =
getDefined('AUTH__API_URL') || 'https://auth.alternatefutures.ai';

return signupActionHandler({
authApiUrl,
Expand Down
38 changes: 28 additions & 10 deletions src/commands/auth/loginEmail.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as readline from 'readline';
import * as readline from 'node:readline';
import { output } from '../../cli';
import { config } from '../../config';

Expand Down Expand Up @@ -30,15 +30,20 @@ const createPrompt = (): readline.Interface => {
});
};

const askQuestion = (rl: readline.Interface, question: string): Promise<string> => {
const askQuestion = (
rl: readline.Interface,
question: string,
): Promise<string> => {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer.trim());
});
});
};

export const emailLoginActionHandler = async ({ authApiUrl }: EmailLoginActionHandlerArgs) => {
export const emailLoginActionHandler = async ({
authApiUrl,
}: EmailLoginActionHandlerArgs) => {
const rl = createPrompt();

try {
Expand Down Expand Up @@ -78,11 +83,16 @@ export const emailLoginActionHandler = async ({ authApiUrl }: EmailLoginActionHa

output.success('Verification code sent to your email');
output.log('');
output.log(`📬 Check your inbox for a 6-digit code (expires in ${Math.floor((requestData.expiresIn || 300) / 60)} minutes)`);
output.log(
`📬 Check your inbox for a 6-digit code (expires in ${Math.floor((requestData.expiresIn || 300) / 60)} minutes)`,
);
output.log('');

// Step 3: Get verification code
const code = await askQuestion(rl, '🔐 Enter the 6-digit verification code: ');
const code = await askQuestion(
rl,
'🔐 Enter the 6-digit verification code: ',
);

if (!code || code.length !== 6) {
output.error('Invalid verification code. Please enter a 6-digit code.');
Expand Down Expand Up @@ -117,7 +127,7 @@ export const emailLoginActionHandler = async ({ authApiUrl }: EmailLoginActionHa
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
name: `CLI Token - ${new Date().toISOString().split('T')[0]}`,
Expand All @@ -143,7 +153,9 @@ export const emailLoginActionHandler = async ({ authApiUrl }: EmailLoginActionHa
output.log('');
output.log(`✅ Logged in as: ${email}`);
output.log('');
output.log('You can now use the AlternateFutures CLI to deploy your projects.');
output.log(
'You can now use the AlternateFutures CLI to deploy your projects.',
);
output.printNewLine();

rl.close();
Expand All @@ -163,14 +175,20 @@ export const emailLoginActionHandler = async ({ authApiUrl }: EmailLoginActionHa
output.log('');

if (fetchError.cause) {
output.log(`Technical details: ${fetchError.cause.message || fetchError.cause}`);
output.log(
`Technical details: ${fetchError.cause.message || fetchError.cause}`,
);
}

output.log('');
output.log('Troubleshooting:');
output.log(` 1. Verify the auth service is running: curl -k ${authApiUrl}/health`);
output.log(
` 1. Verify the auth service is running: curl -k ${authApiUrl}/health`,
);
output.log(' 2. Check your network connection');
output.log(' 3. Try setting NODE_TLS_REJECT_UNAUTHORIZED=0 if using self-signed certs');
output.log(
' 3. Try setting NODE_TLS_REJECT_UNAUTHORIZED=0 if using self-signed certs',
);
} else {
output.error(error.message);
if (fetchError.cause) {
Expand Down
38 changes: 28 additions & 10 deletions src/commands/auth/signup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as readline from 'readline';
import * as readline from 'node:readline';
import { output } from '../../cli';
import { config } from '../../config';

Expand Down Expand Up @@ -30,15 +30,20 @@ const createPrompt = (): readline.Interface => {
});
};

const askQuestion = (rl: readline.Interface, question: string): Promise<string> => {
const askQuestion = (
rl: readline.Interface,
question: string,
): Promise<string> => {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer.trim());
});
});
};

export const signupActionHandler = async ({ authApiUrl }: SignupActionHandlerArgs) => {
export const signupActionHandler = async ({
authApiUrl,
}: SignupActionHandlerArgs) => {
const rl = createPrompt();

try {
Expand Down Expand Up @@ -76,11 +81,16 @@ export const signupActionHandler = async ({ authApiUrl }: SignupActionHandlerArg

output.success('Verification code sent to your email');
output.log('');
output.log(`📬 Check your inbox for a 6-digit code (expires in ${Math.floor((requestData.expiresIn || 300) / 60)} minutes)`);
output.log(
`📬 Check your inbox for a 6-digit code (expires in ${Math.floor((requestData.expiresIn || 300) / 60)} minutes)`,
);
output.log('');

// Step 3: Get verification code
const code = await askQuestion(rl, '🔐 Enter the 6-digit verification code: ');
const code = await askQuestion(
rl,
'🔐 Enter the 6-digit verification code: ',
);

if (!code || code.length !== 6) {
output.error('Invalid verification code. Please enter a 6-digit code.');
Expand Down Expand Up @@ -115,7 +125,7 @@ export const signupActionHandler = async ({ authApiUrl }: SignupActionHandlerArg
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
name: `CLI Token - ${new Date().toISOString().split('T')[0]}`,
Expand All @@ -141,7 +151,9 @@ export const signupActionHandler = async ({ authApiUrl }: SignupActionHandlerArg
output.log('');
output.log(`✅ Logged in as: ${email}`);
output.log('');
output.log('You can now use the AlternateFutures CLI to deploy your projects.');
output.log(
'You can now use the AlternateFutures CLI to deploy your projects.',
);
output.printNewLine();

rl.close();
Expand All @@ -161,14 +173,20 @@ export const signupActionHandler = async ({ authApiUrl }: SignupActionHandlerArg
output.log('');

if (fetchError.cause) {
output.log(`Technical details: ${fetchError.cause.message || fetchError.cause}`);
output.log(
`Technical details: ${fetchError.cause.message || fetchError.cause}`,
);
}

output.log('');
output.log('Troubleshooting:');
output.log(` 1. Verify the auth service is running: curl -k ${authApiUrl}/health`);
output.log(
` 1. Verify the auth service is running: curl -k ${authApiUrl}/health`,
);
output.log(' 2. Check your network connection');
output.log(' 3. Try setting NODE_TLS_REJECT_UNAUTHORIZED=0 if using self-signed certs');
output.log(
' 3. Try setting NODE_TLS_REJECT_UNAUTHORIZED=0 if using self-signed certs',
);
} else {
output.error(error.message);
if (fetchError.cause) {
Expand Down
7 changes: 4 additions & 3 deletions src/commands/billing/invoices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ const invoicesAction: SdkGuardedFunction<InvoicesOptions> = async ({
'Paid At': invoice.paidAt
? new Date(invoice.paidAt * 1000).toLocaleDateString()
: 'N/A',
Period: invoice.periodStart && invoice.periodEnd
? `${new Date(invoice.periodStart * 1000).toLocaleDateString()} - ${new Date(invoice.periodEnd * 1000).toLocaleDateString()}`
: 'N/A',
Period:
invoice.periodStart && invoice.periodEnd
? `${new Date(invoice.periodStart * 1000).toLocaleDateString()} - ${new Date(invoice.periodEnd * 1000).toLocaleDateString()}`
: 'N/A',
PDF: invoice.pdfUrl || 'N/A',
}));

Expand Down
9 changes: 7 additions & 2 deletions src/commands/billing/utils/getBillingClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ export const getBillingClient = (sdk: AnySdk): AnyBillingClient | null => {
try {
return sdk.billing();
} catch (error) {
if (error instanceof Error && error.message.includes('SDK__AUTH_SERVICE_URL')) {
if (
error instanceof Error &&
error.message.includes('SDK__AUTH_SERVICE_URL')
) {
output.error('Billing requires SDK__AUTH_SERVICE_URL to be configured.');
output.hint('Add SDK__AUTH_SERVICE_URL to your environment configuration.');
output.hint(
'Add SDK__AUTH_SERVICE_URL to your environment configuration.',
);
return null;
}
throw error;
Expand Down
9 changes: 7 additions & 2 deletions src/commands/observability/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export default (program: Command): Command => {
const cmd = program
.command('observability')
.alias('obs')
.description('Query and manage APM observability data (traces, logs, metrics)');
.description(
'Query and manage APM observability data (traces, logs, metrics)',
);

cmd
.command('traces')
Expand Down Expand Up @@ -39,7 +41,10 @@ export default (program: Command): Command => {
.command('logs')
.description('Query logs')
.option('--service <name>', 'Filter by service name')
.option('--severity <level>', 'Filter by severity (DEBUG, INFO, WARN, ERROR)')
.option(
'--severity <level>',
'Filter by severity (DEBUG, INFO, WARN, ERROR)',
)
.option('--search <text>', 'Search in log body')
.option('--hours <number>', 'Look back N hours (default: 1)', '1')
.option('--limit <number>', 'Maximum number of logs to return', '50')
Expand Down
8 changes: 5 additions & 3 deletions src/commands/observability/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ const logsAction: SdkGuardedFunction<LogsActionArgs> = async ({
const projectId = config.projectId.get();

if (!projectId) {
output.error('No project selected. Use `af projects switch` to select a project.');
output.error(
'No project selected. Use `af projects switch` to select a project.',
);
return;
}

Expand All @@ -45,15 +47,15 @@ const logsAction: SdkGuardedFunction<LogsActionArgs> = async ({

try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const logs = await (sdk as any).observability().queryLogs({
const logs = (await (sdk as any).observability().queryLogs({
projectId,
startTime,
endTime,
serviceName: args.service,
severityText: args.severity,
bodyContains: args.search,
limit,
}) as LogEntry[];
})) as LogEntry[];

output.stopSpinner();

Expand Down
31 changes: 23 additions & 8 deletions src/commands/observability/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ const servicesAction: SdkGuardedFunction<ServicesActionArgs> = async ({
const projectId = config.projectId.get();

if (!projectId) {
output.error('No project selected. Use `af projects switch` to select a project.');
output.error(
'No project selected. Use `af projects switch` to select a project.',
);
return;
}

Expand All @@ -34,20 +36,24 @@ const servicesAction: SdkGuardedFunction<ServicesActionArgs> = async ({
const endTime = new Date();
const startTime = new Date(endTime.getTime() - hours * 60 * 60 * 1000);

output.spinner(`Fetching service statistics for the last ${hours} hour(s)...`);
output.spinner(
`Fetching service statistics for the last ${hours} hour(s)...`,
);

try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const services = await (sdk as any)
const services = (await (sdk as any)
.observability()
.getServices(projectId, startTime, endTime) as ServiceStats[];
.getServices(projectId, startTime, endTime)) as ServiceStats[];

output.stopSpinner();

if (!services || services.length === 0) {
output.warn('No services found with telemetry data');
output.printNewLine();
output.log('Make sure your application is sending traces to AlternateFutures.');
output.log(
'Make sure your application is sending traces to AlternateFutures.',
);
output.log('See: https://docs.alternatefutures.ai/observability/setup');
return;
}
Expand All @@ -72,9 +78,18 @@ const servicesAction: SdkGuardedFunction<ServicesActionArgs> = async ({
output.printNewLine();

// Calculate totals
const totalSpans = services.reduce((sum: number, s: ServiceStats) => sum + s.spanCount, 0);
const totalErrors = services.reduce((sum: number, s: ServiceStats) => sum + s.errorCount, 0);
const totalTraces = services.reduce((sum: number, s: ServiceStats) => sum + s.traceCount, 0);
const totalSpans = services.reduce(
(sum: number, s: ServiceStats) => sum + s.spanCount,
0,
);
const totalErrors = services.reduce(
(sum: number, s: ServiceStats) => sum + s.errorCount,
0,
);
const totalTraces = services.reduce(
(sum: number, s: ServiceStats) => sum + s.traceCount,
0,
);

output.log('Totals:');
output.log(` Services: ${services.length}`);
Expand Down
Loading