Skip to content

Commit 2068154

Browse files
authored
Merge pull request #12 from HeyPuter/feature/profiles
feat: support multiple user profiles and hosts
2 parents 3cbcd2f + 2d58b75 commit 2068154

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)