@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/bin/index.js ADDED
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import { login, logout } from '../src/commands/auth.js';
5
+ import { init } from '../src/commands/init.js';
6
+ import { startShell } from '../src/commands/shell.js';
7
+ 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';
11
+ import { initPuterModule } from '../src/modules/PuterModule.js';
12
+ import { createSite, infoSite, listSites, deleteSite } from '../src/commands/sites.js';
13
+
14
+ async function main() {
15
+ initProfileModule();
16
+ initPuterModule();
17
+
18
+ const profileModule = getProfileModule();
19
+
20
+ const version = await getLatestVersion(PROJECT_NAME);
21
+
22
+ const program = new Command();
23
+ program
24
+ .name('puter')
25
+ .description('CLI tool for Puter cloud platform')
26
+ .version(version);
27
+
28
+ program
29
+ .command('login')
30
+ .description('Login to Puter account')
31
+ .option('-s, --save', 'Save authentication token in .env file')
32
+ .option('--web', 'Use browser-based login (default)')
33
+ .option('--with-credentials', 'Use username/password login')
34
+ .option('--host <url>', 'Puter host URL', 'https://puter.com')
35
+ .action(async (options) => {
36
+ await login(options);
37
+ process.exit(0);
38
+ });
39
+
40
+ program
41
+ .command('logout')
42
+ .description('Logout from Puter account')
43
+ .action(async () => {
44
+ await logout();
45
+ process.exit(0);
46
+ });
47
+
48
+ program
49
+ .command('init')
50
+ .description('Initialize a new Puter app')
51
+ .action(init);
52
+
53
+ program
54
+ .command('shell')
55
+ .description('Start interactive shell')
56
+ .action(() => startShell());
57
+
58
+
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
+ if (process.argv.length === 2) {
249
+ startShell();
250
+ } else {
251
+ program.parse(process.argv);
252
+ }
253
+ }
254
+
255
+ main().catch((err) => {
256
+ console.error(err);
257
+ process.exit(1);
258
+ });
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@heyputer/shell",
3
+ "version": "2.1.0",
4
+ "description": "SSH-style shell access to your Puter files",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "puter-sh": "./bin/index.js",
8
+ "psh": "./bin/index.js"
9
+ },
10
+ "preferGlobal": true,
11
+ "type": "module",
12
+ "scripts": {
13
+ "start": "node bin/index.js",
14
+ "test": "TZ=UTC vitest run tests/*",
15
+ "test:watch": "TZ=UTC vitest --watch tests/*",
16
+ "version": "auto-changelog -p && git add CHANGELOG.md",
17
+ "coverage": "TZ=UTC vitest run --coverage"
18
+ },
19
+ "engines": {
20
+ "node": ">=20.0.0"
21
+ },
22
+ "keywords": [
23
+ "puter",
24
+ "shell",
25
+ "cloud"
26
+ ],
27
+ "author": "Ibrahim.H",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@heyputer/puter.js": "^2.2.8",
31
+ "chalk": "^5.3.0",
32
+ "cli-table3": "^0.6.5",
33
+ "commander": "^13.0.0",
34
+ "conf": "^12.0.0",
35
+ "cross-spawn": "^7.0.3",
36
+ "dotenv": "^16.4.7",
37
+ "glob": "^11.0.0",
38
+ "inquirer": "^9.2.12",
39
+ "minimatch": "^10.0.1",
40
+ "node-fetch": "^3.3.2",
41
+ "ora": "^8.0.1",
42
+ "uuid": "^11.0.5",
43
+ "yargs-parser": "^21.1.1"
44
+ },
45
+ "devDependencies": {
46
+ "@vitest/coverage-v8": "2.1.8",
47
+ "auto-changelog": "^2.5.0",
48
+ "vitest": "^2.1.8"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/HeyPuter/puter-sh.git"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/HeyPuter/puter-sh/issues"
56
+ },
57
+ "homepage": "https://github.com/HeyPuter/puter-sh"
58
+ }
package/screenshot.png ADDED
Binary file
@@ -0,0 +1,356 @@
1
+ import path from 'path';
2
+ import chalk from 'chalk';
3
+ import fetch from 'node-fetch';
4
+ import Table from 'cli-table3';
5
+ import { displayNonNullValues, formatDate } from '../utils.js';
6
+ import { API_BASE, getHeaders, getDefaultHomePage, isValidAppName, resolvePath } from '../commons.js';
7
+ import { createSubdomain, getSubdomains } from './subdomains.js';
8
+ import { deleteSite } from './sites.js';
9
+ import { copyFile, createFile, listRemoteFiles, pathExists, removeFileOrDirectory } from './files.js';
10
+ import { getCurrentDirectory } from './auth.js';
11
+ import crypto from '../crypto.js';
12
+ import { getPuter } from '../modules/PuterModule.js';
13
+
14
+ /**
15
+ * List all apps
16
+ *
17
+ * @param {object} options
18
+ * ```json
19
+ * {
20
+ * statsPeriod: [all (default), today, yesterday, 7d, 30d, this_month, last_month, this_year, last_year, month_to_date, year_to_date, last_12_months],
21
+ * iconSize: [16, 32, 64, 128, 256, 512]
22
+ * }
23
+ * ```
24
+ */
25
+ export async function listApps({ statsPeriod = 'all', iconSize = 64 } = {}) {
26
+ console.log(chalk.green(`Listing of apps during period "${chalk.cyan(statsPeriod)}" (try also: today, yesterday, 7d, 30d, this_month, last_month):\n`));
27
+ const puter = getPuter();
28
+ try {
29
+ const result = await puter.apps.list({
30
+ icon_size: iconSize,
31
+ stats_period: statsPeriod
32
+ });
33
+ if (result) {
34
+ // Create a new table instance
35
+ const table = new Table({
36
+ head: [
37
+ chalk.cyan('#'),
38
+ chalk.cyan('Title'),
39
+ chalk.cyan('Name'),
40
+ chalk.cyan('Created'),
41
+ chalk.cyan('Subdomain'),
42
+ // chalk.cyan('Description'),
43
+ chalk.cyan('#Open'),
44
+ chalk.cyan('#User')
45
+ ],
46
+ colWidths: [5, 20, 30, 25, 35, 8, 8],
47
+ wordWrap: false
48
+ });
49
+
50
+ // Populate the table with app data
51
+ let i = 0;
52
+ for (const app of result) {
53
+ table.push([
54
+ i++,
55
+ app['title'],
56
+ app['name'],
57
+ formatDate(app['created_at']),
58
+ app['index_url']?app['index_url'].split('.')[0].split('//')[1]:'<NO_URL>',
59
+ // app['description'].slice(0, 10) || 'N/A',
60
+ app['stats']['open_count'],
61
+ app['stats']['user_count']
62
+ ]);
63
+ }
64
+
65
+ // Display the table
66
+ console.log(table.toString());
67
+ console.log(chalk.green(`You have in total: ${chalk.cyan(result.length)} application(s).`));
68
+ } else {
69
+ console.error(chalk.red('Unable to list your apps. Please check your credentials.'));
70
+ }
71
+ } catch (error) {
72
+ console.error(chalk.red(`Failed to list apps. Error: ${error.message}`));
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Get app informations
78
+ *
79
+ * @param {Array} List of options (only "name" is supported at the moment)
80
+ * @example:
81
+ * ```json
82
+ * const data = await appInfo("app name");
83
+ * ```
84
+ */
85
+ export async function appInfo(args = []) {
86
+ if (!args || args.length == 0){
87
+ console.log(chalk.red('Usage: app <name>'));
88
+ return;
89
+ }
90
+ const appName = args[0].trim()
91
+ console.log(chalk.green(`Looking for "${chalk.dim(appName)}" app informations:\n`));
92
+ const puter = getPuter();
93
+ try {
94
+ const result = await puter.apps.get(appName);
95
+ if (result) {
96
+ // Display the informations
97
+ displayNonNullValues(result);
98
+ } else {
99
+ console.error(chalk.red('Could not find this app.'));
100
+ }
101
+ } catch (error) {
102
+ console.error(chalk.red(`Failed to get app info. Error: ${error.message}`));
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Create a new web application
108
+ * @param {string} name The name of the App
109
+ * @param {string} directory Optional directory path
110
+ * @param {string} description A description of the App
111
+ * @param {string} url A default coming-soon URL
112
+ * @returns {Promise<Object>} Output JSON data
113
+ */
114
+ export async function createApp(args) {
115
+ const name = args.name; // App name (required)
116
+ if (!name || !isValidAppName(name)) {
117
+ console.log(chalk.red('Usage: app:create <name> <directory>'));
118
+ console.log(chalk.yellow('Example: app:create myApp .'));
119
+ console.log(chalk.yellow('Example: app:create myApp ./myApp'));
120
+ return;
121
+ }
122
+ // Use the default home page if the root directory if none specified
123
+ const localDir = args.directory ? resolvePath(getCurrentDirectory(), args.directory) : '';
124
+ // Optional description
125
+ const description = args.description || '';
126
+ const url = args.url || '';
127
+
128
+ console.log(chalk.green(`Creating app "${name}"...`));
129
+ console.log(chalk.dim(`Directory: ${localDir || '[default]'}`));
130
+ console.log(chalk.dim(`Description: ${description}`));
131
+ console.log(chalk.dim(`URL: ${url}`));
132
+
133
+ const puter = getPuter();
134
+ try {
135
+ // Step 1: Create the app
136
+ const createAppData = await puter.apps.create({
137
+ name: name,
138
+ indexURL: url,
139
+ title: name,
140
+ description: description,
141
+ maximizeOnStart: false,
142
+ dedupeName: true
143
+ });
144
+ if (!createAppData) {
145
+ console.error(chalk.red(`Failed to create app "${name}"`));
146
+ return;
147
+ }
148
+ const appUid = createAppData.uid;
149
+ const appName = createAppData.name;
150
+ const username = createAppData.owner.username;
151
+ console.log(chalk.green(`App "${chalk.dim(name)}" created successfully!`));
152
+ console.log(chalk.cyan(`AppName: ${chalk.dim(appName)}\nUID: ${chalk.dim(appUid)}\nUsername: ${chalk.dim(username)}`));
153
+
154
+ // Step 2: Create a directory for the app
155
+ const uid = crypto.randomUUID();
156
+ const appDir = `/${username}/AppData/${appUid}`;
157
+ console.log(chalk.green(`Creating directory...\nPath: ${chalk.dim(appDir)}\nApp: ${chalk.dim(name)}\nUID: ${chalk.dim(uid)}\n`));
158
+ const createDirData = await puter.fs.mkdir(`${appDir}/app-${uid}`, {
159
+ overwrite: true,
160
+ dedupeName: false,
161
+ createMissingParents: true,
162
+ })
163
+ if (!createDirData || !createDirData.uid) {
164
+ console.error(chalk.red(`Failed to create directory for app "${name}"`));
165
+ return;
166
+ }
167
+ const dirUid = createDirData.uid;
168
+ console.log(chalk.green(`Directory created successfully!`));
169
+ console.log(chalk.cyan(`Directory UID: ${chalk.dim(dirUid)}`));
170
+
171
+ // Step 3: Create a subdomain for the app
172
+ const subdomainName = `${name}-${uid.split('-')[0]}`;
173
+ const remoteDir = `${appDir}/${createDirData.name}`;
174
+ console.log(chalk.green(`Linking to subdomain...\nSubdomain: "${chalk.dim(subdomainName)}"\nPath: ${chalk.dim(remoteDir)}\n`));
175
+ const subdomainResult = await createSubdomain(subdomainName, remoteDir);
176
+ if (!subdomainResult) {
177
+ console.error(chalk.red(`Failed to create subdomain: "${subdomainName}"`));
178
+ return;
179
+ }
180
+ console.log(chalk.green(`Subdomain created successfully!`));
181
+ console.log(chalk.cyan(`Subdomain: ${chalk.dim(subdomainName)}`));
182
+
183
+ // Step 4: Create a home page
184
+ if (localDir.length > 0){
185
+ // List files in the current "localDir" then copy them to the "remoteDir"
186
+ const files = await listRemoteFiles(localDir);
187
+ if (Array.isArray(files) && files.length > 0) {
188
+ console.log(chalk.cyan(`Copying ${chalk.dim(files.length)} files from: ${chalk.dim(localDir)}`));
189
+ console.log(chalk.cyan(`To destination: ${chalk.dim(remoteDir)}`));
190
+ for (const file of files) {
191
+ const fileSource = path.join(localDir, file.name);
192
+ await copyFile([fileSource, remoteDir]);
193
+ }
194
+ } else {
195
+ console.log(chalk.yellow("We could not find any file in the specified directory!"));
196
+ }
197
+ } else {
198
+ const homePageResult = await createFile([path.join(remoteDir, 'index.html'), getDefaultHomePage(appName)]);
199
+ if (!homePageResult){
200
+ console.log(chalk.yellow("We could not create the home page file!"));
201
+ }
202
+ }
203
+
204
+ // Step 5: Update the app's index_url to point to the subdomain
205
+ console.log(chalk.green(`Set "${chalk.dim(subdomainName)}" as a subdomain for app: "${chalk.dim(appName)}"...\n`));
206
+ const updateAppData = await puter.apps.update(appName, {
207
+ indexURL: `https://${subdomainName}.puter.site`,
208
+ title: name
209
+ })
210
+ if (!updateAppData) {
211
+ console.error(chalk.red(`Failed to update app "${name}" with new subdomain`));
212
+ return;
213
+ }
214
+ console.log(chalk.green(`App deployed successfully at:`));
215
+ console.log(chalk.cyanBright(`https://${subdomainName}.puter.site`));
216
+ } catch (error) {
217
+ console.error(chalk.red(`Failed to create app "${name}".\nError: ${error.message}`));
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Update an application from the directory
223
+ * @param {string} name The name of the App
224
+ * @param {string} remote_dir The remote directory
225
+ */
226
+ export async function updateApp(args = []) {
227
+ if (args.length < 1) {
228
+ console.log(chalk.red('Usage: app:update <valid_name_app> [<remote_dir>]'));
229
+ console.log(chalk.yellow('Example: app:create myapp'));
230
+ console.log(chalk.yellow('Example: app:create myapp ./myapp'));
231
+ return;
232
+ }
233
+ const name = args[0]; // App name (required)
234
+ // Fix: Properly handle absolute paths by checking if the path starts with '/'
235
+ let remoteDir;
236
+ if (args[1] && args[1].startsWith('/')) {
237
+ remoteDir = args[1]; // Use the absolute path as-is
238
+ } else {
239
+ remoteDir = resolvePath(getCurrentDirectory(), args[1] || '.');
240
+ }
241
+
242
+ const remoteDirExists = await pathExists(remoteDir);
243
+
244
+ if (!remoteDirExists){
245
+ console.log(chalk.red(`Cannot find directory: ${chalk.dim(remoteDir)}...\n`));
246
+ return;
247
+ }
248
+
249
+ const puter = getPuter();
250
+ console.log(chalk.green(`Updating app: "${chalk.dim(name)}" from directory: ${chalk.dim(remoteDir)}\n`));
251
+ try {
252
+ // Step 1: Get the app info
253
+ const data = await puter.apps.get(name);
254
+ if (!data) {
255
+ console.error(chalk.red(`Failed to find app: "${name}"`));
256
+ return;
257
+ }
258
+ const appUid = data.uid;
259
+ const appName = data.name;
260
+ const username = data.owner.username;
261
+ const indexUrl = data.index_url;
262
+ const appDir = `/${username}/AppData/${appUid}`;
263
+ console.log(chalk.cyan(`AppName: ${chalk.dim(appName)}\nUID: ${chalk.dim(appUid)}\nUsername: ${chalk.dim(username)}`));
264
+
265
+ // Step 2: Find the path from subdomain
266
+ const subdomains = await getSubdomains();
267
+ const appSubdomain = subdomains.find(sd => sd.root_dir?.dirname?.endsWith(appUid));
268
+ if (!appSubdomain){
269
+ console.error(chalk.red(`Sorry! We could not find the subdomain for ${chalk.cyan(name)} application.`));
270
+ return;
271
+ }
272
+ const subdomainDir = appSubdomain['root_dir']['path'];
273
+ if (!subdomainDir){
274
+ console.error(chalk.red(`Sorry! We could not find the path for ${chalk.cyan(name)} application.`));
275
+ return;
276
+ }
277
+
278
+ // Step 3: List files in the current "remoteDir" then copy them to the "subdomainDir"
279
+ const files = await listRemoteFiles(remoteDir);
280
+ if (Array.isArray(files) && files.length > 0) {
281
+ console.log(chalk.cyan(`Copying ${chalk.dim(files.length)} files from: ${chalk.dim(remoteDir)}`));
282
+ console.log(chalk.cyan(`To destination: ${chalk.dim(subdomainDir)}`));
283
+ for (const file of files) {
284
+ const fileSource = path.join(remoteDir, file.name);
285
+ const fileDest = path.join(subdomainDir, file.name);
286
+ if ((await pathExists(fileDest))){
287
+ await removeFileOrDirectory([fileDest, '-f']);
288
+ }
289
+ await copyFile([fileSource, subdomainDir]);
290
+ }
291
+ } else {
292
+ console.log(chalk.red("We could not find any file in the specified directory!"));
293
+ }
294
+
295
+ console.log(chalk.green(`App updated successfully at:`));
296
+ console.log(chalk.dim(indexUrl));
297
+ } catch (error) {
298
+ console.error(chalk.red(`Failed to update app "${name}".\nError: ${error.message}`));
299
+ console.error(error);
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Delete an app by its name
305
+ * @param {string} name The name of the app to delete
306
+ * @returns a boolean success value
307
+ */
308
+ export async function deleteApp(name) {
309
+ if (!name || name.length == 0){
310
+ console.log(chalk.red('Usage: app:delete <name>'));
311
+ return false;
312
+ }
313
+ const puter = getPuter();
314
+ console.log(chalk.green(`Checking app "${name}"...\n`));
315
+ try {
316
+ // Step 1: Read app details
317
+ const readData = await puter.apps.get(name);
318
+
319
+ if (!readData) {
320
+ console.log(chalk.red(`App "${chalk.bold(name)}" not found.`));
321
+ return false;
322
+ }
323
+
324
+ // Show app details and confirm deletion
325
+ console.log(chalk.cyan('\nApp Details:'));
326
+ console.log(chalk.dim('----------------------------------------'));
327
+ console.log(chalk.dim(`Name: ${chalk.cyan(readData.name)}`));
328
+ console.log(chalk.dim(`Title: ${chalk.cyan(readData.title)}`));
329
+ console.log(chalk.dim(`Created: ${chalk.cyan(formatDate(readData.created_at))}`));
330
+ console.log(chalk.dim(`URL: ${readData.index_url}`));
331
+ console.log(chalk.dim('----------------------------------------'));
332
+
333
+ // Step 2: Delete the app
334
+ console.log(chalk.green(`Deleting app "${chalk.red(name)}"...`));
335
+
336
+ const deleteData = await puter.apps.delete(name);
337
+ if (!deleteData) {
338
+ console.error(chalk.red(`Failed to delete app "${name}".\nP.S. Make sure to provide the 'name' attribute not the 'title'.`));
339
+ return false;
340
+ }
341
+
342
+ // Lookup subdomainUID then delete it
343
+ const subdomains = await getSubdomains();
344
+ const appSubdomain = subdomains.find(sd => sd.root_dir?.dirname?.endsWith(readData.uid));
345
+ const subdomainDeleted = await deleteSite([appSubdomain.uid]);
346
+ if (subdomainDeleted){
347
+ console.log(chalk.green(`Subdomain: ${chalk.dim(appSubdomain.uid)} deleted.`));
348
+ }
349
+
350
+ console.log(chalk.green(`App "${chalk.dim(name)}" deleted successfully!`));
351
+ } catch (error) {
352
+ console.error(chalk.red(`Failed to delete app "${name}".\nError: ${error.message}`));
353
+ return false;
354
+ }
355
+ return true;
356
+ }