@heyputer/shell 2.1.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/src/commons.js ADDED
@@ -0,0 +1,409 @@
1
+ import chalk from 'chalk';
2
+ import { getAuthToken } from './commands/auth.js';
3
+ import { formatSize } from './utils.js';
4
+ import { readFile } from 'fs/promises';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname, join } from 'path';
7
+ import dotenv from 'dotenv';
8
+
9
+ dotenv.config();
10
+
11
+ export const PROJECT_NAME = 'puter-cli';
12
+ // If you haven't defined your own values in .env file, we'll assume you're running Puter on a local instance:
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
+ };
21
+
22
+ /**
23
+ * Get headers with the correct Content-Type for multipart form data.
24
+ * @param {string} contentType - The "Content-Type" argument for the header ('application/json' is the default)
25
+ * Use the multipart form data for upload a file.
26
+ * @returns {Object} The headers object.
27
+ */
28
+ export function getHeaders(contentType = 'application/json') {
29
+ return {
30
+ 'Accept': '*/*',
31
+ 'Accept-Language': 'en-US,en;q=0.9',
32
+ 'Authorization': `Bearer ${getAuthToken()}`,
33
+ 'Connection': 'keep-alive',
34
+ // 'Host': 'api.puter.com',
35
+ 'Content-Type': contentType,
36
+ 'Origin': `${BASE_URL}`,
37
+ 'Referer': `${BASE_URL}/`,
38
+ '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'
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Generate a random app name
44
+ * @returns a random app name or null if it fails
45
+ * @see: [randName](https://github.com/HeyPuter/puter/blob/06a67a3b223a6cbd7ec2e16853b6d2304f621a88/src/puter-js/src/index.js#L389)
46
+ */
47
+ export function generateAppName(separateWith = '-'){
48
+ console.log(chalk.cyan('Generating random name...'));
49
+ try {
50
+ const first_adj = ['helpful','sensible', 'loyal', 'honest', 'clever', 'capable','calm', 'smart', 'genius', 'bright', 'charming', 'creative', 'diligent', 'elegant', 'fancy',
51
+ 'colorful', 'avid', 'active', 'gentle', 'happy', 'intelligent', 'jolly', 'kind', 'lively', 'merry', 'nice', 'optimistic', 'polite',
52
+ 'quiet', 'relaxed', 'silly', 'victorious', 'witty', 'young', 'zealous', 'strong', 'brave', 'agile', 'bold'];
53
+
54
+ const nouns = ['street', 'roof', 'floor', 'tv', 'idea', 'morning', 'game', 'wheel', 'shoe', 'bag', 'clock', 'pencil', 'pen',
55
+ 'magnet', 'chair', 'table', 'house', 'dog', 'room', 'book', 'car', 'cat', 'tree',
56
+ 'flower', 'bird', 'fish', 'sun', 'moon', 'star', 'cloud', 'rain', 'snow', 'wind', 'mountain',
57
+ 'river', 'lake', 'sea', 'ocean', 'island', 'bridge', 'road', 'train', 'plane', 'ship', 'bicycle',
58
+ 'horse', 'elephant', 'lion', 'tiger', 'bear', 'zebra', 'giraffe', 'monkey', 'snake', 'rabbit', 'duck',
59
+ 'goose', 'penguin', 'frog', 'crab', 'shrimp', 'whale', 'octopus', 'spider', 'ant', 'bee', 'butterfly', 'dragonfly',
60
+ 'ladybug', 'snail', 'camel', 'kangaroo', 'koala', 'panda', 'piglet', 'sheep', 'wolf', 'fox', 'deer', 'mouse', 'seal',
61
+ 'chicken', 'cow', 'dinosaur', 'puppy', 'kitten', 'circle', 'square', 'garden', 'otter', 'bunny', 'meerkat', 'harp']
62
+
63
+ // return a random combination of first_adj + noun + number (between 0 and 9999)
64
+ // e.g. clever-idea-123
65
+ 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);
66
+ console.log(chalk.green(`Name: "${appName}"`));
67
+ return appName;
68
+ } catch (error) {
69
+ console.error(`Error: ${error.message}`);
70
+ return null;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Display data in a structured format
76
+ * @param {Array} data - The data to display
77
+ * @param {Object} options - Display options
78
+ * @param {Array} options.headers - Headers for the table
79
+ * @param {Array} options.columns - Columns to display
80
+ * @param {number} options.columnWidth - Width of each column
81
+ */
82
+ export function displayTable(data, options = {}) {
83
+ const { headers = [], columns = [], columnWidth = 20 } = options;
84
+
85
+ // Create the header row
86
+ const headerRow = headers.map(header => chalk.cyan(header.padEnd(columnWidth))).join(' | ');
87
+ console.log(headerRow);
88
+ console.log(chalk.dim('-'.repeat(headerRow.length)));
89
+
90
+ // Create and display each row of data
91
+ data.forEach(item => {
92
+ const row = columns.map(col => {
93
+ const value = item[col] || 'N/A';
94
+ return value.toString().padEnd(columnWidth);
95
+ }).join(' | ');
96
+ console.log(row);
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Display structured ouput of disk usage informations
102
+ */
103
+ export function showDiskSpaceUsage(data) {
104
+ const freeSpace = parseInt(data.capacity) - parseInt(data.used);
105
+ const usagePercentage = (parseInt(data.used) / parseInt(data.capacity)) * 100;
106
+ console.log(chalk.cyan('Disk Usage Information:'));
107
+ console.log(chalk.dim('----------------------------------------'));
108
+ console.log(chalk.cyan(`Total Capacity: `) + chalk.white(formatSize(data.capacity)));
109
+ console.log(chalk.cyan(`Used Space: `) + chalk.white(formatSize(data.used)));
110
+ console.log(chalk.cyan(`Free Space: `) + chalk.white(formatSize(freeSpace)));
111
+ // format the usagePercentage with 2 decimal floating point value:
112
+ console.log(chalk.cyan(`Usage Percentage: `) + chalk.white(`${usagePercentage.toFixed(2)}%`));
113
+ console.log(chalk.dim('----------------------------------------'));
114
+ }
115
+
116
+ /**
117
+ * Resolve a relative path to an absolute path
118
+ * @param {string} currentPath - The current working directory
119
+ * @param {string} relativePath - The relative path to resolve
120
+ * @returns {string} The resolved absolute path
121
+ */
122
+ export function resolvePath(currentPath, relativePath) {
123
+ // Normalize the current path (remove trailing slashes)
124
+ currentPath = currentPath.replace(/\/+$/, '');
125
+
126
+ // Split the relative path into parts
127
+ const parts = relativePath.split('/').filter(p => p); // Remove empty parts
128
+
129
+ // Handle each part of the relative path
130
+ for (const part of parts) {
131
+ if (part === '..') {
132
+ // Move one level up
133
+ const currentParts = currentPath.split('/').filter(p => p);
134
+ if (currentParts.length > 0) {
135
+ currentParts.pop(); // Remove the last part
136
+ }
137
+ currentPath = '/' + currentParts.join('/');
138
+ } else if (part === '.') {
139
+ // Stay in the current directory (no change)
140
+ continue;
141
+ } else {
142
+ // Move into a subdirectory
143
+ currentPath += `/${part}`;
144
+ }
145
+ }
146
+
147
+ // Normalize the final path (remove duplicate slashes)
148
+ currentPath = currentPath.replace(/\/+/g, '/');
149
+
150
+ // Ensure the path ends with a slash if it's the root
151
+ if (currentPath === '') {
152
+ currentPath = '/';
153
+ }
154
+
155
+ return currentPath;
156
+ }
157
+
158
+ /**
159
+ * Resolve a remote path to an absolute path, handling both absolute and relative paths.
160
+ * @param {string} currentPath - The current working directory.
161
+ * @param {string} remotePath - The remote path to resolve.
162
+ * @returns {string} The resolved absolute path.
163
+ */
164
+ export function resolveRemotePath(currentPath, remotePath) {
165
+ if (remotePath.startsWith('/')) {
166
+ return remotePath;
167
+ }
168
+ return resolvePath(currentPath, remotePath);
169
+ }
170
+
171
+ /**
172
+ * Checks if a given string is a valid app name.
173
+ * The name must:
174
+ * - Not be '.' or '..'
175
+ * - Not contain path separators ('/' or '\\')
176
+ * - Not contain wildcard characters ('*')
177
+ * - (Optional) Contain only allowed characters (letters, numbers, spaces, underscores, hyphens)
178
+ *
179
+ * @param {string} name - The app name to validate.
180
+ * @returns {boolean} - Returns true if valid, false otherwise.
181
+ */
182
+ export function isValidAppName(name) {
183
+ // Ensure the name is a non-empty string
184
+ if (typeof name !== 'string' || name.trim().length === 0) {
185
+ return false;
186
+ }
187
+
188
+ // Trim whitespace from both ends
189
+ const trimmedName = name.trim();
190
+
191
+ // Reject reserved names
192
+ if (trimmedName === '.' || trimmedName === '..') {
193
+ return false;
194
+ }
195
+
196
+ // Regex patterns for invalid characters
197
+ const invalidPattern = /[\/\\*]/; // Disallow /, \, and *
198
+
199
+ if (invalidPattern.test(trimmedName)) {
200
+ return false;
201
+ }
202
+
203
+ // Optional: Define allowed characters pattern
204
+ // Uncomment the following lines if you want to enforce allowed characters
205
+ /*
206
+ const allowedPattern = /^[A-Za-z0-9 _-]+$/;
207
+ if (!allowedPattern.test(trimmedName)) {
208
+ return false;
209
+ }
210
+ */
211
+
212
+ // All checks passed
213
+ return true;
214
+ }
215
+
216
+ /**
217
+ * Generate the default home page for a new web application
218
+ * @param {string} appName The name of the web application
219
+ * @returns HTML template of the app
220
+ */
221
+ export function getDefaultHomePage(appName, jsFiles = [], cssFiles= []) {
222
+ const defaultIndexContent = `<!DOCTYPE html>
223
+ <html lang="en">
224
+ <head>
225
+ <meta charset="UTF-8">
226
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
227
+ <title>${appName}</title>
228
+ ${cssFiles.map(css => `<link href="${css}" rel="stylesheet">`).join('\n ')}
229
+ <style>
230
+ body {
231
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
232
+ line-height: 1.6;
233
+ max-width: 800px;
234
+ margin: 0 auto;
235
+ padding: 20px;
236
+ background: #f9fafb;
237
+ color: #1f2937;
238
+ }
239
+ .container {
240
+ background: white;
241
+ padding: 2rem;
242
+ border-radius: 8px;
243
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
244
+ }
245
+ h1 {
246
+ color: #2563eb;
247
+ margin-bottom: 1rem;
248
+ }
249
+ .code-block {
250
+ background: #f1f5f9;
251
+ padding: 1rem;
252
+ border-radius: 4px;
253
+ font-family: monospace;
254
+ overflow-x: auto;
255
+ }
256
+ .tip {
257
+ background: #dbeafe;
258
+ border-left: 4px solid #2563eb;
259
+ padding: 1rem;
260
+ margin: 1rem 0;
261
+ }
262
+ .links {
263
+ display: flex;
264
+ gap: 1rem;
265
+ margin-top: 2rem;
266
+ }
267
+ .links a {
268
+ color: #2563eb;
269
+ text-decoration: none;
270
+ }
271
+ .links a:hover {
272
+ text-decoration: underline;
273
+ }
274
+ .footer {
275
+ text-align: center;
276
+ margin-top: 50px;
277
+ color: var(--color-grey);
278
+ font-size: 0.9rem;
279
+ }
280
+ </style>
281
+ </head>
282
+ <body>
283
+ <div class="container">
284
+ <h1>🚀 Welcome to ${appName}!</h1>
285
+
286
+ <p>This is your new website powered by Puter. You can start customizing it right away!</p>
287
+
288
+ <div class="tip">
289
+ <strong>Quick Tip:</strong> Replace this content with your own by editing the <code>index.html</code> file.
290
+ </div>
291
+
292
+ <h2>🌟 Getting Started</h2>
293
+
294
+ <p>Here's a simple example using Puter.js:</p>
295
+
296
+ <div class="code-block">
297
+ &lt;script src="https://js.puter.com/v2/">&lt;/script>
298
+ &lt;script>
299
+ // Create a new file in the cloud
300
+ puter.fs.write('hello.txt', 'Hello, Puter!')
301
+ .then(file => console.log(\`File created at: \${file.path}\`));
302
+ &lt;/script>
303
+ </div>
304
+
305
+ <h2>💡 Key Features</h2>
306
+ <ul>
307
+ <li>Cloud Storage</li>
308
+ <li>AI Services (GPT-4, DALL-E)</li>
309
+ <li>Static Website Hosting</li>
310
+ <li>Key-Value Store</li>
311
+ <li>Authentication</li>
312
+ </ul>
313
+
314
+ <div class="links">
315
+ <a href="https://docs.puter.com" target="_blank">📚 Documentation</a>
316
+ <a href="https://discord.gg/puter" target="_blank">💬 Discord Community</a>
317
+ <a href="https://github.com/HeyPuter" target="_blank">👩‍💻 GitHub</a>
318
+ </div>
319
+ </div>
320
+
321
+ <footer class="footer">
322
+ &copy; 2025 ${appName}. All rights reserved.
323
+ </footer>
324
+
325
+ <div id="${(jsFiles.length && jsFiles.some(f => f.includes('react'))) ? 'root' : 'app'}"></div>
326
+ ${jsFiles.map(js =>
327
+ `<script ${js.endsWith('app.js') ? 'type="text/babel"' : ''} src="${js}"></script>`
328
+ ).join('\n ')}
329
+ </body>
330
+ </html>`;
331
+
332
+ return defaultIndexContent;
333
+ }
334
+
335
+
336
+ /**
337
+ * Read latest package from package file
338
+ */
339
+ export async function getVersionFromPackage() {
340
+ try {
341
+ const __filename = fileURLToPath(import.meta.url);
342
+ const __dirname = dirname(__filename);
343
+
344
+ // First try parent directory (dev mode)
345
+ try {
346
+ const devPackage = JSON.parse(
347
+ await readFile(join(__dirname, '..', 'package.json'), 'utf8')
348
+ );
349
+ return devPackage.version;
350
+ } catch (devError) {
351
+ // Fallback to current directory (production)
352
+ const prodPackage = JSON.parse(
353
+ await readFile(join(__dirname, 'package.json'), 'utf8')
354
+ );
355
+ return prodPackage.version;
356
+ }
357
+ } catch (error) {
358
+ console.error(`Error fetching latest version:`, error.message);
359
+ return null;
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Get latest package info from npm registery
365
+ */
366
+ export async function getLatestVersion(packageName) {
367
+ let currentVersion = 'unknown';
368
+ let latestVersion = null;
369
+ let status = 'offline'; // Default status
370
+
371
+ try {
372
+ // Attempt to get the current version first
373
+ currentVersion = await getVersionFromPackage();
374
+ if (!currentVersion) {
375
+ currentVersion = 'unknown'; // Fallback if local version fetch fails
376
+ }
377
+
378
+ // Attempt to fetch the latest version from npm
379
+ try {
380
+ const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
381
+ if (response.ok) {
382
+ const data = await response.json();
383
+ latestVersion = data.version;
384
+ }
385
+ } catch (fetchError) {
386
+ // Ignore fetch errors
387
+ // console.warn(chalk.yellow(`Could not fetch latest version for ${packageName}: ${fetchError.message}`));
388
+ }
389
+
390
+ // Determine the status based on fetched versions
391
+ if (latestVersion) {
392
+ if (currentVersion !== 'unknown' && latestVersion === currentVersion) {
393
+ status = 'up-to-date';
394
+ } else if (currentVersion !== 'unknown' && latestVersion !== currentVersion) {
395
+ status = `latest: ${latestVersion}`;
396
+ } else {
397
+ // If currentVersion is unknown but we got latest, show latest
398
+ status = `latest: ${latestVersion}`;
399
+ }
400
+ }
401
+ // status remains 'offline'...
402
+
403
+ } catch (error) {
404
+ // Catch errors from getVersionFromPackage or other unexpected issues
405
+ console.error(chalk.red(`Error determining version status: ${error.message}`));
406
+ status = 'error'; // Indicate an error occurred
407
+ }
408
+ return `v${currentVersion} (${status})`;
409
+ }
package/src/crypto.js ADDED
@@ -0,0 +1,9 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+
3
+ const randomUUID = () => {
4
+ return uuidv4();
5
+ };
6
+
7
+ export default {
8
+ randomUUID
9
+ };