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
22 changes: 18 additions & 4 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { error, info, warn } from './cli/messages';
import { loginSelection } from './cli/selection';
import { Config, save } from './config';
import { ErrorCode } from './deploy';
import { mockAPI, mockLogin, mockSignup, shouldUseMocks } from './mocks';
import { forever } from './utils';

const authToken = async (config: Config): Promise<string> => {
Expand All @@ -25,7 +26,9 @@ const authToken = async (config: Config): Promise<string> => {
const shouldKeepAsking = args['token'] === undefined;
let token: string = args['token'] || (await askToken());

const api = API(token, config.baseURL);
const api = shouldUseMocks()
? mockAPI(token, config.baseURL)
: API(token, config.baseURL);

if (process.stdout.isTTY && shouldKeepAsking) {
while (forever) {
Expand Down Expand Up @@ -76,10 +79,15 @@ const authLogin = async (config: Config): Promise<string> => {
// Now we got email and password let's call login api endpoint and get the token and store it int somewhere else
let token = '';

const loginFn = shouldUseMocks()
? (email: string, password: string) => mockLogin(email, password)
: (email: string, password: string) =>
login(email, password, config.baseURL);

if (process.stdout.isTTY && shouldKeepAsking) {
while (forever) {
try {
token = await login(email, password, config.baseURL);
token = await loginFn(email, password);
break;
} catch (err) {
warn(String((err as ProtocolError).data));
Expand All @@ -89,7 +97,7 @@ const authLogin = async (config: Config): Promise<string> => {
}
} else {
try {
token = await login(email, password, config.baseURL);
token = await loginFn(email, password);
} catch (err) {
error(String((err as ProtocolError).data));
}
Expand Down Expand Up @@ -141,9 +149,15 @@ const authSignup = async (config: Config): Promise<string> => {

let res: string;

const signupFn = shouldUseMocks()
? (email: string, password: string, alias: string) =>
mockSignup(email, password, alias)
: (email: string, password: string, alias: string) =>
signup(email, password, alias, config.baseURL);

while (forever) {
try {
res = await signup(email, password, userAlias, config.baseURL);
res = await signupFn(email, password, userAlias);
info(res);
info(
'Visit Metacall Hub directly to learn more about deployments and to purchase plans: https://metacall.io/pricing/'
Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { Plans } from '@metacall/protocol/plan';
import API, { API as APIInterface } from '@metacall/protocol/protocol';
import type { API as APIInterface } from '@metacall/protocol/protocol';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';
import args, { InspectFormat } from './cli/args';
Expand All @@ -20,6 +20,7 @@ import { deployFromRepository, deployPackage, ErrorCode } from './deploy';
import { force } from './force';
import { listPlans } from './listPlans';
import { logout } from './logout';
import { getAPI } from './mocks';
import { plan } from './plan';
import { startup } from './startup';

Expand Down Expand Up @@ -62,7 +63,7 @@ void (async () => {
if (args['serverUrl']) {
config.baseURL = args['serverUrl'];
}
const api: APIInterface = API(
const api: APIInterface = getAPI(
config.token as string,
args['dev'] ? config.devURL : config.baseURL
);
Expand Down
5 changes: 3 additions & 2 deletions src/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {
LogType
} from '@metacall/protocol/deployment';
import { RunnerToDisplayName } from '@metacall/protocol/language';
import API, { isProtocolError } from '@metacall/protocol/protocol';
import { isProtocolError } from '@metacall/protocol/protocol';
import args from './cli/args';
import { error, info } from './cli/messages';
import { listSelection } from './cli/selection';
import { getAPI } from './mocks';
import { startup } from './startup';
import { sleep } from './utils';

Expand All @@ -18,7 +19,7 @@ const showLogs = async (
dev: boolean
): Promise<void> => {
const config = await startup(args['confDir']);
const api = API(
const api = getAPI(
config.token as string,
dev ? config.devURL : config.baseURL
);
Expand Down
20 changes: 20 additions & 0 deletions src/mocks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { API as APIInterface } from '@metacall/protocol/protocol';
import API from '@metacall/protocol/protocol';
import mockAPI from './protocol';

export { default as mockLogin } from './login';
export { default as mockSignup } from './signup';

/** * Returns true if mocks should be used instead of real implementations */ export function shouldUseMocks(): boolean {
return process.env.TEST_DEPLOY_LOCAL === 'true';
}

/** * Returns the appropriate API instance (mock or real) based on the environment */ export function getAPI(
token: string,
baseURL: string
): APIInterface {
return shouldUseMocks() ? mockAPI(token, baseURL) : API(token, baseURL);
}

// Re-export mockAPI for direct access
export { default as mockAPI } from './protocol';
39 changes: 39 additions & 0 deletions src/mocks/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ProtocolError } from '@metacall/protocol/protocol';

export default async function login(
email: string,
password: string
): Promise<string> {
// simulate async
console.log('[MOCK LOGIN] Called with email:', email);
await new Promise(resolve => setTimeout(resolve, 10));

// missing credentials
if (!email || !password) {
const err = new Error(
'Invalid authorization header, no credentials provided.'
) as ProtocolError;
err.data = 'Invalid authorization header, no credentials provided.';
throw err;
}

// invalid email
if (!email.includes('@')) {
const err = new Error('Invalid email') as ProtocolError;
err.data = 'Invalid email';
throw err;
}

// invalid credentials (yeet@yeet.com / yeetyeet test case)
if (email === 'yeet@yeet.com' && password === 'yeetyeet') {
const err = new Error(
'Invalid account email or password.'
) as ProtocolError;
err.data = 'Invalid account email or password.';
throw err;
}

// success case → return token
console.log('[MOCK LOGIN] Returning token for:', email);
return `mock_token_${email}`;
}
191 changes: 191 additions & 0 deletions src/mocks/protocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { API as APIInterface } from '@metacall/protocol/protocol';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';

type DeployResponse = {
id: string;
prefix: string;
suffix: string;
version: string;
status: string;
};

type Deployment = {
id: string;
prefix: string;
suffix: string;
version: string;
status: 'create' | 'ready' | 'failed';
packages?: Record<string, any>;
};

type Subscription = {
Essential: number;
Professional: number;
Premium: number;
};

// Path to shared deployments store file
const getDeploymentsPath = (): string => {
return join(homedir(), '.metacall-deploy-test-state.json');
};

// Load deployments from shared file
const loadDeployments = (): Deployment[] => {
const path = getDeploymentsPath();
try {
if (existsSync(path)) {
const data = readFileSync(path, 'utf-8');
return JSON.parse(data) as Deployment[];
}
} catch (e) {
// If file doesn't exist or can't be parsed, return empty array
}
return [];
};

// Save deployments to shared file
const saveDeployments = (deployments: Deployment[]): void => {
const path = getDeploymentsPath();
try {
writeFileSync(path, JSON.stringify(deployments, null, 2), 'utf-8');
} catch (e) {
// Silently fail if can't write
}
};

export default function mockAPI(token: string, baseURL: string): APIInterface {
void baseURL; // May be used for future implementations
void token; // Mocks always succeed

const mockAPI: {
validate(): Promise<boolean>;
refresh(): Promise<string>;
inspect(): Promise<Deployment[]>;
upload(): Promise<{ id: string }>;
deploy(name: string): Promise<DeployResponse>;
deployDelete(
prefix: string,
suffix: string,
version: string
): Promise<string>;
add(
url: string,
branch: string,
runners: string[]
): Promise<{ id: string }>;
branchList(url: string): Promise<{ branches: string[] }>;
fileList(url: string, branch: string): Promise<unknown[]>;
listSubscriptions(): Promise<Subscription>;
listSubscriptionsDeploys(): Promise<Deployment[]>;
logs(): Promise<string>;
} = {
validate(): Promise<boolean> {
// In mock mode, validate any non-empty token
// Real validation would check JWT structure and signature
console.log('[MOCK API] validate() called with token:', token);
if (!token || token.length === 0) {
console.log('[MOCK API] Token is empty, throwing error');
throw new Error('Token is empty');
}
console.log('[MOCK API] Token is valid, returning true');
return Promise.resolve(true);
},

refresh(): Promise<string> {
return Promise.resolve(`${token}_refreshed`);
},

inspect(): Promise<Deployment[]> {
return Promise.resolve(
loadDeployments().map(d => ({
...d,
packages: {}
}))
);
},

upload(): Promise<{ id: string }> {
return Promise.resolve({ id: 'mock-upload-id' });
},

deploy(name: string): Promise<DeployResponse> {
const deployments = loadDeployments();

const deployment: Deployment = {
id: `mock-${Date.now()}`,
prefix: 'mock',
suffix: name,
version: `${Date.now()}`,
status: 'ready'
};

deployments.push(deployment);
saveDeployments(deployments);

return Promise.resolve(deployment);
},

deployDelete(
prefix: string,
suffix: string,
version: string
): Promise<string> {
const deployments = loadDeployments();

const index = deployments.findIndex(
d =>
d.prefix === prefix &&
d.suffix === suffix &&
d.version === version
);

if (index === -1) {
throw new Error('No deployment found');
}

deployments.splice(index, 1);
saveDeployments(deployments);

return Promise.resolve('Deploy Delete Succeed');
},

add(
url: string,
branch: string,
runners: string[]
): Promise<{ id: string }> {
return Promise.resolve({ id: 'mock-add-id' });
},

branchList(url: string): Promise<{ branches: string[] }> {
return Promise.resolve({ branches: ['main'] });
},

fileList(url: string, branch: string): Promise<unknown[]> {
return Promise.resolve([]);
},

listSubscriptions(): Promise<Subscription> {
return Promise.resolve({
Essential: 1,
Professional: 0,
Premium: 0
});
},

listSubscriptionsDeploys(): Promise<Deployment[]> {
const deployments = loadDeployments();
return Promise.resolve(deployments);
},

logs(): Promise<string> {
return Promise.resolve('');
}
};

return mockAPI as unknown as APIInterface;
}
Loading