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.
@@ -0,0 +1,296 @@
1
+ import chalk from 'chalk';
2
+ import { getAuthToken } from './auth.js';
3
+
4
+ export const PROJECT_NAME = 'puter-cli';
5
+ export const API_BASE = 'https://api.puter.com';
6
+ export const BASE_URL = 'https://puter.com';
7
+
8
+ /**
9
+ * Get headers with the correct Content-Type for multipart form data.
10
+ * @param {string} contentType - The "Content-Type" argument for the header ('application/json' is the default)
11
+ * Use the multipart form data for upload a file.
12
+ * @returns {Object} The headers object.
13
+ */
14
+ export function getHeaders(contentType = 'application/json') {
15
+ return {
16
+ 'Accept': '*/*',
17
+ 'Accept-Language': 'en-US,en;q=0.9',
18
+ 'Authorization': `Bearer ${getAuthToken()}`,
19
+ 'Connection': 'keep-alive',
20
+ // 'Host': 'api.puter.com',
21
+ 'Content-Type': contentType,
22
+ 'Origin': `${BASE_URL}`,
23
+ 'Referer': `${BASE_URL}/`,
24
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Generate a random app name
30
+ * @returns a random app name or null if it fails
31
+ * @see: [randName](https://github.com/HeyPuter/puter/blob/06a67a3b223a6cbd7ec2e16853b6d2304f621a88/src/puter-js/src/index.js#L389)
32
+ */
33
+ export function generateAppName(separateWith = '-'){
34
+ console.log(chalk.cyan('Generating random app name...'));
35
+ try {
36
+ const first_adj = ['helpful','sensible', 'loyal', 'honest', 'clever', 'capable','calm', 'smart', 'genius', 'bright', 'charming', 'creative', 'diligent', 'elegant', 'fancy',
37
+ 'colorful', 'avid', 'active', 'gentle', 'happy', 'intelligent', 'jolly', 'kind', 'lively', 'merry', 'nice', 'optimistic', 'polite',
38
+ 'quiet', 'relaxed', 'silly', 'victorious', 'witty', 'young', 'zealous', 'strong', 'brave', 'agile', 'bold'];
39
+
40
+ const nouns = ['street', 'roof', 'floor', 'tv', 'idea', 'morning', 'game', 'wheel', 'shoe', 'bag', 'clock', 'pencil', 'pen',
41
+ 'magnet', 'chair', 'table', 'house', 'dog', 'room', 'book', 'car', 'cat', 'tree',
42
+ 'flower', 'bird', 'fish', 'sun', 'moon', 'star', 'cloud', 'rain', 'snow', 'wind', 'mountain',
43
+ 'river', 'lake', 'sea', 'ocean', 'island', 'bridge', 'road', 'train', 'plane', 'ship', 'bicycle',
44
+ 'horse', 'elephant', 'lion', 'tiger', 'bear', 'zebra', 'giraffe', 'monkey', 'snake', 'rabbit', 'duck',
45
+ 'goose', 'penguin', 'frog', 'crab', 'shrimp', 'whale', 'octopus', 'spider', 'ant', 'bee', 'butterfly', 'dragonfly',
46
+ 'ladybug', 'snail', 'camel', 'kangaroo', 'koala', 'panda', 'piglet', 'sheep', 'wolf', 'fox', 'deer', 'mouse', 'seal',
47
+ 'chicken', 'cow', 'dinosaur', 'puppy', 'kitten', 'circle', 'square', 'garden', 'otter', 'bunny', 'meerkat', 'harp']
48
+
49
+ // return a random combination of first_adj + noun + number (between 0 and 9999)
50
+ // e.g. clever-idea-123
51
+ const appName = first_adj[Math.floor(Math.random() * first_adj.length)] + separateWith + nouns[Math.floor(Math.random() * nouns.length)] + separateWith + Math.floor(Math.random() * 10000);
52
+ console.log(chalk.green(`AppName: "${appName}"`));
53
+ return appName;
54
+ } catch (error) {
55
+ console.error(`Error: ${error.message}`);
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Display data in a structured format
62
+ * @param {Array} data - The data to display
63
+ * @param {Object} options - Display options
64
+ * @param {Array} options.headers - Headers for the table
65
+ * @param {Array} options.columns - Columns to display
66
+ * @param {number} options.columnWidth - Width of each column
67
+ */
68
+ export function displayTable(data, options = {}) {
69
+ const { headers = [], columns = [], columnWidth = 20 } = options;
70
+
71
+ // Create the header row
72
+ const headerRow = headers.map(header => chalk.cyan(header.padEnd(columnWidth))).join(' | ');
73
+ console.log(headerRow);
74
+ console.log(chalk.dim('-'.repeat(headerRow.length)));
75
+
76
+ // Create and display each row of data
77
+ data.forEach(item => {
78
+ const row = columns.map(col => {
79
+ const value = item[col] || 'N/A';
80
+ return value.toString().padEnd(columnWidth);
81
+ }).join(' | ');
82
+ console.log(row);
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Display structured ouput of disk usage informations
88
+ */
89
+ export function showDiskSpaceUsage(data) {
90
+ const freeSpace = parseInt(data.capacity) - parseInt(data.used);
91
+ const usagePercentage = (parseInt(data.used) / parseInt(data.capacity)) * 100;
92
+ console.log(chalk.cyan('Disk Usage Information:'));
93
+ console.log(chalk.dim('----------------------------------------'));
94
+ console.log(chalk.cyan(`Total Capacity: `) + chalk.white(formatSize(data.capacity)));
95
+ console.log(chalk.cyan(`Used Space: `) + chalk.white(formatSize(data.used)));
96
+ console.log(chalk.cyan(`Free Space: `) + chalk.white(formatSize(freeSpace)));
97
+ // format the usagePercentage with 2 decimal floating point value:
98
+ console.log(chalk.cyan(`Usage Percentage: `) + chalk.white(`${usagePercentage.toFixed(2)}%`));
99
+ console.log(chalk.dim('----------------------------------------'));
100
+ console.log(chalk.green('Done.'));
101
+ }
102
+
103
+ /**
104
+ * Resolve a relative path to an absolute path
105
+ * @param {string} currentPath - The current working directory
106
+ * @param {string} relativePath - The relative path to resolve
107
+ * @returns {string} The resolved absolute path
108
+ */
109
+ export function resolvePath(currentPath, relativePath) {
110
+ // Normalize the current path (remove trailing slashes)
111
+ currentPath = currentPath.replace(/\/+$/, '');
112
+
113
+ // Split the relative path into parts
114
+ const parts = relativePath.split('/').filter(p => p); // Remove empty parts
115
+
116
+ // Handle each part of the relative path
117
+ for (const part of parts) {
118
+ if (part === '..') {
119
+ // Move one level up
120
+ const currentParts = currentPath.split('/').filter(p => p);
121
+ if (currentParts.length > 0) {
122
+ currentParts.pop(); // Remove the last part
123
+ }
124
+ currentPath = '/' + currentParts.join('/');
125
+ } else if (part === '.') {
126
+ // Stay in the current directory (no change)
127
+ continue;
128
+ } else {
129
+ // Move into a subdirectory
130
+ currentPath += `/${part}`;
131
+ }
132
+ }
133
+
134
+ // Normalize the final path (remove duplicate slashes)
135
+ currentPath = currentPath.replace(/\/+/g, '/');
136
+
137
+ // Ensure the path ends with a slash if it's the root
138
+ if (currentPath === '') {
139
+ currentPath = '/';
140
+ }
141
+
142
+ return currentPath;
143
+ }
144
+
145
+ /**
146
+ * Checks if a given string is a valid app name.
147
+ * The name must:
148
+ * - Not be '.' or '..'
149
+ * - Not contain path separators ('/' or '\\')
150
+ * - Not contain wildcard characters ('*')
151
+ * - (Optional) Contain only allowed characters (letters, numbers, spaces, underscores, hyphens)
152
+ *
153
+ * @param {string} name - The app name to validate.
154
+ * @returns {boolean} - Returns true if valid, false otherwise.
155
+ */
156
+ export function isValidAppName(name) {
157
+ // Ensure the name is a non-empty string
158
+ if (typeof name !== 'string' || name.trim().length === 0) {
159
+ return false;
160
+ }
161
+
162
+ // Trim whitespace from both ends
163
+ const trimmedName = name.trim();
164
+
165
+ // Reject reserved names
166
+ if (trimmedName === '.' || trimmedName === '..') {
167
+ return false;
168
+ }
169
+
170
+ // Regex patterns for invalid characters
171
+ const invalidPattern = /[\/\\*]/; // Disallow /, \, and *
172
+
173
+ if (invalidPattern.test(trimmedName)) {
174
+ return false;
175
+ }
176
+
177
+ // Optional: Define allowed characters pattern
178
+ // Uncomment the following lines if you want to enforce allowed characters
179
+ /*
180
+ const allowedPattern = /^[A-Za-z0-9 _-]+$/;
181
+ if (!allowedPattern.test(trimmedName)) {
182
+ return false;
183
+ }
184
+ */
185
+
186
+ // All checks passed
187
+ return true;
188
+ }
189
+
190
+ export function getDefaultHomePage(appName) {
191
+ const defaultIndexContent = `<!DOCTYPE html>
192
+ <html lang="en">
193
+ <head>
194
+ <meta charset="UTF-8">
195
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
196
+ <title>${appName}</title>
197
+ <style>
198
+ body {
199
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
200
+ line-height: 1.6;
201
+ max-width: 800px;
202
+ margin: 0 auto;
203
+ padding: 20px;
204
+ background: #f9fafb;
205
+ color: #1f2937;
206
+ }
207
+ .container {
208
+ background: white;
209
+ padding: 2rem;
210
+ border-radius: 8px;
211
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
212
+ }
213
+ h1 {
214
+ color: #2563eb;
215
+ margin-bottom: 1rem;
216
+ }
217
+ .code-block {
218
+ background: #f1f5f9;
219
+ padding: 1rem;
220
+ border-radius: 4px;
221
+ font-family: monospace;
222
+ overflow-x: auto;
223
+ }
224
+ .tip {
225
+ background: #dbeafe;
226
+ border-left: 4px solid #2563eb;
227
+ padding: 1rem;
228
+ margin: 1rem 0;
229
+ }
230
+ .links {
231
+ display: flex;
232
+ gap: 1rem;
233
+ margin-top: 2rem;
234
+ }
235
+ .links a {
236
+ color: #2563eb;
237
+ text-decoration: none;
238
+ }
239
+ .links a:hover {
240
+ text-decoration: underline;
241
+ }
242
+ .footer {
243
+ text-align: center;
244
+ margin-top: 50px;
245
+ color: var(--color-grey);
246
+ font-size: 0.9rem;
247
+ }
248
+ </style>
249
+ </head>
250
+ <body>
251
+ <div class="container">
252
+ <h1>🚀 Welcome to ${appName}!</h1>
253
+
254
+ <p>This is your new website powered by Puter. You can start customizing it right away!</p>
255
+
256
+ <div class="tip">
257
+ <strong>Quick Tip:</strong> Replace this content with your own by editing the <code>index.html</code> file.
258
+ </div>
259
+
260
+ <h2>🌟 Getting Started</h2>
261
+
262
+ <p>Here's a simple example using Puter.js:</p>
263
+
264
+ <div class="code-block">
265
+ &lt;script src="https://js.puter.com/v2/">&lt;/script>
266
+ &lt;script>
267
+ // Create a new file in the cloud
268
+ puter.fs.write('hello.txt', 'Hello, Puter!')
269
+ .then(file => console.log(\`File created at: \${file.path}\`));
270
+ &lt;/script>
271
+ </div>
272
+
273
+ <h2>💡 Key Features</h2>
274
+ <ul>
275
+ <li>Cloud Storage</li>
276
+ <li>AI Services (GPT-4, DALL-E)</li>
277
+ <li>Static Website Hosting</li>
278
+ <li>Key-Value Store</li>
279
+ <li>Authentication</li>
280
+ </ul>
281
+
282
+ <div class="links">
283
+ <a href="https://docs.puter.com" target="_blank">📚 Documentation</a>
284
+ <a href="https://discord.gg/puter" target="_blank">💬 Discord Community</a>
285
+ <a href="https://github.com/HeyPuter" target="_blank">👩‍💻 GitHub</a>
286
+ </div>
287
+ </div>
288
+
289
+ <footer class="footer">
290
+ &copy; 2025 ${appName}. All rights reserved.
291
+ </footer>
292
+ </body>
293
+ </html>`;
294
+
295
+ return defaultIndexContent;
296
+ }
@@ -0,0 +1,306 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import Conf from 'conf';
4
+ import { listApps, appInfo, createApp, updateApp, deleteApp } from './apps.js';
5
+ import { listSites, createSite, deleteSite, infoSite } from './sites.js';
6
+ import { listFiles, makeDirectory, renameFileOrDirectory,
7
+ removeFileOrDirectory, emptyTrash, changeDirectory, showCwd,
8
+ getInfo, getDiskUsage, createFile, readFile, uploadFile,
9
+ downloadFile, copyFile, syncDirectory } from './files.js';
10
+ import { getUserInfo, getUsageInfo } from './auth.js';
11
+ import { PROJECT_NAME, API_BASE, getHeaders } from './commons.js';
12
+ import inquirer from 'inquirer';
13
+ import { exec } from 'node:child_process';
14
+
15
+ const config = new Conf({ projectName: PROJECT_NAME });
16
+
17
+ /**
18
+ * Update the prompt function
19
+ * @returns The current prompt
20
+ */
21
+ export function getPrompt() {
22
+ return chalk.cyan(`puter@${config.get('cwd').slice(1)}> `);
23
+ }
24
+
25
+ const commands = {
26
+ help: showHelp,
27
+ exit: () => process.exit(0),
28
+ logout: async () => {
29
+ await import('./auth.js').then(m => m.logout());
30
+ process.exit(0);
31
+ },
32
+ whoami: getUserInfo,
33
+ stat: getInfo,
34
+ apps: async (args) => {
35
+ await listApps({
36
+ statsPeriod: args[0] || 'all'
37
+ });
38
+ },
39
+ app: appInfo,
40
+ 'app:create': async (args) => {
41
+ if (args.length < 1) {
42
+ console.log(chalk.red('Usage: app:create <name> [<remote_dir>] [--description=<description>] [--url=<url>]'));
43
+ return;
44
+ }
45
+ await createApp(args);
46
+ },
47
+ 'app:update': async (args) => {
48
+ if (args.length < 1) {
49
+ console.log(chalk.red('Usage: app:update <name> <remote_dir>'));
50
+ return;
51
+ }
52
+ await updateApp(args);
53
+ },
54
+ 'app:delete': async (args) => {
55
+ if (args.length < 1) {
56
+ console.log(chalk.red('Usage: app:delete <name>'));
57
+ return;
58
+ }
59
+ const name = args.find(arg => arg !=='-f')
60
+ const force = args.some(arg => arg =='-f')? true: false;
61
+
62
+ if (!force){
63
+ const { confirm } = await inquirer.prompt([
64
+ {
65
+ type: 'confirm',
66
+ name: 'confirm',
67
+ message: chalk.yellow(`Are you sure you want to delete "${name}"?`),
68
+ default: false
69
+ }
70
+ ]);
71
+ if (!confirm) {
72
+ console.log(chalk.yellow('Operation cancelled.'));
73
+ return false;
74
+ }
75
+ }
76
+ await deleteApp(name);
77
+ },
78
+ ls: listFiles,
79
+ cd: async (args) => {
80
+ await changeDirectory(args);
81
+ },
82
+ pwd: showCwd,
83
+ mkdir: makeDirectory,
84
+ mv: renameFileOrDirectory,
85
+ rm: removeFileOrDirectory,
86
+ // rmdir: deleteFolder, // Not implemented in Puter API
87
+ clean: emptyTrash,
88
+ df: getDiskUsage,
89
+ usage: getUsageInfo,
90
+ cp: copyFile,
91
+ touch: createFile,
92
+ cat: readFile,
93
+ push: uploadFile,
94
+ pull: downloadFile,
95
+ update: syncDirectory,
96
+ sites: listSites,
97
+ site: infoSite,
98
+ 'site:delete': deleteSite,
99
+ 'site:create': createSite
100
+ };
101
+
102
+ /**
103
+ * Execute a command
104
+ * @param {string} input The command line input
105
+ */
106
+ export async function execCommand(input) {
107
+ const [cmd, ...args] = input.split(' ');
108
+
109
+ if (cmd === 'help') {
110
+ // Handle help command
111
+ const command = args[0];
112
+ showHelp(command);
113
+ } else if (cmd.startsWith('!')) {
114
+ // Execute the command on the host machine
115
+ const hostCommand = input.slice(1); // Remove the "!"
116
+ exec(hostCommand, (error, stdout, stderr) => {
117
+ if (error) {
118
+ console.error(chalk.red(`Host Error: ${error.message}`));
119
+ return;
120
+ }
121
+ if (stderr) {
122
+ console.error(chalk.red(stderr));
123
+ return;
124
+ }
125
+ console.log(stdout);
126
+ console.log(chalk.green(`Press <Enter> to return.`));
127
+ });
128
+ } else if (commands[cmd]) {
129
+ try {
130
+ await commands[cmd](args);
131
+ } catch (error) {
132
+ console.error(chalk.red(`Error executing command: ${error.message}`));
133
+ }
134
+ } else {
135
+ if (!['Y', 'N'].includes(cmd.toUpperCase()[0])) {
136
+ console.log(chalk.red(`Unknown command: ${cmd}`));
137
+ showHelp();
138
+ }
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Display help for a specific command or general help if no command is provided.
144
+ * @param {string} [command] - The command to display help for.
145
+ */
146
+ function showHelp(command) {
147
+ const commandHelp = {
148
+ help: `
149
+ ${chalk.cyan('help [command]')}
150
+ Display help for a specific command or show general help.
151
+ Example: help ls
152
+ `,
153
+ exit: `
154
+ ${chalk.cyan('exit')}
155
+ Exit the shell.
156
+ `,
157
+ logout: `
158
+ ${chalk.cyan('logout')}
159
+ Logout from Puter account.
160
+ `,
161
+ whoami: `
162
+ ${chalk.cyan('whoami')}
163
+ Show user information.
164
+ `,
165
+ stat: `
166
+ ${chalk.cyan('stat <path>')}
167
+ Show file or directory information.
168
+ Example: stat /path/to/file
169
+ `,
170
+ df: `
171
+ ${chalk.cyan('df')}
172
+ Show disk usage information.
173
+ `,
174
+ usage: `
175
+ ${chalk.cyan('usage')}
176
+ Show usage information.
177
+ `,
178
+ apps: `
179
+ ${chalk.cyan('apps [period]')}
180
+ List all your apps.
181
+ period: today, yesterday, 7d, 30d, this_month, last_month
182
+ Example: apps today
183
+ `,
184
+ app: `
185
+ ${chalk.cyan('app <app_name>')}
186
+ Get application information.
187
+ Example: app myapp
188
+ `,
189
+ 'app:create': `
190
+ ${chalk.cyan('app:create <name> [url]')}
191
+ Create a new app.
192
+ Example: app:create myapp https://example.com
193
+ `,
194
+ 'app:update': `
195
+ ${chalk.cyan('app:update <name> [dir]')}
196
+ Update an app.
197
+ Example: app:update myapp .
198
+ `,
199
+ 'app:delete': `
200
+ ${chalk.cyan('app:delete <name>')}
201
+ Delete an app.
202
+ Example: app:delete myapp
203
+ `,
204
+ ls: `
205
+ ${chalk.cyan('ls [dir]')}
206
+ List files and directories.
207
+ Example: ls /path/to/dir
208
+ `,
209
+ cd: `
210
+ ${chalk.cyan('cd [dir]')}
211
+ Change the current working directory.
212
+ Example: cd /path/to/dir
213
+ `,
214
+ pwd: `
215
+ ${chalk.cyan('pwd')}
216
+ Print the current working directory.
217
+ `,
218
+ mkdir: `
219
+ ${chalk.cyan('mkdir <dir>')}
220
+ Create a new directory.
221
+ Example: mkdir /path/to/newdir
222
+ `,
223
+ mv: `
224
+ ${chalk.cyan('mv <src> <dest>')}
225
+ Move or rename a file or directory.
226
+ Example: mv /path/to/src /path/to/dest
227
+ `,
228
+ rm: `
229
+ ${chalk.cyan('rm <file>')}
230
+ Move a file or directory to the system's Trash.
231
+ Example: rm /path/to/file
232
+ `,
233
+ clean: `
234
+ ${chalk.cyan('clean')}
235
+ Empty the system's Trash.
236
+ `,
237
+ cp: `
238
+ ${chalk.cyan('cp <src> <dest>')}
239
+ Copy files or directories.
240
+ Example: cp /path/to/src /path/to/dest
241
+ `,
242
+ touch: `
243
+ ${chalk.cyan('touch <file>')}
244
+ Create a new empty file.
245
+ Example: touch /path/to/file
246
+ `,
247
+ cat: `
248
+ ${chalk.cyan('cat <file>')}
249
+ Output file content to the console.
250
+ Example: cat /path/to/file
251
+ `,
252
+ push: `
253
+ ${chalk.cyan('push <file>')}
254
+ Upload file to Puter cloud.
255
+ Example: push /path/to/file
256
+ `,
257
+ pull: `
258
+ ${chalk.cyan('pull <file>')}
259
+ Download file from Puter cloud.
260
+ Example: pull /path/to/file
261
+ `,
262
+ update: `
263
+ ${chalk.cyan('update <src> <dest>')}
264
+ Sync local directory with remote cloud.
265
+ Example: update /local/path /remote/path
266
+ `,
267
+ sites: `
268
+ ${chalk.cyan('sites')}
269
+ List sites and subdomains.
270
+ `,
271
+ site: `
272
+ ${chalk.cyan('site <site_uid>')}
273
+ Get site information by UID.
274
+ Example: site sd-123456
275
+ `,
276
+ 'site:delete': `
277
+ ${chalk.cyan('site:delete <uid>')}
278
+ Delete a site by UID.
279
+ Example: site:delete sd-123456
280
+ `,
281
+ 'site:create': `
282
+ ${chalk.cyan('site:create <dir> [--subdomain=<name>]')}
283
+ Create a static website from directory.
284
+ Example: site:create /path/to/dir --subdomain=myapp
285
+ `,
286
+ '!': `
287
+ ${chalk.cyan('!<command>')}
288
+ Execute a command on the host machine.
289
+ Example: !ls -la
290
+ `,
291
+ };
292
+
293
+ if (command && commandHelp[command]) {
294
+ console.log(chalk.yellow(`\nHelp for command: ${chalk.cyan(command)}`));
295
+ console.log(commandHelp[command]);
296
+ } else if (command) {
297
+ console.log(chalk.red(`Unknown command: ${command}`));
298
+ console.log(chalk.yellow('Use "help" to see a list of available commands.'));
299
+ } else {
300
+ console.log(chalk.yellow('\nAvailable commands:'));
301
+ for (const cmd in commandHelp) {
302
+ console.log(chalk.cyan(cmd.padEnd(20)) + '- ' + commandHelp[cmd].split('\n')[2].trim());
303
+ }
304
+ console.log(chalk.yellow('\nUse "help <command>" for detailed help on a specific command.'));
305
+ }
306
+ }