@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.
@@ -14,8 +14,6 @@ jobs:
14
14
  - uses: actions/checkout@v4
15
15
  - name: Install pnpm
16
16
  uses: pnpm/action-setup@v4
17
- with:
18
- version: 8
19
17
  - name: Use Node.js ${{ matrix.node-version }}
20
18
  uses: actions/setup-node@v4
21
19
  with:
@@ -15,8 +15,6 @@ jobs:
15
15
  - uses: actions/checkout@v4
16
16
  - name: Install pnpm
17
17
  uses: pnpm/action-setup@v4
18
- with:
19
- version: 8
20
18
  - name: Use Node.js 20
21
19
  uses: actions/setup-node@v4
22
20
  with:
@@ -34,8 +32,6 @@ jobs:
34
32
  - uses: actions/checkout@v4
35
33
  - name: Install pnpm
36
34
  uses: pnpm/action-setup@v4
37
- with:
38
- version: 8
39
35
  - name: Use Node.js 20
40
36
  uses: actions/setup-node@v4
41
37
  with:
package/bin/index.js CHANGED
@@ -2,21 +2,29 @@
2
2
  import { Command } from 'commander';
3
3
  import chalk from 'chalk';
4
4
  import { login, logout } from '../src/commands/auth.js';
5
- import { init } from '../src/commands/init.js';
6
5
  import { startShell } from '../src/commands/shell.js';
7
6
  import { PROJECT_NAME, getLatestVersion } from '../src/commons.js';
8
- import { appInfo, createApp, listApps, deleteApp, updateApp } from '../src/commands/apps.js';
9
- import inquirer from 'inquirer';
10
- import { initProfileModule, getProfileModule } from '../src/modules/ProfileModule.js';
7
+ import { initProfileModule } from '../src/modules/ProfileModule.js';
11
8
  import { initPuterModule } from '../src/modules/PuterModule.js';
12
- import { createSite, infoSite, listSites, deleteSite } from '../src/commands/sites.js';
9
+ import { formatError, isAuthError, report } from '../src/modules/ErrorModule.js';
10
+
11
+ // puter.js attaches an async 'load' listener to its XHRs, so a failed request
12
+ // rejects twice: once on the promise we await, and once on a promise nothing
13
+ // holds. Without this guard Node kills the CLI and prints the rejected plain
14
+ // object as "#<Object>". Auth failures are already surfaced with a re-login
15
+ // prompt by checkLogin(), so only report what is not handled elsewhere.
16
+ process.on('unhandledRejection', (error) => {
17
+ report(error);
18
+ if (!isAuthError(error)) {
19
+ console.error(chalk.red(`Error: ${formatError(error)}`));
20
+ console.error(chalk.dim('Run "last-error" in the shell for the full details.'));
21
+ }
22
+ });
13
23
 
14
24
  async function main() {
15
25
  initProfileModule();
16
26
  initPuterModule();
17
27
 
18
- const profileModule = getProfileModule();
19
-
20
28
  const version = await getLatestVersion(PROJECT_NAME);
21
29
 
22
30
  const program = new Command();
@@ -45,206 +53,12 @@ async function main() {
45
53
  process.exit(0);
46
54
  });
47
55
 
48
- program
49
- .command('init')
50
- .description('Initialize a new Puter app')
51
- .action(init);
52
-
53
56
  program
54
57
  .command('shell')
55
58
  .description('Start interactive shell')
56
59
  .action(() => startShell());
57
60
 
58
61
 
