@heyputer/shell 2.1.0 → 3.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/.github/workflows/npm-build.yml +0 -2
- package/.github/workflows/npm-publish.yml +0 -4
- package/bin/index.js +22 -202
- package/package.json +3 -5
- package/src/commands/auth.js +7 -12
- package/src/commands/files.js +16 -18
- package/src/commands/shell.js +5 -3
- package/src/commons.js +81 -256
- package/src/executor.js +12 -129
- package/src/modules/ErrorModule.js +62 -1
- package/src/modules/ProfileModule.js +154 -38
- package/src/utils.js +0 -91
- package/tests/ErrorModule.test.js +73 -1
- package/tests/ProfileModule.test.js +58 -44
- package/tests/commons.test.js +66 -124
- package/tests/executor.test.js +8 -0
- package/tests/files.test.js +23 -3
- package/tests/shell.test.js +2 -1
- package/tests/utils.test.js +1 -141
- package/src/commands/apps.js +0 -356
- package/src/commands/deploy.js +0 -54
- package/src/commands/init.js +0 -322
- package/src/commands/sites.js +0 -169
- package/src/commands/subdomains.js +0 -95
- package/tests/apps.test.js +0 -194
- package/tests/deploy.test.js +0 -84
- package/tests/sites.test.js +0 -67
- package/tests/subdomains.test.js +0 -90
package/src/commons.js
CHANGED
|
@@ -8,11 +8,10 @@ import dotenv from 'dotenv';
|
|
|
8
8
|
|
|
9
9
|
dotenv.config();
|
|
10
10
|
|
|
11
|
-
export const PROJECT_NAME = 'puter-
|
|
11
|
+
export const PROJECT_NAME = 'puter-sh';
|
|
12
12
|
// If you haven't defined your own values in .env file, we'll assume you're running Puter on a local instance:
|
|
13
13
|
export let API_BASE = process.env.PUTER_API_BASE || 'https://api.puter.com';
|
|
14
14
|
export let BASE_URL = process.env.PUTER_BASE_URL || 'https://puter.com';
|
|
15
|
-
export const NULL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
16
15
|
|
|
17
16
|
export const reconfigureURLs = ({ api, base }) => {
|
|
18
17
|
API_BASE = api;
|
|
@@ -39,64 +38,6 @@ export function getHeaders(contentType = 'application/json') {
|
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
40
|
|
|
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
41
|
/**
|
|
101
42
|
* Display structured ouput of disk usage informations
|
|
102
43
|
*/
|
|
@@ -114,225 +55,109 @@ export function showDiskSpaceUsage(data) {
|
|
|
114
55
|
}
|
|
115
56
|
|
|
116
57
|
/**
|
|
117
|
-
*
|
|
58
|
+
* The home anchor as typed by the user.
|
|
59
|
+
*/
|
|
60
|
+
export const HOME = '~';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The concrete home directory ("/<username>") for the active profile.
|
|
64
|
+
*
|
|
65
|
+
* Resolved from the token at login and refreshed on every run -- never read
|
|
66
|
+
* from a cached username -- so it always names the account's current home.
|
|
67
|
+
* Null until a profile is selected.
|
|
68
|
+
*/
|
|
69
|
+
export let HOME_PATH = null;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Point the home anchor at the current account's home directory.
|
|
73
|
+
* @param {string} homePath - The concrete home path, e.g. "/alice"
|
|
74
|
+
*/
|
|
75
|
+
export const setHomePath = (homePath) => {
|
|
76
|
+
HOME_PATH = homePath;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Expand a leading "~" to the resolved home directory. Paths that are not
|
|
81
|
+
* home-anchored are returned untouched, as are all paths when no profile is
|
|
82
|
+
* active yet (nothing to expand against).
|
|
83
|
+
* @param {string} p - The path to expand
|
|
84
|
+
* @returns {string} The expanded path
|
|
85
|
+
*/
|
|
86
|
+
export function expandHome(p) {
|
|
87
|
+
if (!HOME_PATH || typeof p !== 'string') return p;
|
|
88
|
+
if (p === HOME) return HOME_PATH;
|
|
89
|
+
if (p.startsWith('~/')) return `${HOME_PATH}${p.slice(1)}`;
|
|
90
|
+
return p;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check whether a path is already fully-qualified (root- or home-anchored) and
|
|
95
|
+
* therefore must NOT be resolved against the current working directory.
|
|
96
|
+
* @param {string} p - The path to test
|
|
97
|
+
* @returns {boolean} True if the path is absolute or home-anchored
|
|
98
|
+
*/
|
|
99
|
+
export function isAbsolutePath(p) {
|
|
100
|
+
if (typeof p !== 'string') return false;
|
|
101
|
+
return p === HOME || p.startsWith('~/') || p.startsWith('/');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a path against the current working directory.
|
|
106
|
+
*
|
|
107
|
+
* If `relativePath` is itself absolute ("/...") or home-anchored ("~", "~/..."),
|
|
108
|
+
* it replaces `currentPath` entirely instead of being appended to it.
|
|
109
|
+
*
|
|
118
110
|
* @param {string} currentPath - The current working directory
|
|
119
|
-
* @param {string} relativePath - The
|
|
120
|
-
* @returns {string} The resolved
|
|
111
|
+
* @param {string} relativePath - The path to resolve
|
|
112
|
+
* @returns {string} The resolved path, preserving a "~" root if present
|
|
121
113
|
*/
|
|
122
114
|
export function resolvePath(currentPath, relativePath) {
|
|
123
|
-
//
|
|
124
|
-
|
|
115
|
+
// A fully-qualified path re-roots the resolution rather than extending it.
|
|
116
|
+
if (isAbsolutePath(relativePath)) {
|
|
117
|
+
currentPath = relativePath.startsWith('~') ? HOME : '/';
|
|
118
|
+
relativePath = relativePath.replace(/^~/, '');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Track whether we are anchored at home so "~" survives normalization.
|
|
122
|
+
const atHome = currentPath === HOME || currentPath.startsWith('~/');
|
|
123
|
+
const root = atHome ? HOME : '';
|
|
125
124
|
|
|
126
|
-
//
|
|
127
|
-
|
|
125
|
+
// Strip the root and any trailing slashes, leaving bare segments.
|
|
126
|
+
let parts = currentPath
|
|
127
|
+
.replace(/^~/, '')
|
|
128
|
+
.split('/')
|
|
129
|
+
.filter(p => p);
|
|
128
130
|
|
|
129
|
-
|
|
130
|
-
for (const part of parts) {
|
|
131
|
+
for (const part of relativePath.split('/').filter(p => p)) {
|
|
131
132
|
if (part === '..') {
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
if (currentParts.length > 0) {
|
|
135
|
-
currentParts.pop(); // Remove the last part
|
|
136
|
-
}
|
|
137
|
-
currentPath = '/' + currentParts.join('/');
|
|
133
|
+
// Clamp at the root: "~/.." stays at home, "/.." stays at "/".
|
|
134
|
+
parts.pop();
|
|
138
135
|
} else if (part === '.') {
|
|
139
|
-
// Stay in the current directory (no change)
|
|
140
136
|
continue;
|
|
141
137
|
} else {
|
|
142
|
-
|
|
143
|
-
currentPath += `/${part}`;
|
|
138
|
+
parts.push(part);
|
|
144
139
|
}
|
|
145
140
|
}
|
|
146
141
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (currentPath === '') {
|
|
152
|
-
currentPath = '/';
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return currentPath;
|
|
142
|
+
const joined = parts.join('/');
|
|
143
|
+
// Expand "~" so callers and the API always see a concrete "/<username>" path.
|
|
144
|
+
if (!joined) return expandHome(root || '/');
|
|
145
|
+
return expandHome(`${root}/${joined}`);
|
|
156
146
|
}
|
|
157
147
|
|
|
158
148
|
/**
|
|
159
|
-
* Resolve a remote path to
|
|
149
|
+
* Resolve a remote path to a fully-qualified path.
|
|
160
150
|
* @param {string} currentPath - The current working directory.
|
|
161
151
|
* @param {string} remotePath - The remote path to resolve.
|
|
162
|
-
* @returns {string} The resolved
|
|
152
|
+
* @returns {string} The resolved path.
|
|
163
153
|
*/
|
|
164
154
|
export function resolveRemotePath(currentPath, remotePath) {
|
|
165
|
-
if (remotePath
|
|
166
|
-
return remotePath;
|
|
155
|
+
if (isAbsolutePath(remotePath)) {
|
|
156
|
+
return expandHome(remotePath);
|
|
167
157
|
}
|
|
168
158
|
return resolvePath(currentPath, remotePath);
|
|
169
159
|
}
|
|
170
160
|
|
|
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
|
-
<script src="https://js.puter.com/v2/"></script>
|
|
298
|
-
<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
|
-
</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
|
-
© 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
161
|
/**
|
|
337
162
|
* Read latest package from package file
|
|
338
163
|
*/
|
package/src/executor.js
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import Conf from 'conf';
|
|
3
|
-
import { listApps, appInfo, createApp, updateApp, deleteApp } from './commands/apps.js';
|
|
4
|
-
import { listSites, createSite, deleteSite, infoSite } from './commands/sites.js';
|
|
5
3
|
import {
|
|
6
4
|
listFiles, makeDirectory, renameFileOrDirectory,
|
|
7
5
|
removeFileOrDirectory, emptyTrash, changeDirectory, showCwd,
|
|
8
6
|
getInfo, getDiskUsage, createFile, readFile, uploadFile,
|
|
9
7
|
downloadFile, copyFile, syncDirectory, editFile
|
|
10
8
|
} from './commands/files.js';
|
|
11
|
-
import { getUserInfo, getUsageInfo, login } from './commands/auth.js';
|
|
12
|
-
import {
|
|
13
|
-
import { PROJECT_NAME, API_BASE, getHeaders } from './commons.js';
|
|
14
|
-
import inquirer from 'inquirer';
|
|
9
|
+
import { getUserInfo, getUsageInfo, login, getCurrentDirectory } from './commands/auth.js';
|
|
10
|
+
import { PROJECT_NAME, API_BASE, HOME, expandHome, getHeaders } from './commons.js';
|
|
15
11
|
import { exec } from 'node:child_process';
|
|
16
|
-
import {
|
|
12
|
+
import { getSystemEditor } from './utils.js';
|
|
17
13
|
import { rl } from './commands/shell.js';
|
|
18
|
-
import { showLast } from './modules/ErrorModule.js'
|
|
14
|
+
import { showLast, report, formatError, isAuthError } from './modules/ErrorModule.js'
|
|
19
15
|
|
|
20
16
|
const config = new Conf({ projectName: PROJECT_NAME });
|
|
21
17
|
|
|
@@ -27,7 +23,7 @@ const commandHistory = [];
|
|
|
27
23
|
* @returns The current prompt
|
|
28
24
|
*/
|
|
29
25
|
export function getPrompt() {
|
|
30
|
-
return chalk.cyan(`puter@${
|
|
26
|
+
return chalk.cyan(`puter@${getCurrentDirectory().slice(1)}> `);
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
const commands = {
|
|
@@ -40,12 +36,6 @@ const commands = {
|
|
|
40
36
|
login: login,
|
|
41
37
|
whoami: getUserInfo,
|
|
42
38
|
stat: getInfo,
|
|
43
|
-
apps: async (args) => {
|
|
44
|
-
await listApps({
|
|
45
|
-
statsPeriod: args[0] || 'all'
|
|
46
|
-
});
|
|
47
|
-
},
|
|
48
|
-
app: appInfo,
|
|
49
39
|
history: async (args) => {
|
|
50
40
|
const lineNumber = parseInt(args[0]);
|
|
51
41
|
|
|
@@ -67,64 +57,6 @@ const commands = {
|
|
|
67
57
|
}
|
|
68
58
|
},
|
|
69
59
|
'last-error': showLast,
|
|
70
|
-
'app:create': async (rawArgs) => {
|
|
71
|
-
try {
|
|
72
|
-
const args = parseArgs(rawArgs.join(' '));
|
|
73
|
-
// Consider using explicit argument definition if necessary
|
|
74
|
-
// const args = parseArgs(rawArgs.join(' '), {string: ['description', 'url'],
|
|
75
|
-
// alias: { d: 'description', u: 'url', },
|
|
76
|
-
// });
|
|
77
|
-
|
|
78
|
-
// NOTE: Keep the check for now at the function level, move the check here so in the future we'll use the function for non-interactive command mode.
|
|
79
|
-
await createApp({
|
|
80
|
-
name: args._[0],
|
|
81
|
-
directory: args._[1] || '',
|
|
82
|
-
description: args.description || '',
|
|
83
|
-
url: args.url || 'https://dev-center.puter.com/coming-soon.html'
|
|
84
|
-
});
|
|
85
|
-
} catch (error) {
|
|
86
|
-
console.error(chalk.red(error.message));
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
'app:update': async (args) => {
|
|
90
|
-
if (args.length < 1) {
|
|
91
|
-
console.log(chalk.red('Usage: app:update <name> <remote_dir>'));
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
await updateApp(args);
|
|
95
|
-
},
|
|
96
|
-
'app:delete': async (rawArgs) => {
|
|
97
|
-
const args = parseArgs(rawArgs.join(' '), {
|
|
98
|
-
string: ['_'],
|
|
99
|
-
boolean: ['f'],
|
|
100
|
-
configuration: {
|
|
101
|
-
'populate--': true
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
if (args._.length < 1) {
|
|
105
|
-
console.log(chalk.red('You must specify the app name:'));
|
|
106
|
-
console.log(chalk.yellow('Example: app:delete <name>'));
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const name = args._[0];
|
|
110
|
-
const force = !!args.f;
|
|
111
|
-
|
|
112
|
-
if (!force) {
|
|
113
|
-
const { confirm } = await inquirer.prompt([
|
|
114
|
-
{
|
|
115
|
-
type: 'confirm',
|
|
116
|
-
name: 'confirm',
|
|
117
|
-
message: chalk.yellow(`Are you sure you want to delete "${name}"?`),
|
|
118
|
-
default: false
|
|
119
|
-
}
|
|
120
|
-
]);
|
|
121
|
-
if (!confirm) {
|
|
122
|
-
console.log(chalk.yellow('Operation cancelled.'));
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
await deleteApp(name);
|
|
127
|
-
},
|
|
128
60
|
ls: listFiles,
|
|
129
61
|
cd: async (args) => {
|
|
130
62
|
await changeDirectory(args);
|
|
@@ -144,11 +76,6 @@ const commands = {
|
|
|
144
76
|
pull: downloadFile,
|
|
145
77
|
update: syncDirectory,
|
|
146
78
|
edit: editFile,
|
|
147
|
-
sites: listSites,
|
|
148
|
-
site: infoSite,
|
|
149
|
-
'site:delete': deleteSite,
|
|
150
|
-
'site:create': createSite,
|
|
151
|
-
'site:deploy': deploy,
|
|
152
79
|
};
|
|
153
80
|
|
|
154
81
|
/**
|
|
@@ -190,7 +117,13 @@ export async function execCommand(input) {
|
|
|
190
117
|
try {
|
|
191
118
|
await commands[cmd](args);
|
|
192
119
|
} catch (error) {
|
|
193
|
-
|
|
120
|
+
report(error);
|
|
121
|
+
if (isAuthError(error)) {
|
|
122
|
+
console.error(chalk.red('Your session has expired or its token is no longer valid.'));
|
|
123
|
+
console.error(chalk.cyan('Type "login" to sign in again.'));
|
|
124
|
+
} else {
|
|
125
|
+
console.error(chalk.red(`Error executing command: ${formatError(error)}`));
|
|
126
|
+
}
|
|
194
127
|
}
|
|
195
128
|
return;
|
|
196
129
|
}
|
|
@@ -238,32 +171,6 @@ function showHelp(command) {
|
|
|
238
171
|
${chalk.cyan('usage')}
|
|
239
172
|
Show usage information.
|
|
240
173
|
`,
|
|
241
|
-
apps: `
|
|
242
|
-
${chalk.cyan('apps [period]')}
|
|
243
|
-
List all your apps.
|
|
244
|
-
period: today, yesterday, 7d, 30d, this_month, last_month
|
|
245
|
-
Example: apps today
|
|
246
|
-
`,
|
|
247
|
-
app: `
|
|
248
|
-
${chalk.cyan('app <app_name>')}
|
|
249
|
-
Get application information.
|
|
250
|
-
Example: app myapp
|
|
251
|
-
`,
|
|
252
|
-
'app:create': `
|
|
253
|
-
${chalk.cyan('app:create <name> <remote_dir>')}
|
|
254
|
-
Create a new app.
|
|
255
|
-
Example: app:create myapp https://myapp.puter.site
|
|
256
|
-
`,
|
|
257
|
-
'app:update': `
|
|
258
|
-
${chalk.cyan('app:update <name> [dir]')}
|
|
259
|
-
Update an app.
|
|
260
|
-
Example: app:update myapp .
|
|
261
|
-
`,
|
|
262
|
-
'app:delete': `
|
|
263
|
-
${chalk.cyan('app:delete <name>')}
|
|
264
|
-
Delete an app.
|
|
265
|
-
Example: app:delete myapp
|
|
266
|
-
`,
|
|
267
174
|
ls: `
|
|
268
175
|
${chalk.cyan('ls [dir]')}
|
|
269
176
|
List files and directories.
|
|
@@ -334,30 +241,6 @@ function showHelp(command) {
|
|
|
334
241
|
|
|
335
242
|
System editor: ${chalk.green(getSystemEditor())}
|
|
336
243
|
`,
|
|
337
|
-
sites: `
|
|
338
|
-
${chalk.cyan('sites')}
|
|
339
|
-
List sites and subdomains.
|
|
340
|
-
`,
|
|
341
|
-
site: `
|
|
342
|
-
${chalk.cyan('site <site_uid>')}
|
|
343
|
-
Get site information by UID.
|
|
344
|
-
Example: site sd-123456
|
|
345
|
-
`,
|
|
346
|
-
'site:delete': `
|
|
347
|
-
${chalk.cyan('site:delete <uid>')}
|
|
348
|
-
Delete a site by UID.
|
|
349
|
-
Example: site:delete sd-123456
|
|
350
|
-
`,
|
|
351
|
-
'site:create': `
|
|
352
|
-
${chalk.cyan('site:create <app_name> [<dir>] [--subdomain=<name>]')}
|
|
353
|
-
Create a static website from directory.
|
|
354
|
-
Example: site:create mywebsite /path/to/dir --subdomain=mywebsite
|
|
355
|
-
`,
|
|
356
|
-
'site:deploy': `
|
|
357
|
-
${chalk.cyan('site:deploy [<remote_dir>] [--subdomain=<subdomain>]')}
|
|
358
|
-
Deploy a local web project to Puter.
|
|
359
|
-
Example: site:deploy ./my-app --subdomain my-app
|
|
360
|
-
`,
|
|
361
244
|
'!': `
|
|
362
245
|
${chalk.cyan('!<command>')}
|
|
363
246
|
Execute a command on the host machine.
|
|
@@ -17,4 +17,65 @@ export const showLast = () => {
|
|
|
17
17
|
} else {
|
|
18
18
|
console.log('No errors to report');
|
|
19
19
|
}
|
|
20
|
-
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The Puter API rejects with plain objects rather than Error instances, e.g.
|
|
24
|
+
* `{ status: 401, message: 'Unauthorized' }`. Node renders such a value as the
|
|
25
|
+
* useless "#<Object>", so normalize everything to a real Error before it is
|
|
26
|
+
* shown or rethrown.
|
|
27
|
+
*
|
|
28
|
+
* @param {*} error - Any thrown or rejected value
|
|
29
|
+
* @returns {Error} An Error carrying the original `status`/`code` when present
|
|
30
|
+
*/
|
|
31
|
+
export const normalizeError = (error) => {
|
|
32
|
+
if (error instanceof Error) return error;
|
|
33
|
+
|
|
34
|
+
if (error && typeof error === 'object') {
|
|
35
|
+
// Puter nests driver failures one level down as `{ error: { code, message } }`.
|
|
36
|
+
const detail = error.error && typeof error.error === 'object' ? error.error : error;
|
|
37
|
+
const message = detail.message || detail.code || JSON.stringify(error);
|
|
38
|
+
const normalized = new Error(message);
|
|
39
|
+
if (detail.code !== undefined) normalized.code = detail.code;
|
|
40
|
+
if (error.status !== undefined) normalized.status = error.status;
|
|
41
|
+
return normalized;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return new Error(String(error ?? 'Unknown error'));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Server codes that mean "this token will not work again", as opposed to a
|
|
48
|
+
// transient failure worth retrying.
|
|
49
|
+
const AUTH_ERROR_CODES = [
|
|
50
|
+
'token_auth_failed',
|
|
51
|
+
'token_unsupported',
|
|
52
|
+
'invalid_token',
|
|
53
|
+
'auth_failed',
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether an error means the stored token is no longer usable and the user has
|
|
58
|
+
* to log in again.
|
|
59
|
+
*
|
|
60
|
+
* @param {*} error - Any thrown or rejected value
|
|
61
|
+
* @returns {boolean} True if the session is invalid
|
|
62
|
+
*/
|
|
63
|
+
export const isAuthError = (error) => {
|
|
64
|
+
const status = error?.status ?? error?.response?.status;
|
|
65
|
+
if (status === 401 || status === 403) return true;
|
|
66
|
+
|
|
67
|
+
const code = String(error?.code ?? error?.error?.code ?? '').toLowerCase();
|
|
68
|
+
if (AUTH_ERROR_CODES.includes(code)) return true;
|
|
69
|
+
|
|
70
|
+
const message = String(error?.message ?? '').toLowerCase();
|
|
71
|
+
return AUTH_ERROR_CODES.some(c => message.includes(c))
|
|
72
|
+
|| message.includes('unauthorized')
|
|
73
|
+
|| message.includes('invalid token');
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A single-line, human-readable description of any thrown value.
|
|
78
|
+
* @param {*} error - Any thrown or rejected value
|
|
79
|
+
* @returns {string} The message to display
|
|
80
|
+
*/
|
|
81
|
+
export const formatError = (error) => normalizeError(error).message;
|