Skip to content
Merged
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
4 changes: 3 additions & 1 deletion bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ async function main() {
.command('login')
.description('Login to Puter account')
.option('-s, --save', 'Save authentication token in .env file', '')
.action(login);
.action(() => {
startShell('login');
});

program
.command('logout')
Expand Down
107 changes: 9 additions & 98 deletions src/commands/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,109 +5,17 @@
import ora from 'ora';
import fetch from 'node-fetch';
import { PROJECT_NAME, API_BASE, getHeaders, BASE_URL } from '../commons.js'
import { ProfileAPI } from '../modules/ProfileModule.js';
import { get_context } from '../temporary/context_helpers.js';
const config = new Conf({ projectName: PROJECT_NAME });

/**
* Login user
* @returns void
*/
export async function login(args = {}) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'username',
message: 'Username:',
validate: input => input.length >= 1 || 'Username is required'
},
{
type: 'password',
name: 'password',
message: 'Password:',
mask: '*',
validate: input => input.length >= 1 || 'Password is required'
}
]);

let spinner;
try {
spinner = ora('Logging in to Puter...').start();

const response = await fetch(`${BASE_URL}/login`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({
username: answers.username,
password: answers.password
})
});

let data = await response.json();

while ( data.proceed && data.next_step ) {
if ( data.next_step === 'otp') {
spinner.succeed(chalk.green('2FA is enabled'));
const answers = await inquirer.prompt([
{
type: 'input',
name: 'otp',
message: 'Authenticator Code:',
validate: input => input.length === 6 || 'OTP must be 6 digits'
}
]);
spinner = ora('Logging in to Puter...').start();
const response = await fetch(`${BASE_URL}/login/otp`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({
token: data.otp_jwt_token,
code: answers.otp,
}),
});
data = await response.json();
continue;
}

if ( data.next_step === 'complete' ) break;

spinner.fail(chalk.red(`Unrecognized login step "${data.next_step}"; you might need to update puter-cli.`));
return;
}

if (data.proceed && data.token) {
config.set('auth_token', data.token);
config.set('username', answers.username);
config.set('cwd', `/${answers.username}`);
if (spinner){
spinner.succeed(chalk.green('Successfully logged in to Puter!'));
}
console.log(chalk.dim(`Token: ${data.token.slice(0, 5)}...${data.token.slice(-5)}`));
// Save token
if (args.save){
const localEnvFile = '.env';
try {
// Check if the file exists, if so then delete it before writing.
if (fs.existsSync(localEnvFile)) {
console.log(chalk.yellow(`File "${localEnvFile}" already exists... Adding token.`));
fs.appendFileSync(localEnvFile, `\nPUTER_API_KEY="${data.token}"`, 'utf8');
} else {
console.log(chalk.cyan(`Saving token to ${chalk.green(localEnvFile)} file.`));
fs.writeFileSync(localEnvFile, `PUTER_API_KEY="${data.token}"`, 'utf8');
}
} catch (error) {
console.error(chalk.red(`Cannot save token to .env file. Error: ${error.message}`));
console.log(chalk.cyan(`PUTER_API_KEY="${data.token}"`));
}
}
} else {
spinner.fail(chalk.red('Login failed. Please check your credentials.'));
}
} catch (error) {
if (spinner) {
spinner.fail(chalk.red('Failed to login'));
} else {
console.error(chalk.red(`Failed to login: ${error.message}`));
}
}
export async function login(args = {}, context) {
const profileAPI = context[ProfileAPI];

Check failure on line 17 in src/commands/auth.js

View workflow job for this annotation

GitHub Actions / build (18.x)

tests/login.test.js > auth.js > login > should login successfully with valid credentials

TypeError: Cannot read properties of undefined (reading 'Symbol(ProfileAPI)') ❯ Module.login src/commands/auth.js:17:29 ❯ tests/login.test.js:76:13

Check failure on line 17 in src/commands/auth.js

View workflow job for this annotation

GitHub Actions / build (18.x)

tests/login.test.js > auth.js > login > should fail login with invalid credentials

TypeError: Cannot read properties of undefined (reading 'Symbol(ProfileAPI)') ❯ Module.login src/commands/auth.js:17:29 ❯ tests/login.test.js:103:13

Check failure on line 17 in src/commands/auth.js

View workflow job for this annotation

GitHub Actions / build (20.x)

tests/login.test.js > auth.js > login > should login successfully with valid credentials

TypeError: Cannot read properties of undefined (reading 'Symbol(ProfileAPI)') ❯ Module.login src/commands/auth.js:17:29 ❯ tests/login.test.js:76:13

Check failure on line 17 in src/commands/auth.js

View workflow job for this annotation

GitHub Actions / build (20.x)

tests/login.test.js > auth.js > login > should fail login with invalid credentials

TypeError: Cannot read properties of undefined (reading 'Symbol(ProfileAPI)') ❯ Module.login src/commands/auth.js:17:29 ❯ tests/login.test.js:103:13

Check failure on line 17 in src/commands/auth.js

View workflow job for this annotation

GitHub Actions / build (23.x)

tests/login.test.js > auth.js > login > should login successfully with valid credentials

TypeError: Cannot read properties of undefined (reading 'Symbol(ProfileAPI)') ❯ Module.login src/commands/auth.js:17:29 ❯ tests/login.test.js:76:13

Check failure on line 17 in src/commands/auth.js

View workflow job for this annotation

GitHub Actions / build (23.x)

tests/login.test.js > auth.js > login > should fail login with invalid credentials

TypeError: Cannot read properties of undefined (reading 'Symbol(ProfileAPI)') ❯ Module.login src/commands/auth.js:17:29 ❯ tests/login.test.js:103:13
await profileAPI.switchProfileWizard();
}

