Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/npm-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 8
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
Expand All @@ -34,8 +32,6 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 8
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
Expand Down
224 changes: 22 additions & 202 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@
import { Command } from 'commander';
import chalk from 'chalk';
import { login, logout } from '../src/commands/auth.js';
import { init } from '../src/commands/init.js';
import { startShell } from '../src/commands/shell.js';
import { PROJECT_NAME, getLatestVersion } from '../src/commons.js';
import { appInfo, createApp, listApps, deleteApp, updateApp } from '../src/commands/apps.js';
import inquirer from 'inquirer';
import { initProfileModule, getProfileModule } from '../src/modules/ProfileModule.js';
import { initProfileModule } from '../src/modules/ProfileModule.js';
import { initPuterModule } from '../src/modules/PuterModule.js';
import { createSite, infoSite, listSites, deleteSite } from '../src/commands/sites.js';
import { formatError, isAuthError, report } from '../src/modules/ErrorModule.js';

// puter.js attaches an async 'load' listener to its XHRs, so a failed request
// rejects twice: once on the promise we await, and once on a promise nothing
// holds. Without this guard Node kills the CLI and prints the rejected plain
// object as "#<Object>". Auth failures are already surfaced with a re-login
// prompt by checkLogin(), so only report what is not handled elsewhere.
process.on('unhandledRejection', (error) => {
report(error);
if (!isAuthError(error)) {
console.error(chalk.red(`Error: ${formatError(error)}`));
console.error(chalk.dim('Run "last-error" in the shell for the full details.'));
}
});

async function main() {
initProfileModule();
initPuterModule();

const profileModule = getProfileModule();

const version = await getLatestVersion(PROJECT_NAME);

const program = new Command();
Expand Down Expand Up @@ -45,206 +53,12 @@ async function main() {
process.exit(0);
});

program
.command('init')
.description('Initialize a new Puter app')
.action(init);

program
.command('shell')
.description('Start interactive shell')
.action(() => startShell());


// App commands
program
.command('apps')
.description('List all your apps')
.argument('[period]', 'period: today, yesterday, 7d, 30d, this_month, last_month')
.action(async (period) => {
await profileModule.checkLogin();
await listApps({
statsPeriod: period || 'all'
});
process.exit(0);
});

const app = program
.command('app')
.description('App management commands');

app
.command('info')
.description('Get application information')
.argument('<app_name>', 'Name of the application')
.action(async (app_name) => {
await profileModule.checkLogin();
await appInfo([app_name]);
process.exit(0);
});

app
.command('create')
.description('Create a new app')
.argument('<name>', 'Name of the application')
.argument('<remote_dir>', 'Remote directory URL')
.action(async (name, remote_dir) => {
try {
await profileModule.checkLogin();
await createApp({
name: name,
directory: remote_dir || '',
description: '',
url: 'https://dev-center.puter.com/coming-soon.html'
});
} catch (error) {
console.error(chalk.red(error.message));
}
process.exit(0);
});

app
.command('update')
.description('Update an app')
.argument('<name>', 'Name of the application')
.argument('[dir]', 'Directory path', '.')
.action(async (name, dir) => {
await profileModule.checkLogin();
await updateApp([name, dir]);
process.exit(0);
});

app
.command('delete')
.description('Delete an app')
.argument('<name>', 'Name of the application')
.option('-f, --force', 'Force deletion without confirmation')
.action(async (name, options) => {
await profileModule.checkLogin();
let shouldDelete = options.force;

if (!shouldDelete) {
const answer = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: `Are you sure you want to delete the app "${name}"?`,
default: false
}
]);
shouldDelete = answer.confirm;
}

if (shouldDelete) {
await deleteApp(name);
} else {
console.log(chalk.yellow('App deletion cancelled.'));
}
process.exit(0);
});

program
.command('sites')
.description('List sites and subdomains')
.action(async () => {
await profileModule.checkLogin();
await listSites();
process.exit(0);
});

const site = program
.command('site')
.description('Site management commands');

site
.command('info')
.description('Get site information by UID')
.argument('<site_uid>', 'Site UID')
.action(async (site_uid) => {
await profileModule.checkLogin();
await infoSite([site_uid]);
process.exit(0);
});

site
.command('create')
.description('Create a static website from directory')
.argument('<app_name>', 'Application name')
.argument('[dir]', 'Directory path')
.option('--subdomain <name>', 'Subdomain name')
.action(async (app_name, dir, options) => {
await profileModule.checkLogin();
const args = [app_name];
if (dir) args.push(dir);
if (options.subdomain) args.push(`--subdomain=${options.subdomain}`)

await createSite(args)
process.exit(0);
});

site
.command('deploy')
.description('Deploy a local web project to Puter')
.argument('[local_dir]', 'Local directory path')
.argument('[subdomain]', 'Deployment subdomain (<subdomain>.puter.site)')
.action(async (local_dir, subdomain) => {
await profileModule.checkLogin();
if (!local_dir) {
const answer = await inquirer.prompt([
{
type: 'input',
name: 'local_dir',
message: 'Local directory path:',
default: '.'
}
]);
local_dir = answer.local_dir;
}

if (!subdomain) {
const answer = await inquirer.prompt([
{
type: 'input',
name: 'subdomain',
message: 'Deployment subdomain (leave empty for random):',
}
]);
subdomain = answer.subdomain;
}

await startShell(`site:deploy ${local_dir}${subdomain ? ` --subdomain=${subdomain}` : ''}`)
process.exit(0);
});

site
.command('delete')
.description('Delete a site by UID')
.argument('<uid>', 'Site UID')
.option('-f, --force', 'Force deletion without confirmation')
.action(async (uid, options) => {
await profileModule.checkLogin();
let shouldDelete = options.force;

if (!shouldDelete) {
const answer = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: `Are you sure you want to delete the site with UID "${uid}"?`,
default: false
}
]);
shouldDelete = answer.confirm;
}

if (shouldDelete) {
await deleteSite([uid]);
} else {
console.log(chalk.yellow('Site deletion cancelled.'));
}
process.exit(0);
});

if (process.argv.length === 2) {
startShell();
} else {
Expand All @@ -253,6 +67,12 @@ async function main() {
}

main().catch((err) => {
console.error(err);
report(err);
if (isAuthError(err)) {
console.error(chalk.red('Your session has expired or its token is no longer valid.'));
console.error(chalk.cyan('Run "puter login" to sign in again.'));
} else {
console.error(chalk.red(`Error: ${formatError(err)}`));
}
process.exit(1);
});
8 changes: 3 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@heyputer/shell",
"version": "2.1.0",
"version": "3.0.0",
"description": "SSH-style shell access to your Puter files",
"main": "index.js",
"bin": {
Expand All @@ -19,6 +19,7 @@
"engines": {
"node": ">=20.0.0"
},
"packageManager": "pnpm@9.9.0",
"keywords": [
"puter",
"shell",
Expand All @@ -29,18 +30,15 @@
"dependencies": {
"@heyputer/puter.js": "^2.2.8",
"chalk": "^5.3.0",
"cli-table3": "^0.6.5",
"commander": "^13.0.0",
"conf": "^12.0.0",
"cross-spawn": "^7.0.3",
"dotenv": "^16.4.7",
"glob": "^11.0.0",
"inquirer": "^9.2.12",
"minimatch": "^10.0.1",
"node-fetch": "^3.3.2",
"ora": "^8.0.1",
"uuid": "^11.0.5",
"yargs-parser": "^21.1.1"
"uuid": "^11.0.5"
},
"devDependencies": {
"@vitest/coverage-v8": "2.1.8",
Expand Down
Loading
Loading