59
- // App commands
60
- program
61
- .command('apps')
62
- .description('List all your apps')
63
- .argument('[period]', 'period: today, yesterday, 7d, 30d, this_month, last_month')
64
- .action(async (period) => {
65
- await profileModule.checkLogin();
66
- await listApps({
67
- statsPeriod: period || 'all'
68
- });
69
- process.exit(0);
70
- });
71
-
72
- const app = program
73
- .command('app')
74
- .description('App management commands');
75
-
76
- app
77
- .command('info')
78
- .description('Get application information')
79
- .argument('<app_name>', 'Name of the application')
80
- .action(async (app_name) => {
81
- await profileModule.checkLogin();
82
- await appInfo([app_name]);
83
- process.exit(0);
84
- });
85
-
86
- app
87
- .command('create')
88
- .description('Create a new app')
89
- .argument('<name>', 'Name of the application')
90
- .argument('<remote_dir>', 'Remote directory URL')
91
- .action(async (name, remote_dir) => {
92
- try {
93
- await profileModule.checkLogin();
94
- await createApp({
95
- name: name,
96
- directory: remote_dir || '',
97
- description: '',
98
- url: 'https://dev-center.puter.com/coming-soon.html'
99
- });
100
- } catch (error) {
101
- console.error(chalk.red(error.message));
102
- }
103
- process.exit(0);
104
- });
105
-
106
- app
107
- .command('update')
108
- .description('Update an app')
109
- .argument('<name>', 'Name of the application')
110
- .argument('[dir]', 'Directory path', '.')
111
- .action(async (name, dir) => {
112
- await profileModule.checkLogin();
113
- await updateApp([name, dir]);
114
- process.exit(0);
115
- });
116
-
117
- app
118
- .command('delete')
119
- .description('Delete an app')
120
- .argument('<name>', 'Name of the application')
121
- .option('-f, --force', 'Force deletion without confirmation')
122
- .action(async (name, options) => {
123
- await profileModule.checkLogin();
124
- let shouldDelete = options.force;
125
-
126
- if (!shouldDelete) {
127
- const answer = await inquirer.prompt([
128
- {
129
- type: 'confirm',
130
- name: 'confirm',
131
- message: `Are you sure you want to delete the app "${name}"?`,
132
- default: false
133
- }
134
- ]);
135
- shouldDelete = answer.confirm;
136
- }
137
-
138
- if (shouldDelete) {
139
- await deleteApp(name);
140
- } else {
141
- console.log(chalk.yellow('App deletion cancelled.'));
142
- }
143
- process.exit(0);
144
- });
145
-
146
- program
147
- .command('sites')
148
- .description('List sites and subdomains')
149
- .action(async () => {
150
- await profileModule.checkLogin();
151
- await listSites();
152
- process.exit(0);
153
- });
154
-
155
- const site = program
156
- .command('site')
157
- .description('Site management commands');
158
-
159
- site
160
- .command('info')
161
- .description('Get site information by UID')
162
- .argument('<site_uid>', 'Site UID')
163
- .action(async (site_uid) => {
164
- await profileModule.checkLogin();
165
- await infoSite([site_uid]);
166
- process.exit(0);
167
- });
168
-
169
- site
170
- .command('create')
171
- .description('Create a static website from directory')
172
- .argument('<app_name>', 'Application name')
173
- .argument('[dir]', 'Directory path')
174
- .option('--subdomain <name>', 'Subdomain name')
175
- .action(async (app_name, dir, options) => {
176
- await profileModule.checkLogin();
177
- const args = [app_name];
178
- if (dir) args.push(dir);
179
- if (options.subdomain) args.push(`--subdomain=${options.subdomain}`)
180
-
181
- await createSite(args)
182
- process.exit(0);
183
- });
184
-
185
- site
186
- .command('deploy')
187
- .description('Deploy a local web project to Puter')
188
- .argument('[local_dir]', 'Local directory path')
189
- .argument('[subdomain]', 'Deployment subdomain (<subdomain>.puter.site)')
190
- .action(async (local_dir, subdomain) => {
191
- await profileModule.checkLogin();
192
- if (!local_dir) {
193
- const answer = await inquirer.prompt([
194
- {
195
- type: 'input',
196
- name: 'local_dir',
197
- message: 'Local directory path:',
198
- default: '.'
199
- }
200
- ]);
201
- local_dir = answer.local_dir;
202
- }
203
-
204
- if (!subdomain) {
205
- const answer = await inquirer.prompt([
206
- {
207
- type: 'input',
208
- name: 'subdomain',
209
- message: 'Deployment subdomain (leave empty for random):',
210
- }
211
- ]);
212
- subdomain = answer.subdomain;
213
- }
214
-
215
- await startShell(`site:deploy ${local_dir}${subdomain ? ` --subdomain=${subdomain}` : ''}`)
216
- process.exit(0);
217
- });
218
-
219
- site
220
- .command('delete')
221
- .description('Delete a site by UID')
222
- .argument('<uid>', 'Site UID')
223
- .option('-f, --force', 'Force deletion without confirmation')
224
- .action(async (uid, options) => {
225
- await profileModule.checkLogin();
226
- let shouldDelete = options.force;
227
-
228
- if (!shouldDelete) {
229
- const answer = await inquirer.prompt([
230
- {
231
- type: 'confirm',
232
- name: 'confirm',
233
- message: `Are you sure you want to delete the site with UID "${uid}"?`,
234
- default: false
235
- }
236
- ]);
237
- shouldDelete = answer.confirm;
238
- }
239
-
240
- if (shouldDelete) {
241
- await deleteSite([uid]);
242
- } else {
243
- console.log(chalk.yellow('Site deletion cancelled.'));
244
- }
245
- process.exit(0);
246
- });
247
-
248
62
  if (process.argv.length === 2) {
249
63
  startShell();
250
64
  } else {
@@ -253,6 +67,12 @@ async function main() {
253
67
  }
254
68
 
255
69
  main().catch((err) => {
256
- console.error(err);
70
+ report(err);
71
+ if (isAuthError(err)) {
72
+ console.error(chalk.red('Your session has expired or its token is no longer valid.'));
73
+ console.error(chalk.cyan('Run "puter login" to sign in again.'));
74
+ } else {
75
+ console.error(chalk.red(`Error: ${formatError(err)}`));
76
+ }
257
77
  process.exit(1);
258
78
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyputer/shell",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "SSH-style shell access to your Puter files",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "engines": {
20
20
  "node": ">=20.0.0"
21
21
  },
22
+ "packageManager": "pnpm@9.9.0",
22
23
  "keywords": [
23
24
  "puter",
24
25
  "shell",
@@ -29,18 +30,15 @@
29
30
  "dependencies": {
30
31
  "@heyputer/puter.js": "^2.2.8",
31
32
  "chalk": "^5.3.0",
32
- "cli-table3": "^0.6.5",
33
33
  "commander": "^13.0.0",
34
34
  "conf": "^12.0.0",
35
- "cross-spawn": "^7.0.3",
36
35
  "dotenv": "^16.4.7",
37
36
  "glob": "^11.0.0",
38
37
  "inquirer": "^9.2.12",
39
38
  "minimatch": "^10.0.1",
40
39
  "node-fetch": "^3.3.2",
41
40
  "ora": "^8.0.1",
42
- "uuid": "^11.0.5",
43
- "yargs-parser": "^21.1.1"
41
+ "uuid": "^11.0.5"
44
42
  },
45
43
  "devDependencies": {
46
44
  "@vitest/coverage-v8": "2.1.8",
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import Conf from 'conf';
3
3
  import ora from 'ora';
4
- import { PROJECT_NAME, } from '../commons.js'
4
+ import { HOME, PROJECT_NAME, expandHome } from '../commons.js'
5
5
  import { getProfileModule } from '../modules/ProfileModule.js';
6
6
  import { getPuter } from '../modules/PuterModule.js';
7
7
  const config = new Conf({ projectName: PROJECT_NAME });
@@ -29,18 +29,10 @@ export async function logout() {
29
29
  let spinner;
30
30
  try {
31
31
  spinner = ora('Logging out from Puter...').start();
32
- const token = config.get('auth_token');
33
32
  const selected_profile = config.get('selected_profile');
34
33
 
35
- if (token) {
36
- // legacy auth
37
- config.clear();
38
- spinner.succeed(chalk.green('Successfully logged out from Puter!'));
39
- } else if (selected_profile) {
40
- // multi profile auth
34
+ if (selected_profile) {
41
35
  config.delete('selected_profile');
42
- config.delete('username');
43
- config.delete('cwd');
44
36
 
45
37
  const profiles = config.get('profiles');
46
38
  config.set('profiles', profiles.filter(profile => profile.uuid != selected_profile));
@@ -86,7 +78,10 @@ export async function getUserInfo() {
86
78
  }
87
79
  }
88
80
  export function isAuthenticated() {
89
- return !!config.get('auth_token');
81
+ const uuid = config.get('selected_profile');
82
+ if (!uuid) return false;
83
+ const profiles = config.get('profiles') ?? [];
84
+ return !!profiles.find(p => p.uuid === uuid)?.token;
90
85
  }
91
86
 
92
87
  export function getAuthToken() {
@@ -100,7 +95,7 @@ export function getCurrentUserName() {
100
95
  }
101
96
 
102
97
  export function getCurrentDirectory() {
103
- return config.get('cwd');
98
+ return expandHome(getProfileModule().getCwd());
104
99
  }
105
100
 
106
101
  /**
@@ -7,10 +7,10 @@ import { minimatch } from 'minimatch';
7
7
  import chalk from 'chalk';
8
8
  import Conf from 'conf';
9
9
  import fetch from 'node-fetch';
10
- import { API_BASE, BASE_URL, PROJECT_NAME, getHeaders, showDiskSpaceUsage, resolvePath, resolveRemotePath } from '../commons.js';
10
+ import { API_BASE, BASE_URL, HOME, PROJECT_NAME, expandHome, getHeaders, isAbsolutePath, showDiskSpaceUsage, resolvePath, resolveRemotePath } from '../commons.js';
11
11
  import { formatDateTime, formatSize, getSystemEditor } from '../utils.js';
12
12
  import inquirer from 'inquirer';
13
- import { getAuthToken, getCurrentDirectory, getCurrentUserName } from './auth.js';
13
+ import { getAuthToken, getCurrentDirectory } from './auth.js';
14
14
  import { updatePrompt } from './shell.js';
15
15
  import crypto from '../crypto.js';
16
16
  import { getPuter } from '../modules/PuterModule.js';
@@ -85,7 +85,7 @@ export async function makeDirectory(args = []) {
85
85
  const puter = getPuter();
86
86
 
87
87
  try {
88
- const data = await puter.fs.mkdir(`${getCurrentDirectory()}/${directoryName}`, {
88
+ const data = await puter.fs.mkdir(resolvePath(getCurrentDirectory(), directoryName), {
89
89
  overwrite: false,
90
90
  dedupeName: true,
91
91
  createMissingParents: false
@@ -314,7 +314,7 @@ export async function removeFileOrDirectory(args = []) {
314
314
  const uid = statData.uid;
315
315
 
316
316
  // Step 4.2: Perform the move operation to Trash
317
- const moveData = await puter.fs.move(uid, `/${getCurrentUserName()}/Trash`, {
317
+ const moveData = await puter.fs.move(uid, expandHome(`${HOME}/Trash`), {
318
318
  overwrite: false,
319
319
  newName: uid,
320
320
  createMissingParents: false,
@@ -385,7 +385,7 @@ export async function deleteFolder(folderPath, skipConfirmation = false) {
385
385
  * @param {boolean} skipConfirmation - Whether to skip the confirmation prompt.
386
386
  */
387
387
  export async function emptyTrash(skipConfirmation = true) {
388
- const trashPath = `/${getCurrentUserName()}/Trash`;
388
+ const trashPath = expandHome(`${HOME}/Trash`);
389
389
  await deleteFolder(trashPath, skipConfirmation);
390
390
  }
391
391
 
@@ -398,7 +398,7 @@ export async function getInfo(args = []) {
398
398
  const puter = getPuter();
399
399
  for (let name of names)
400
400
  try {
401
- name = `${getCurrentDirectory()}/${name}`;
401
+ name = resolvePath(getCurrentDirectory(), name);
402
402
  console.log(chalk.green(`Getting stat info for: "${name}"...\n`));
403
403
  const data = await puter.fs.stat(name);
404
404
  if (data) {
@@ -425,7 +425,7 @@ export async function getInfo(args = []) {
425
425
  * Show the current working directory
426
426
  */
427
427
  export async function showCwd() {
428
- console.log(chalk.green(`${config.get('cwd')}`));
428
+ console.log(chalk.green(`${getCurrentDirectory()}`));
429
429
  }
430
430
 
431
431
  /**
@@ -434,7 +434,7 @@ export async function showCwd() {
434
434
  * @returns void
435
435
  */
436
436
  export async function changeDirectory(args) {
437
- let currentPath = config.get('cwd');
437
+ let currentPath = getCurrentDirectory();
438
438
  // If no arguments, print the current directory
439
439
  if (!args.length) {
440
440
  console.log(chalk.green(currentPath));
@@ -443,17 +443,15 @@ export async function changeDirectory(args) {
443
443
  const puter = getPuter();
444
444
 
445
445
  const path = args[0];
446
- // Handle "/","~",".." and deeper navigation
447
- const newPath = path.startsWith('/')? path: (path === '~'? `/${getCurrentUserName()}` :resolvePath(currentPath, path));
446
+ // resolvePath handles "/", "~", "~/...", "." and ".." in one place.
447
+ const newPath = resolvePath(currentPath, path);
448
448
  try {
449
449
  // Check if the new path is a valid directory
450
450
  const data = await puter.fs.stat(newPath);
451
451
  if (data && data.is_dir) {
452
- // Update the newPath to use the correct name from the response
453
- const arrayDirs = newPath.split('/');
454
- arrayDirs.pop();
455
- arrayDirs.push(data.name);
456
- updatePrompt(arrayDirs.join('/')); // Update the shell prompt
452
+ // Adopt the server's canonical path, which is already rooted at the
453
+ // account's current home directory.
454
+ updatePrompt(data.path || newPath); // Update the shell prompt
457
455
  } else {
458
456
  console.log(chalk.red(`"${newPath}" is not a directory`));
459
457
  }
@@ -519,7 +517,7 @@ export async function createFile(args = []) {
519
517
  const filePath = args[0]; // File path (e.g., "app/index.html")
520
518
  const content = args.length > 1 ? args.slice(1).join(' ') : ''; // Optional content
521
519
  let fullPath = filePath;
522
- if (!filePath.startsWith(`/${getCurrentUserName()}/`)){
520
+ if (!isAbsolutePath(filePath)) {
523
521
  fullPath = resolvePath(getCurrentDirectory(), filePath); // Resolve the full path
524
522
  }
525
523
  const dirName = path.dirname(fullPath); // Extract the directory name
@@ -763,8 +761,8 @@ export async function copyFile(args = []) {
763
761
  return;
764
762
  }
765
763
 
766
- const sourcePath = args[0].startsWith(`/${getCurrentUserName()}`) ? args[0] : resolvePath(getCurrentDirectory(), args[0]); // Resolve the source path
767
- const destinationPath = args[1].startsWith(`/${getCurrentUserName()}`) ? args[1] : resolvePath(getCurrentDirectory(), args[1]); // Resolve the destination path
764
+ const sourcePath = resolvePath(getCurrentDirectory(), args[0]); // Resolve the source path
765
+ const destinationPath = resolvePath(getCurrentDirectory(), args[1]); // Resolve the destination path
768
766
 
769
767
  console.log(chalk.green(`Copy: "${chalk.dim(sourcePath)}" to: "${chalk.dim(destinationPath)}"...\n`));
770
768
  const puter = getPuter();
@@ -4,6 +4,7 @@ import Conf from 'conf';
4
4
  import { execCommand, getPrompt } from '../executor.js';
5
5
  import { PROJECT_NAME } from '../commons.js';
6
6
  import { getProfileModule } from '../modules/ProfileModule.js';
7
+ import { report, formatError } from '../modules/ErrorModule.js';
7
8
 
8
9
  const config = new Conf({ projectName: PROJECT_NAME });
9
10
 
@@ -13,7 +14,7 @@ export let rl;
13
14
  * Update the current shell prompt
14
15
  */
15
16
  export function updatePrompt(currentPath) {
16
- config.set('cwd', currentPath);
17
+ getProfileModule().setCwd(currentPath);
17
18
  rl.setPrompt(getPrompt());
18
19
  }
19
20
 
@@ -37,7 +38,7 @@ export async function startShell(command) {
37
38
  })
38
39
 
39
40
  try {
40
- console.log(chalk.green('Welcome to Puter-CLI! Type "help" for available commands.'));
41
+ console.log(chalk.green('Welcome to Puter Shell! Type "help" for available commands.'));
41
42
  rl.setPrompt(getPrompt());
42
43
  rl.prompt();
43
44
 
@@ -47,7 +48,8 @@ export async function startShell(command) {
47
48
  try {
48
49
  await execCommand(trimmedLine);
49
50
  } catch (error) {
50
- console.error(chalk.red(error.message));
51
+ report(error);
52
+ console.error(chalk.red(formatError(error)));
51
53
  }
52
54
  }
53
55
  rl.prompt();