/**
Expand Down Expand Up @@ -164,14 +72,17 @@
}
} catch (error) {
console.error(chalk.red(`Failed to get user info.\nError: ${error.message}`));
console.log(error);
}
}
export function isAuthenticated() {
return !!config.get('auth_token');
}

export function getAuthToken() {
return config.get('auth_token');
const context = get_context();
const profileAPI = context[ProfileAPI];

Check failure on line 84 in src/commands/auth.js

View workflow job for this annotation

GitHub Actions / build (18.x)

tests/login.test.js > auth.js > Authentication > should return null if the auth_token is not defined

TypeError: Cannot read properties of undefined (reading 'Symbol(ProfileAPI)') ❯ Module.getAuthToken src/commands/auth.js:84:29 ❯ tests/login.test.js:213:22

Check failure on line 84 in src/commands/auth.js

View workflow job for this annotation

GitHub Actions / build (20.x)

tests/login.test.js > auth.js > Authentication > should return null if the auth_token is not defined

TypeError: Cannot read properties of undefined (reading 'Symbol(ProfileAPI)') ❯ Module.getAuthToken src/commands/auth.js:84:29 ❯ tests/login.test.js:213:22

Check failure on line 84 in src/commands/auth.js

View workflow job for this annotation

GitHub Actions / build (23.x)

tests/login.test.js > auth.js > Authentication > should return null if the auth_token is not defined

TypeError: Cannot read properties of undefined (reading 'Symbol(ProfileAPI)') ❯ Module.getAuthToken src/commands/auth.js:84:29 ❯ tests/login.test.js:213:22
return profileAPI.getAuthToken();
}

export function getCurrentUserName() {
Expand Down
1 change: 1 addition & 0 deletions src/commands/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,7 @@ export async function pathExists(filePath) {
return statResponse.ok;
} catch (error){
console.error(chalk.red('Failed to check if file exists.'));
console.error('ERROR', error);
return false;
}
}
Expand Down
24 changes: 14 additions & 10 deletions src/commands/shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import readline from 'node:readline';
import chalk from 'chalk';
import Conf from 'conf';
import { execCommand, getPrompt } from '../executor.js';
import { getAuthToken, login } from './auth.js';
import { PROJECT_NAME } from '../commons.js';
import SetContextModule from '../modules/SetContextModule.js';
import ErrorModule from '../modules/ErrorModule.js';
import ProfileModule from '../modules/ProfileModule.js';
import putility from '@heyputer/putility';

const config = new Conf({ projectName: PROJECT_NAME });
Expand All @@ -26,16 +27,11 @@ export function updatePrompt(currentPath) {
/**
* Start the interactive shell
*/
export async function startShell() {
if (!getAuthToken()) {
console.log(chalk.cyan('Please login first (or use CTRL+C to exit):'));
await login();
console.log(chalk.green(`Now just type: ${chalk.cyan('puter')} to begin.`));
process.exit(0);
}

export async function startShell(command) {
const modules = [
SetContextModule,
ErrorModule,
ProfileModule,
];

const context = new putility.libs.context.Context({
Expand All @@ -44,6 +40,14 @@ export async function startShell() {

for ( const module of modules ) module({ context });

await context.events.emit('check-login', {});

// This argument enables the `puter <subcommand>` commands
if ( command ) {
await execCommand(context, command);
process.exit(0);
}

try {
console.log(chalk.green('Welcome to Puter-CLI! Type "help" for available commands.'));
rl.setPrompt(getPrompt());
Expand All @@ -66,4 +70,4 @@ export async function startShell() {
} catch (error) {
console.error(chalk.red('Error starting shell:', error));
}
}
}
10 changes: 8 additions & 2 deletions src/commons.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ dotenv.config();

export const PROJECT_NAME = 'puter-cli';
// If you haven't defined your own values in .env file, we'll assume you're running Puter on a local instance:
export const API_BASE = process.env.PUTER_API_BASE || 'https://api.puter.com';
export const BASE_URL = process.env.PUTER_BASE_URL || 'https://puter.com';
export let API_BASE = process.env.PUTER_API_BASE || 'https://api.puter.com';
export let BASE_URL = process.env.PUTER_BASE_URL || 'https://puter.com';
export const NULL_UUID = '00000000-0000-0000-0000-000000000000';

export const reconfigureURLs = ({ api, base }) => {
API_BASE = api;
BASE_URL = base;
};

/**
* Get headers with the correct Content-Type for multipart form data.
Expand Down
3 changes: 2 additions & 1 deletion src/executor.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { listFiles, makeDirectory, renameFileOrDirectory,
removeFileOrDirectory, emptyTrash, changeDirectory, showCwd,
getInfo, getDiskUsage, createFile, readFile, uploadFile,
downloadFile, copyFile, syncDirectory, editFile } from './commands/files.js';
import { getUserInfo, getUsageInfo } from './commands/auth.js';
import { getUserInfo, getUsageInfo, login } from './commands/auth.js';
import { PROJECT_NAME, API_BASE, getHeaders } from './commons.js';
import inquirer from 'inquirer';
import { exec } from 'node:child_process';
Expand Down Expand Up @@ -34,6 +34,7 @@ const commands = {
await import('./commands/auth.js').then(m => m.logout());
process.exit(0);
},
login: login,
whoami: getUserInfo,
stat: getInfo,
apps: async (args) => {
Expand Down
Loading
Loading