puter-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.idx/dev.nix +53 -0
- package/LICENSE.md +209 -0
- package/README.md +292 -0
- package/bin/index.js +38 -0
- package/commands/apps.js +421 -0
- package/commands/auth.js +233 -0
- package/commands/commons.js +296 -0
- package/commands/executor.js +306 -0
- package/commands/files.js +1290 -0
- package/commands/init.js +67 -0
- package/commands/shell.js +56 -0
- package/commands/sites.js +214 -0
- package/commands/subdomains.js +103 -0
- package/commands/utils.js +92 -0
- package/package.json +37 -0
- package/tests/login.test.js +268 -0
package/commands/init.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { promises as fs } from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { generateAppName } from './commons.js';
|
|
7
|
+
|
|
8
|
+
export async function init() {
|
|
9
|
+
const answers = await inquirer.prompt([
|
|
10
|
+
{
|
|
11
|
+
type: 'input',
|
|
12
|
+
name: 'name',
|
|
13
|
+
message: 'What is your app name?',
|
|
14
|
+
default: `${generateAppName()}`
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
type: 'list',
|
|
18
|
+
name: 'template',
|
|
19
|
+
message: 'Select a template:',
|
|
20
|
+
choices: ['basic', 'static-site', 'full-stack']
|
|
21
|
+
}
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const spinner = ora('Creating Puter app...').start();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Create basic app structure
|
|
28
|
+
await createAppStructure(answers);
|
|
29
|
+
spinner.succeed(chalk.green('Successfully created Puter app!'));
|
|
30
|
+
|
|
31
|
+
console.log('\nNext steps:');
|
|
32
|
+
console.log(chalk.cyan('1. cd'), answers.name);
|
|
33
|
+
console.log(chalk.cyan('2. npm install'));
|
|
34
|
+
console.log(chalk.cyan('3. npm start'));
|
|
35
|
+
} catch (error) {
|
|
36
|
+
spinner.fail(chalk.red('Failed to create app'));
|
|
37
|
+
console.error(error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function createAppStructure({ name, template }) {
|
|
42
|
+
// Create project directory
|
|
43
|
+
await fs.mkdir(name, { recursive: true });
|
|
44
|
+
|
|
45
|
+
// Create basic files
|
|
46
|
+
const files = {
|
|
47
|
+
'.env': `APP_NAME=${name}\PUTER_API_KEY=`,
|
|
48
|
+
'index.html': `<!DOCTYPE html>
|
|
49
|
+
<html>
|
|
50
|
+
<head>
|
|
51
|
+
<title>${name}</title>
|
|
52
|
+
</head>
|
|
53
|
+
<body>
|
|
54
|
+
<h1>Welcome to ${name}</h1>
|
|
55
|
+
<script src="https://js.puter.com/v2/"></script>
|
|
56
|
+
<script src="app.js"></script>
|
|
57
|
+
</body>
|
|
58
|
+
</html>`,
|
|
59
|
+
'app.js': `// Initialize Puter app
|
|
60
|
+
console.log('Puter app initialized!');`,
|
|
61
|
+
'README.md': `# ${name}\n\nA Puter app created with puter-cli`
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
65
|
+
await fs.writeFile(path.join(name, filename), content);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import Conf from 'conf';
|
|
4
|
+
import { execCommand, getPrompt } from './executor.js';
|
|
5
|
+
import { getAuthToken } from './auth.js';
|
|
6
|
+
import { PROJECT_NAME } from './commons.js';
|
|
7
|
+
|
|
8
|
+
const config = new Conf({ projectName: PROJECT_NAME });
|
|
9
|
+
|
|
10
|
+
const rl = readline.createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout,
|
|
13
|
+
prompt: null
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Update the current shell prompt
|
|
18
|
+
*/
|
|
19
|
+
export function updatePrompt(currentPath) {
|
|
20
|
+
config.set('cwd', currentPath);
|
|
21
|
+
rl.setPrompt(getPrompt());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start the interactive shell
|
|
26
|
+
*/
|
|
27
|
+
export function startShell() {
|
|
28
|
+
if (!getAuthToken()) {
|
|
29
|
+
console.log(chalk.red('Please login first using: puter login'));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
rl.setPrompt(getPrompt());
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
console.log(chalk.green('Welcome to Puter-CLI! Type "help" for available commands.'));
|
|
37
|
+
rl.prompt();
|
|
38
|
+
|
|
39
|
+
rl.on('line', async (line) => {
|
|
40
|
+
const trimmedLine = line.trim();
|
|
41
|
+
if (trimmedLine) {
|
|
42
|
+
try {
|
|
43
|
+
await execCommand(trimmedLine);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(chalk.red(error.message));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
rl.prompt();
|
|
49
|
+
}).on('close', () => {
|
|
50
|
+
console.log(chalk.yellow('\nGoodbye!'));
|
|
51
|
+
process.exit(0);
|
|
52
|
+
});
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(chalk.red('Error starting shell:', error));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fetch from 'node-fetch';
|
|
3
|
+
import Table from 'cli-table3';
|
|
4
|
+
import { getCurrentUserName, getCurrentDirectory } from './auth.js';
|
|
5
|
+
import { API_BASE, getHeaders, generateAppName, resolvePath, isValidAppName } from './commons.js';
|
|
6
|
+
import { displayNonNullValues, formatDate, formatDateTime } from './utils.js';
|
|
7
|
+
import { getSubdomains, createSubdomain, deleteSubdomain } from './subdomains.js';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Listing subdomains
|
|
12
|
+
*/
|
|
13
|
+
export async function listSites(args = {}) {
|
|
14
|
+
try {
|
|
15
|
+
const data = await getSubdomains(args);
|
|
16
|
+
|
|
17
|
+
if (!data.success || !Array.isArray(data.result)) {
|
|
18
|
+
throw new Error('Failed to fetch subdomains');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Create table instance
|
|
22
|
+
const table = new Table({
|
|
23
|
+
head: [
|
|
24
|
+
chalk.cyan('#'),
|
|
25
|
+
chalk.cyan('UID'),
|
|
26
|
+
chalk.cyan('Subdomain'),
|
|
27
|
+
chalk.cyan('Created'),
|
|
28
|
+
chalk.cyan('Protected'),
|
|
29
|
+
// chalk.cyan('Owner'),
|
|
30
|
+
chalk.cyan('Directory')
|
|
31
|
+
],
|
|
32
|
+
wordWrap: false
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Format and add data to table
|
|
36
|
+
let i = 0;
|
|
37
|
+
data.result.forEach(domain => {
|
|
38
|
+
table.push([
|
|
39
|
+
i++,
|
|
40
|
+
domain.uid,
|
|
41
|
+
chalk.green(`${chalk.dim(domain.subdomain)}.puter.site`),
|
|
42
|
+
formatDate(domain.created_at).split(',')[0],
|
|
43
|
+
domain.protected ? chalk.red('Yes') : chalk.green('No'),
|
|
44
|
+
// domain.owner['username'],
|
|
45
|
+
domain?.root_dir?.path.split('/').pop()
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Print table
|
|
50
|
+
if (data.result.length === 0) {
|
|
51
|
+
console.log(chalk.yellow('No subdomains found'));
|
|
52
|
+
} else {
|
|
53
|
+
console.log(chalk.bold('\nYour Sites:'));
|
|
54
|
+
console.log(table.toString());
|
|
55
|
+
console.log(chalk.dim(`Total Sites: ${data.result.length}`));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(chalk.red('Error listing sites:'), error.message);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get Site info
|
|
66
|
+
* @param {any[]} args Array of site uuid
|
|
67
|
+
*/
|
|
68
|
+
export async function infoSite(args = []) {
|
|
69
|
+
if (args.length < 1){
|
|
70
|
+
console.log(chalk.red('Usage: site <siteUID>'));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (const subdomainId of args)
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch(`${API_BASE}/drivers/call`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: getHeaders(),
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
interface: 'puter-subdomains',
|
|
80
|
+
method: 'read',
|
|
81
|
+
args: { uid: subdomainId }
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new Error('Failed to fetch subdomains.');
|
|
87
|
+
}
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
if (!data.success || !data.result) {
|
|
90
|
+
throw new Error(`Failed to get site info: ${data.error?.message}`);
|
|
91
|
+
}
|
|
92
|
+
displayNonNullValues(data.result);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(chalk.red('Error getting site info:'), error.message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Delete hosted web site
|
|
100
|
+
* @param {any[]} args Array of site uuid
|
|
101
|
+
*/
|
|
102
|
+
export async function deleteSite(args = []) {
|
|
103
|
+
if (args.length < 1){
|
|
104
|
+
console.log(chalk.red('Usage: site:delete <siteUUID>'));
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
for (const uuid of args)
|
|
108
|
+
try {
|
|
109
|
+
// The uuid must be prefixed with: 'subdomainObj-'
|
|
110
|
+
const response = await fetch(`${API_BASE}/delete-site`, {
|
|
111
|
+
headers: getHeaders(),
|
|
112
|
+
method: 'POST',
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
site_uuid: uuid
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
throw new Error(`Failed to delete site (Status: ${response.status})`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const data = await response.json();
|
|
123
|
+
const result = await deleteSubdomain(uuid);
|
|
124
|
+
if (result){
|
|
125
|
+
// check if data is empty object
|
|
126
|
+
if (Object.keys(data).length === 0){
|
|
127
|
+
console.log(chalk.green(`Site ID: "${uuid}" should be deleted.`));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
console.log(chalk.yellow(`Site ID: "${uuid}" may already be deleted!`));
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error(chalk.red('Error deleting site:'), error.message);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create a static web app from the current directory to Puter cloud.
|
|
140
|
+
* @param {string[]} args - Command-line arguments (e.g., [name, --subdomain=<subdomain>]).
|
|
141
|
+
*/
|
|
142
|
+
export async function createSite(args = []) {
|
|
143
|
+
if (args.length < 1 || !isValidAppName(args[0])) {
|
|
144
|
+
console.log(chalk.red('Usage: site:create <valid_name_app> [<remote_dir>] [--subdomain=<subdomain>]'));
|
|
145
|
+
console.log(chalk.yellow('Example: site:create mysite'));
|
|
146
|
+
console.log(chalk.yellow('Example: site:create mysite ./mysite'));
|
|
147
|
+
console.log(chalk.yellow('Example: site:create mysite --subdomain=mysite'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const appName = args[0]; // Site name (required)
|
|
152
|
+
const subdomainOption = args.find(arg => arg.toLocaleLowerCase().startsWith('--subdomain='))?.split('=')[1]; // Optional subdomain
|
|
153
|
+
// Use the current directory as the root directory if none specified
|
|
154
|
+
const remoteDir = resolvePath(getCurrentDirectory(), (args[1] && !args[1].startsWith('--'))?args[1]:'.');
|
|
155
|
+
|
|
156
|
+
console.log(chalk.green(`Creating site "${appName}" from "${remoteDir}"...\n`));
|
|
157
|
+
try {
|
|
158
|
+
// Step 1: Determine the subdomain
|
|
159
|
+
let subdomain;
|
|
160
|
+
if (subdomainOption) {
|
|
161
|
+
subdomain = subdomainOption; // Use the provided subdomain
|
|
162
|
+
} else {
|
|
163
|
+
subdomain = appName; // Default to the app name as the subdomain
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Step 2: Check if the subdomain already exists
|
|
167
|
+
const data = await getSubdomains();
|
|
168
|
+
if (!data.success || !Array.isArray(data.result)) {
|
|
169
|
+
throw new Error('Failed to fetch subdomains');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const subdomains = data.result;
|
|
173
|
+
const subdomainObj = subdomains.find(sd => sd.subdomain === subdomain);
|
|
174
|
+
if (subdomainObj) {
|
|
175
|
+
console.error(chalk.cyan(`The subdomain "${subdomain}" is already in use and owned by: "${subdomainObj.owner['username']}"`));
|
|
176
|
+
if (subdomainObj.owner['username'] === getCurrentUserName()){
|
|
177
|
+
console.log(chalk.green(`It's yours, and linked to: ${subdomainObj.root_dir?.path}`));
|
|
178
|
+
if (subdomainObj.root_dir?.path === remoteDir){
|
|
179
|
+
console.log(chalk.cyan(`Which is already the selected directory, and created at:`));
|
|
180
|
+
console.log(chalk.green(`https://${subdomain}.puter.site`));
|
|
181
|
+
return;
|
|
182
|
+
} else {
|
|
183
|
+
console.log(chalk.yellow(`However, It's linked to different directory at: ${subdomainObj.root_dir?.path}`));
|
|
184
|
+
console.log(chalk.cyan(`We'll try to unlink this subdomain from that directory...`));
|
|
185
|
+
const result = await deleteSubdomain(subdomainObj.uid);
|
|
186
|
+
if (result) {
|
|
187
|
+
console.log(chalk.green('Looks like this subdomain is free again, please try again.'));
|
|
188
|
+
return;
|
|
189
|
+
} else {
|
|
190
|
+
console.log(chalk.red('Could not release this subdomain.'));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
console.log(chalk.yellow(`The subdomain: "${subdomain}" is already taken, so let's generate a new random one:`));
|
|
196
|
+
subdomain = generateAppName(); // Generate a random subdomain
|
|
197
|
+
console.log(chalk.cyan(`New generated subdomain: "${subdomain}" will be used.`));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Step 3: Host the current directory under the subdomain
|
|
201
|
+
console.log(chalk.cyan(`Hosting app "${appName}" under subdomain "${subdomain}"...`));
|
|
202
|
+
const site = await createSubdomain(subdomain, remoteDir);
|
|
203
|
+
if (!site){
|
|
204
|
+
console.error(chalk.red(`Failed to create subdomain: "${chalk.red(subdomain)}"`));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(chalk.green(`App "${chalk.red(appName)}" created successfully at:`));
|
|
209
|
+
console.log(chalk.dim(`https://${site.subdomain}.puter.site`));
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error(chalk.red('Failed to create site.'));
|
|
212
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fetch from 'node-fetch';
|
|
3
|
+
import { API_BASE, getHeaders } from './commons.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get list of subdomains.
|
|
7
|
+
* @param {Object} args - Options for the query.
|
|
8
|
+
* @returns {Array} - Array of subdomains.
|
|
9
|
+
*/
|
|
10
|
+
export async function getSubdomains(args = {}) {
|
|
11
|
+
const response = await fetch(`${API_BASE}/drivers/call`, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: getHeaders(),
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
interface: 'puter-subdomains',
|
|
16
|
+
method: 'select',
|
|
17
|
+
args: args
|
|
18
|
+
})
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
throw new Error('Failed to fetch subdomains.');
|
|
23
|
+
}
|
|
24
|
+
return await response.json();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Delete a subdomain by id
|
|
29
|
+
* @param {Array} subdomain IDs
|
|
30
|
+
* @return {boolean} Result of the operation
|
|
31
|
+
*/
|
|
32
|
+
export async function deleteSubdomain(args = []) {
|
|
33
|
+
if (args.length < 1){
|
|
34
|
+
console.log(chalk.red('Usage: domain:delete <subdomain_id>'));
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const subdomains = args;
|
|
38
|
+
for (const subdomainId of subdomains)
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(`${API_BASE}/drivers/call`, {
|
|
41
|
+
headers: getHeaders(),
|
|
42
|
+
method: 'POST',
|
|
43
|
+
body: JSON.stringify({
|
|
44
|
+
interface: 'puter-subdomains',
|
|
45
|
+
method: 'delete',
|
|
46
|
+
args: {
|
|
47
|
+
id: { subdomain: subdomainId }
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
if (!data.success) {
|
|
54
|
+
if (data.error?.code === 'entity_not_found') {
|
|
55
|
+
console.log(chalk.red(`Subdomain ID: "${subdomainId}" not found`));
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
console.log(chalk.red(`Failed to delete subdomain: ${data.error?.message}`));
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
console.log(chalk.green('Subdomain deleted successfully'));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(chalk.red('Error deleting subdomain:'), error.message);
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create a new subdomain into remote directory
|
|
70
|
+
* @param {string} subdomain - Subdomain name.
|
|
71
|
+
* @param {string} remoteDir - Remote directory path.
|
|
72
|
+
* @returns {Object} - Hosting details (e.g., subdomain).
|
|
73
|
+
*/
|
|
74
|
+
export async function createSubdomain(subdomain, remoteDir) {
|
|
75
|
+
const response = await fetch(`${API_BASE}/drivers/call`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: getHeaders(),
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
interface: 'puter-subdomains',
|
|
80
|
+
method: 'create',
|
|
81
|
+
args: {
|
|
82
|
+
object: {
|
|
83
|
+
subdomain: subdomain,
|
|
84
|
+
root_dir: remoteDir
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error('Failed to host directory.');
|
|
92
|
+
}
|
|
93
|
+
const data = await response.json();
|
|
94
|
+
if (!data.success || !data.result) {
|
|
95
|
+
if (data.error?.code === 'already_in_use') {
|
|
96
|
+
console.log(chalk.yellow(`Subdomain already taken!\nMessage: ${data?.error?.message}`));
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
console.log(chalk.red(`Error when creating "${subdomain}".\nError: ${data?.error?.message}\nCode: ${data.error?.code}`));
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return data.result;
|
|
103
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert "2024-10-07T15:03:53.000Z" to "10/7/2024, 15:03:53"
|
|
5
|
+
* @param {Date} value date value
|
|
6
|
+
* @returns formatted date string
|
|
7
|
+
*/
|
|
8
|
+
export function formatDate(value) {
|
|
9
|
+
const date = new Date(value);
|
|
10
|
+
return date.toLocaleString("en-US", {
|
|
11
|
+
year: "numeric",
|
|
12
|
+
month: "2-digit",
|
|
13
|
+
day: "2-digit",
|
|
14
|
+
hour: "2-digit",
|
|
15
|
+
minute: "2-digit",
|
|
16
|
+
second: "2-digit",
|
|
17
|
+
hour12: false
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Format timestamp to date or time
|
|
23
|
+
* @param {number} timestamp value
|
|
24
|
+
* @returns string
|
|
25
|
+
*/
|
|
26
|
+
export function formatDateTime(timestamp) {
|
|
27
|
+
const date = new Date(timestamp * 1000); // Convert to milliseconds
|
|
28
|
+
const now = new Date();
|
|
29
|
+
const diff = now - date;
|
|
30
|
+
if (diff < 86400000) { // Less than 24 hours
|
|
31
|
+
return date.toLocaleTimeString();
|
|
32
|
+
} else {
|
|
33
|
+
return date.toLocaleDateString();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Format file size in human readable format
|
|
38
|
+
* @param {number} size File size value
|
|
39
|
+
* @returns string formatted in human readable format
|
|
40
|
+
*/
|
|
41
|
+
export function formatSize(size) {
|
|
42
|
+
if (size === null || size === undefined) return '0';
|
|
43
|
+
if (size === 0) return '0';
|
|
44
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
45
|
+
let unit = 0;
|
|
46
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
47
|
+
size /= 1024;
|
|
48
|
+
unit++;
|
|
49
|
+
}
|
|
50
|
+
return `${size.toFixed(1)} ${units[unit]}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Display non null values in formatted table
|
|
55
|
+
* @param {Object} data Object to display
|
|
56
|
+
* @returns null
|
|
57
|
+
*/
|
|
58
|
+
export function displayNonNullValues(data) {
|
|
59
|
+
if (typeof data !== 'object' || data === null) {
|
|
60
|
+
console.error("Invalid input: Input must be a non-null object.");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const tableData = [];
|
|
64
|
+
function flattenObject(obj, parentKey = '') {
|
|
65
|
+
for (const key in obj) {
|
|
66
|
+
const value = obj[key];
|
|
67
|
+
const newKey = parentKey ? `${parentKey}.${key}` : key;
|
|
68
|
+
if (value !== null) {
|
|
69
|
+
if (typeof value === 'object') {
|
|
70
|
+
flattenObject(value, newKey);
|
|
71
|
+
} else {
|
|
72
|
+
tableData.push({ key: newKey, value: value });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
flattenObject(data);
|
|
79
|
+
// Determine max key length for formatting
|
|
80
|
+
const maxKeyLength = tableData.reduce((max, item) => Math.max(max, item.key.length), 0);
|
|
81
|
+
// Format and output the table
|
|
82
|
+
console.log(chalk.cyan('-'.repeat(maxKeyLength*3)));
|
|
83
|
+
console.log(chalk.cyan(`| ${'Key'.padEnd(maxKeyLength)} | Value`));
|
|
84
|
+
console.log(chalk.cyan('-'.repeat(maxKeyLength*3)));
|
|
85
|
+
tableData.forEach(item => {
|
|
86
|
+
const key = item.key.padEnd(maxKeyLength);
|
|
87
|
+
const value = String(item.value);
|
|
88
|
+
console.log(chalk.green(`| ${chalk.dim(key)} | ${value}`));
|
|
89
|
+
});
|
|
90
|
+
console.log(chalk.cyan('-'.repeat(maxKeyLength*3)));
|
|
91
|
+
console.log(chalk.cyan(`You have ${chalk.green(tableData.length)} key/value pair(s).`));
|
|
92
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "puter-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Command line interface for Puter cloud platform",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"puter": "./bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/index.js",
|
|
12
|
+
"test": "vitest run tests/*",
|
|
13
|
+
"test:watch": "vitest --watch tests/*"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"puter",
|
|
17
|
+
"cli",
|
|
18
|
+
"cloud"
|
|
19
|
+
],
|
|
20
|
+
"author": "Ibrahim.H",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"chalk": "^5.3.0",
|
|
24
|
+
"cli-table3": "^0.6.5",
|
|
25
|
+
"commander": "^11.1.0",
|
|
26
|
+
"conf": "^12.0.0",
|
|
27
|
+
"cross-spawn": "^7.0.3",
|
|
28
|
+
"glob": "^11.0.0",
|
|
29
|
+
"inquirer": "^9.2.12",
|
|
30
|
+
"minimatch": "^10.0.1",
|
|
31
|
+
"node-fetch": "^3.3.2",
|
|
32
|
+
"ora": "^8.0.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"vitest": "^2.1.8"
|
|
36
|
+
}
|
|
37
|
+
}
|