UNPKG

puter-cli

Version:

Command line interface for Puter cloud platform

357 lines (331 loc) 14.8 kB
import path from 'path'; import chalk from 'chalk'; import fetch from 'node-fetch'; import Table from 'cli-table3'; import { displayNonNullValues, formatDate } from '../utils.js'; import { API_BASE, getHeaders, getDefaultHomePage, isValidAppName, resolvePath } from '../commons.js'; import { createSubdomain, getSubdomains } from './subdomains.js'; import { deleteSite } from './sites.js'; import { copyFile, createFile, listRemoteFiles, pathExists, removeFileOrDirectory } from './files.js'; import { getCurrentDirectory } from './auth.js'; import crypto from '../crypto.js'; import { getPuter } from '../modules/PuterModule.js'; /** * List all apps * * @param {object} options * ```json * { * statsPeriod: [all (default), today, yesterday, 7d, 30d, this_month, last_month, this_year, last_year, month_to_date, year_to_date, last_12_months], * iconSize: [16, 32, 64, 128, 256, 512] * } * ``` */ export async function listApps({ statsPeriod = 'all', iconSize = 64 } = {}) { console.log(chalk.green(`Listing of apps during period "${chalk.cyan(statsPeriod)}" (try also: today, yesterday, 7d, 30d, this_month, last_month):\n`)); const puter = getPuter(); try { const result = await puter.apps.list({ icon_size: iconSize, stats_period: statsPeriod }); if (result) { // Create a new table instance const table = new Table({ head: [ chalk.cyan('#'), chalk.cyan('Title'), chalk.cyan('Name'), chalk.cyan('Created'), chalk.cyan('Subdomain'), // chalk.cyan('Description'), chalk.cyan('#Open'), chalk.cyan('#User') ], colWidths: [5, 20, 30, 25, 35, 8, 8], wordWrap: false }); // Populate the table with app data let i = 0; for (const app of result) { table.push([ i++, app['title'], app['name'], formatDate(app['created_at']), app['index_url']?app['index_url'].split('.')[0].split('//')[1]:'<NO_URL>', // app['description'].slice(0, 10) || 'N/A', app['stats']['open_count'], app['stats']['user_count'] ]); } // Display the table console.log(table.toString()); console.log(chalk.green(`You have in total: ${chalk.cyan(result.length)} application(s).`)); } else { console.error(chalk.red('Unable to list your apps. Please check your credentials.')); } } catch (error) { console.error(chalk.red(`Failed to list apps. Error: ${error.message}`)); } } /** * Get app informations * * @param {Array} List of options (only "name" is supported at the moment) * @example: * ```json * const data = await appInfo("app name"); * ``` */ export async function appInfo(args = []) { if (!args || args.length == 0){ console.log(chalk.red('Usage: app <name>')); return; } const appName = args[0].trim() console.log(chalk.green(`Looking for "${chalk.dim(appName)}" app informations:\n`)); const puter = getPuter(); try { const result = await puter.apps.get(appName); if (result) { // Display the informations displayNonNullValues(result); } else { console.error(chalk.red('Could not find this app.')); } } catch (error) { console.error(chalk.red(`Failed to get app info. Error: ${error.message}`)); } } /** * Create a new web application * @param {string} name The name of the App * @param {string} directory Optional directory path * @param {string} description A description of the App * @param {string} url A default coming-soon URL * @returns {Promise<Object>} Output JSON data */ export async function createApp(args) { const name = args.name; // App name (required) if (!name || !isValidAppName(name)) { console.log(chalk.red('Usage: app:create <name> <directory>')); console.log(chalk.yellow('Example: app:create myApp .')); console.log(chalk.yellow('Example: app:create myApp ./myApp')); return; } // Use the default home page if the root directory if none specified const localDir = args.directory ? resolvePath(getCurrentDirectory(), args.directory) : ''; // Optional description const description = args.description || ''; const url = args.url || ''; console.log(chalk.green(`Creating app "${name}"...`)); console.log(chalk.dim(`Directory: ${localDir || '[default]'}`)); console.log(chalk.dim(`Description: ${description}`)); console.log(chalk.dim(`URL: ${url}`)); const puter = getPuter(); try { // Step 1: Create the app const createAppData = await puter.apps.create({ name: name, indexURL: url, title: name, description: description, maximizeOnStart: false, dedupeName: true }); if (!createAppData) { console.error(chalk.red(`Failed to create app "${name}"`)); return; } const appUid = createAppData.uid; const appName = createAppData.name; const username = createAppData.owner.username; console.log(chalk.green(`App "${chalk.dim(name)}" created successfully!`)); console.log(chalk.cyan(`AppName: ${chalk.dim(appName)}\nUID: ${chalk.dim(appUid)}\nUsername: ${chalk.dim(username)}`)); // Step 2: Create a directory for the app const uid = crypto.randomUUID(); const appDir = `/${username}/AppData/${appUid}`; console.log(chalk.green(`Creating directory...\nPath: ${chalk.dim(appDir)}\nApp: ${chalk.dim(name)}\nUID: ${chalk.dim(uid)}\n`)); const createDirData = await puter.fs.mkdir(`${appDir}/app-${uid}`, { overwrite: true, dedupeName: false, createMissingParents: true, }) if (!createDirData || !createDirData.uid) { console.error(chalk.red(`Failed to create directory for app "${name}"`)); return; } const dirUid = createDirData.uid; console.log(chalk.green(`Directory created successfully!`)); console.log(chalk.cyan(`Directory UID: ${chalk.dim(dirUid)}`)); // Step 3: Create a subdomain for the app const subdomainName = `${name}-${uid.split('-')[0]}`; const remoteDir = `${appDir}/${createDirData.name}`; console.log(chalk.green(`Linking to subdomain...\nSubdomain: "${chalk.dim(subdomainName)}"\nPath: ${chalk.dim(remoteDir)}\n`)); const subdomainResult = await createSubdomain(subdomainName, remoteDir); if (!subdomainResult) { console.error(chalk.red(`Failed to create subdomain: "${subdomainName}"`)); return; } console.log(chalk.green(`Subdomain created successfully!`)); console.log(chalk.cyan(`Subdomain: ${chalk.dim(subdomainName)}`)); // Step 4: Create a home page if (localDir.length > 0){ // List files in the current "localDir" then copy them to the "remoteDir" const files = await listRemoteFiles(localDir); if (Array.isArray(files) && files.length > 0) { console.log(chalk.cyan(`Copying ${chalk.dim(files.length)} files from: ${chalk.dim(localDir)}`)); console.log(chalk.cyan(`To destination: ${chalk.dim(remoteDir)}`)); for (const file of files) { const fileSource = path.join(localDir, file.name); await copyFile([fileSource, remoteDir]); } } else { console.log(chalk.yellow("We could not find any file in the specified directory!")); } } else { const homePageResult = await createFile([path.join(remoteDir, 'index.html'), getDefaultHomePage(appName)]); if (!homePageResult){ console.log(chalk.yellow("We could not create the home page file!")); } } // Step 5: Update the app's index_url to point to the subdomain console.log(chalk.green(`Set "${chalk.dim(subdomainName)}" as a subdomain for app: "${chalk.dim(appName)}"...\n`)); const updateAppData = await puter.apps.update(appName, { indexURL: `https://${subdomainName}.puter.site`, title: name }) if (!updateAppData) { console.error(chalk.red(`Failed to update app "${name}" with new subdomain`)); return; } console.log(chalk.green(`App deployed successfully at:`)); console.log(chalk.cyanBright(`https://${subdomainName}.puter.site`)); } catch (error) { console.error(chalk.red(`Failed to create app "${name}".\nError: ${error.message}`)); } } /** * Update an application from the directory * @param {string} name The name of the App * @param {string} remote_dir The remote directory */ export async function updateApp(args = []) { if (args.length < 1) { console.log(chalk.red('Usage: app:update <valid_name_app> [<remote_dir>]')); console.log(chalk.yellow('Example: app:create myapp')); console.log(chalk.yellow('Example: app:create myapp ./myapp')); return; } const name = args[0]; // App name (required) // Fix: Properly handle absolute paths by checking if the path starts with '/' let remoteDir; if (args[1] && args[1].startsWith('/')) { remoteDir = args[1]; // Use the absolute path as-is } else { remoteDir = resolvePath(getCurrentDirectory(), args[1] || '.'); } const remoteDirExists = await pathExists(remoteDir); if (!remoteDirExists){ console.log(chalk.red(`Cannot find directory: ${chalk.dim(remoteDir)}...\n`)); return; } const puter = getPuter(); console.log(chalk.green(`Updating app: "${chalk.dim(name)}" from directory: ${chalk.dim(remoteDir)}\n`)); try { // Step 1: Get the app info const data = await puter.apps.get(name); if (!data) { console.error(chalk.red(`Failed to find app: "${name}"`)); return; } const appUid = data.uid; const appName = data.name; const username = data.owner.username; const indexUrl = data.index_url; const appDir = `/${username}/AppData/${appUid}`; console.log(chalk.cyan(`AppName: ${chalk.dim(appName)}\nUID: ${chalk.dim(appUid)}\nUsername: ${chalk.dim(username)}`)); // Step 2: Find the path from subdomain const subdomains = await getSubdomains(); const appSubdomain = subdomains.find(sd => sd.root_dir?.dirname?.endsWith(appUid)); if (!appSubdomain){ console.error(chalk.red(`Sorry! We could not find the subdomain for ${chalk.cyan(name)} application.`)); return; } const subdomainDir = appSubdomain['root_dir']['path']; if (!subdomainDir){ console.error(chalk.red(`Sorry! We could not find the path for ${chalk.cyan(name)} application.`)); return; } // Step 3: List files in the current "remoteDir" then copy them to the "subdomainDir" const files = await listRemoteFiles(remoteDir); if (Array.isArray(files) && files.length > 0) { console.log(chalk.cyan(`Copying ${chalk.dim(files.length)} files from: ${chalk.dim(remoteDir)}`)); console.log(chalk.cyan(`To destination: ${chalk.dim(subdomainDir)}`)); for (const file of files) { const fileSource = path.join(remoteDir, file.name); const fileDest = path.join(subdomainDir, file.name); if ((await pathExists(fileDest))){ await removeFileOrDirectory([fileDest, '-f']); } await copyFile([fileSource, subdomainDir]); } } else { console.log(chalk.red("We could not find any file in the specified directory!")); } console.log(chalk.green(`App updated successfully at:`)); console.log(chalk.dim(indexUrl)); } catch (error) { console.error(chalk.red(`Failed to update app "${name}".\nError: ${error.message}`)); console.error(error); } } /** * Delete an app by its name * @param {string} name The name of the app to delete * @returns a boolean success value */ export async function deleteApp(name) { if (!name || name.length == 0){ console.log(chalk.red('Usage: app:delete <name>')); return false; } const puter = getPuter(); console.log(chalk.green(`Checking app "${name}"...\n`)); try { // Step 1: Read app details const readData = await puter.apps.get(name); if (!readData) { console.log(chalk.red(`App "${chalk.bold(name)}" not found.`)); return false; } // Show app details and confirm deletion console.log(chalk.cyan('\nApp Details:')); console.log(chalk.dim('----------------------------------------')); console.log(chalk.dim(`Name: ${chalk.cyan(readData.name)}`)); console.log(chalk.dim(`Title: ${chalk.cyan(readData.title)}`)); console.log(chalk.dim(`Created: ${chalk.cyan(formatDate(readData.created_at))}`)); console.log(chalk.dim(`URL: ${readData.index_url}`)); console.log(chalk.dim('----------------------------------------')); // Step 2: Delete the app console.log(chalk.green(`Deleting app "${chalk.red(name)}"...`)); const deleteData = await puter.apps.delete(name); if (!deleteData) { console.error(chalk.red(`Failed to delete app "${name}".\nP.S. Make sure to provide the 'name' attribute not the 'title'.`)); return false; } // Lookup subdomainUID then delete it const subdomains = await getSubdomains(); const appSubdomain = subdomains.find(sd => sd.root_dir?.dirname?.endsWith(readData.uid)); const subdomainDeleted = await deleteSite([appSubdomain.uid]); if (subdomainDeleted){ console.log(chalk.green(`Subdomain: ${chalk.dim(appSubdomain.uid)} deleted.`)); } console.log(chalk.green(`App "${chalk.dim(name)}" deleted successfully!`)); } catch (error) { console.error(chalk.red(`Failed to delete app "${name}".\nError: ${error.message}`)); return false; } return true; }