Skip to content

Commit 2d58b75

Browse files
committed
feat: support multiple user profiles and hosts
Before this change it was not possible to use puter-cli on local instances of Puter, nad it was not possible to configure more than one Puter account. With this change, it is possible to add multiple accounts with specified hosts using the "puter login" command.
1 parent 3cbcd2f commit 2d58b75

File tree

9 files changed

+333
-112
lines changed

9 files changed

+333
-112
lines changed

bin/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ async function main() {
2020
.command('login')
2121
.description('Login to Puter account')
2222
.option('-s, --save', 'Save authentication token in .env file', '')
23-
.action(login);
23+
.action(() => {
24+
startShell('login');
25+
});
2426

2527
program
2628
.command('logout')

src/commands/auth.js

Lines changed: 9 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -5,109 +5,17 @@ import Conf from 'conf';
55
import ora from 'ora';
66
import fetch from 'node-fetch';
77
import { PROJECT_NAME, API_BASE, getHeaders, BASE_URL } from '../commons.js'
8+
import { ProfileAPI } from '../modules/ProfileModule.js';
9+
import { get_context } from '../temporary/context_helpers.js';
810
const config = new Conf({ projectName: PROJECT_NAME });
911

1012
/**
1113
* Login user
1214
* @returns void
1315
*/
14-
export async function login(args = {}) {
15-
const answers = await inquirer.prompt([
16-
{
17-
type: 'input',
18-
name: 'username',
19-
message: 'Username:',
20-
validate: input => input.length >= 1 || 'Username is required'
21-
},
22-
{
23-
type: 'password',
24-
name: 'password',
25-
message: 'Password:',
26-
mask: '*',
27-
validate: input => input.length >= 1 || 'Password is required'
28-
}
29-
]);
30-
31-
let spinner;
32-
try {
33-
spinner = ora('Logging in to Puter...').start();
34-
35-
const response = await fetch(`${BASE_URL}/login`, {
36-
method: 'POST',
37-
headers: getHeaders(),
38-
body: JSON.stringify({
39-
username: answers.username,
40-
password: answers.password
41-
})
42-
});
43-
44-
let data = await response.json();
45-
46-
while ( data.proceed && data.next_step ) {
47-
if ( data.next_step === 'otp') {
48-
spinner.succeed(chalk.green('2FA is enabled'));
49-
const answers = await inquirer.prompt([
50-
{
51-
type: 'input',
52-
name: 'otp',
53-
message: 'Authenticator Code:',
54-
validate: input => input.length === 6 || 'OTP must be 6 digits'
55-
}
56-
]);
57-
spinner = ora('Logging in to Puter...').start();
58-
const response = await fetch(`${BASE_URL}/login/otp`, {
59-
method: 'POST',
60-
headers: getHeaders(),
61-
body: JSON.stringify({
62-
token: data.otp_jwt_token,
63-
code: answers.otp,
64-
}),
65-
});
66-
data = await response.json();
67-
continue;
68-
}
69-
70-
if ( data.next_step === 'complete' ) break;
71-
72-
spinner.fail(chalk.red(`Unrecognized login step "${data.next_step}"; you might need to update puter-cli.`));
73-
return;
74-
}
75-
76-
if (data.proceed && data.token) {
77-
config.set('auth_token', data.token);
78-
config.set('username', answers.username);
79-
config.set('cwd', `/${answers.username}`);
80-
if (spinner){
81-
spinner.succeed(chalk.green('Successfully logged in to Puter!'));
82-
}
83-
console.log(chalk.dim(`Token: ${data.token.slice(0, 5)}...${data.token.slice(-5)}`));
84-
// Save token
85-
if (args.save){
86-
const localEnvFile = '.env';
87-
try {
88-
// Check if the file exists, if so then delete it before writing.
89-
if (fs.existsSync(localEnvFile)) {
90-
console.log(chalk.yellow(`File "${localEnvFile}" already exists... Adding token.`));
91-
fs.appendFileSync(localEnvFile, `\nPUTER_API_KEY="${data.token}"`, 'utf8');
92-
} else {
93-
console.log(chalk.cyan(`Saving token to ${chalk.green(localEnvFile)} file.`));
94-
fs.writeFileSync(localEnvFile, `PUTER_API_KEY="${data.token}"`, 'utf8');
95-
}
96-
} catch (error) {
97-
console.error(chalk.red(`Cannot save token to .env file. Error: ${error.message}`));
98-
console.log(chalk.cyan(`PUTER_API_KEY="${data.token}"`));
99-
}
100-
}
101-
} else {
102-
spinner.fail(chalk.red('Login failed. Please check your credentials.'));
103-
}
104-
} catch (error) {
105-
if (spinner) {
106-
spinner.fail(chalk.red('Failed to login'));
107-
} else {
108-
console.error(chalk.red(`Failed to login: ${error.message}`));
109-
}
110-
}
16+
export async function login(args = {}, context) {
17+
const profileAPI = context[ProfileAPI];
18+
await profileAPI.switchProfileWizard();
11119
}
11220

11321
/**
@@ -164,14 +72,17 @@ export async function getUserInfo() {
16472
}
16573
} catch (error) {
16674
console.error(chalk.red(`Failed to get user info.\nError: ${error.message}`));
75+
console.log(error);
16776
}
16877
}
16978
export function isAuthenticated() {
17079
return !!config.get('auth_token');
17180
}
17281

17382
export function getAuthToken() {
174-
return config.get('auth_token');
83+
const context = get_context();
84+
const profileAPI = context[ProfileAPI];
85+
return profileAPI.getAuthToken();
17586
}
17687

17788
export function getCurrentUserName() {

src/commands/files.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,7 @@ export async function pathExists(filePath) {
588588
return statResponse.ok;
589589
} catch (error){
590590
console.error(chalk.red('Failed to check if file exists.'));
591+
console.error('ERROR', error);
591592
return false;
592593
}
593594
}

src/commands/shell.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import readline from 'node:readline';
22
import chalk from 'chalk';
33
import Conf from 'conf';
44
import { execCommand, getPrompt } from '../executor.js';
5-
import { getAuthToken, login } from './auth.js';
65
import { PROJECT_NAME } from '../commons.js';
6+
import SetContextModule from '../modules/SetContextModule.js';
77
import ErrorModule from '../modules/ErrorModule.js';
8+
import ProfileModule from '../modules/ProfileModule.js';
89
import putility from '@heyputer/putility';
910

1011
const config = new Conf({ projectName: PROJECT_NAME });
@@ -26,16 +27,11 @@ export function updatePrompt(currentPath) {
2627
/**
2728
* Start the interactive shell
2829
*/
29-
export async function startShell() {
30-
if (!getAuthToken()) {
31-
console.log(chalk.cyan('Please login first (or use CTRL+C to exit):'));
32-
await login();
33-
console.log(chalk.green(`Now just type: ${chalk.cyan('puter')} to begin.`));
34-
process.exit(0);
35-
}
36-
30+
export async function startShell(command) {
3731
const modules = [
32+
SetContextModule,
3833
ErrorModule,
34+
ProfileModule,
3935
];
4036

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

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

43+
await context.events.emit('check-login', {});
44+
45+
// This argument enables the `puter <subcommand>` commands
46+
if ( command ) {
47+
await execCommand(context, command);
48+
process.exit(0);
49+
}
50+
4751
try {
4852
console.log(chalk.green('Welcome to Puter-CLI! Type "help" for available commands.'));
4953
rl.setPrompt(getPrompt());
@@ -66,4 +70,4 @@ export async function startShell() {
6670
} catch (error) {
6771
console.error(chalk.red('Error starting shell:', error));
6872
}
69-
}
73+
}

src/commons.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@ dotenv.config();
1010

1111
export const PROJECT_NAME = 'puter-cli';
1212
// If you haven't defined your own values in .env file, we'll assume you're running Puter on a local instance:
13-
export const API_BASE = process.env.PUTER_API_BASE || 'https://api.puter.com';
14-
export const BASE_URL = process.env.PUTER_BASE_URL || 'https://puter.com';
13+
export let API_BASE = process.env.PUTER_API_BASE || 'https://api.puter.com';
14+
export let BASE_URL = process.env.PUTER_BASE_URL || 'https://puter.com';
15+
export const NULL_UUID = '00000000-0000-0000-0000-000000000000';
16+
17+
export const reconfigureURLs = ({ api, base }) => {
18+
API_BASE = api;
19+
BASE_URL = base;
20+
};
1521

1622
/**
1723
* Get headers with the correct Content-Type for multipart form data.

src/executor.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { listFiles, makeDirectory, renameFileOrDirectory,
66
removeFileOrDirectory, emptyTrash, changeDirectory, showCwd,
77
getInfo, getDiskUsage, createFile, readFile, uploadFile,
88
downloadFile, copyFile, syncDirectory, editFile } from './commands/files.js';
9-
import { getUserInfo, getUsageInfo } from './commands/auth.js';
9+
import { getUserInfo, getUsageInfo, login } from './commands/auth.js';
1010
import { PROJECT_NAME, API_BASE, getHeaders } from './commons.js';
1111
import inquirer from 'inquirer';
1212
import { exec } from 'node:child_process';
@@ -34,6 +34,7 @@ const commands = {
3434
await import('./commands/auth.js').then(m => m.logout());
3535
process.exit(0);
3636
},
37+
login: login,
3738
whoami: getUserInfo,
3839
stat: getInfo,
3940
apps: async (args) => {

0 commit comments

Comments
 (0)