puter-cli 1.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.
@@ -0,0 +1,67 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { promises as fs } from 'fs';
5
+ import path from 'path';
6
+ import { generateAppName } from './commons.js';
7
+
8
+ export async function init() {
9
+ const answers = await inquirer.prompt([
10
+ {
11
+ type: 'input',
12
+ name: 'name',
13
+ message: 'What is your app name?',
14
+ default: `${generateAppName()}`
15
+ },
16
+ {
17
+ type: 'list',
18
+ name: 'template',
19
+ message: 'Select a template:',
20
+ choices: ['basic', 'static-site', 'full-stack']
21
+ }
22
+ ]);
23
+
24
+ const spinner = ora('Creating Puter app...').start();
25
+
26
+ try {
27
+ // Create basic app structure
28
+ await createAppStructure(answers);
29
+ spinner.succeed(chalk.green('Successfully created Puter app!'));
30
+
31
+ console.log('\nNext steps:');
32
+ console.log(chalk.cyan('1. cd'), answers.name);
33
+ console.log(chalk.cyan('2. npm install'));
34
+ console.log(chalk.cyan('3. npm start'));
35
+ } catch (error) {
36
+ spinner.fail(chalk.red('Failed to create app'));
37
+ console.error(error);
38
+ }
39
+ }
40
+
41
+ async function createAppStructure({ name, template }) {
42
+ // Create project directory
43
+ await fs.mkdir(name, { recursive: true });
44
+
45
+ // Create basic files
46
+ const files = {
47
+ '.env': `APP_NAME=${name}\PUTER_API_KEY=`,
48
+ 'index.html': `<!DOCTYPE html>
49
+ <html>
50
+ <head>
51
+ <title>${name}</title>
52
+ </head>
53
+ <body>
54
+ <h1>Welcome to ${name}</h1>
55
+ <script src="https://js.puter.com/v2/"></script>
56
+ <script src="app.js"></script>
57
+ </body>
58
+ </html>`,
59
+ 'app.js': `// Initialize Puter app
60
+ console.log('Puter app initialized!');`,
61
+ 'README.md': `# ${name}\n\nA Puter app created with puter-cli`
62
+ };
63
+
64
+ for (const [filename, content] of Object.entries(files)) {
65
+ await fs.writeFile(path.join(name, filename), content);
66
+ }
67
+ }
@@ -0,0 +1,56 @@
1
+ import readline from 'node:readline';
2
+ import chalk from 'chalk';
3
+ import Conf from 'conf';
4
+ import { execCommand, getPrompt } from './executor.js';
5
+ import { getAuthToken } from './auth.js';
6
+ import { PROJECT_NAME } from './commons.js';
7
+
8
+ const config = new Conf({ projectName: PROJECT_NAME });
9
+
10
+ const rl = readline.createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout,
13
+ prompt: null
14
+ });
15
+
16
+ /**
17
+ * Update the current shell prompt
18
+ */
19
+ export function updatePrompt(currentPath) {
20
+ config.set('cwd', currentPath);
21
+ rl.setPrompt(getPrompt());
22
+ }
23
+
24
+ /**
25
+ * Start the interactive shell
26
+ */
27
+ export function startShell() {
28
+ if (!getAuthToken()) {
29
+ console.log(chalk.red('Please login first using: puter login'));
30
+ process.exit(1);
31
+ }
32
+
33
+ rl.setPrompt(getPrompt());
34
+
35
+ try {
36
+ console.log(chalk.green('Welcome to Puter-CLI! Type "help" for available commands.'));
37
+ rl.prompt();
38
+
39
+ rl.on('line', async (line) => {
40
+ const trimmedLine = line.trim();
41
+ if (trimmedLine) {
42
+ try {
43
+ await execCommand(trimmedLine);
44
+ } catch (error) {
45
+ console.error(chalk.red(error.message));
46
+ }
47
+ }
48
+ rl.prompt();
49
+ }).on('close', () => {
50
+ console.log(chalk.yellow('\nGoodbye!'));
51
+ process.exit(0);
52
+ });
53
+ } catch (error) {
54
+ console.error(chalk.red('Error starting shell:', error));
55
+ }
56
+ }
@@ -0,0 +1,214 @@
1
+ import chalk from 'chalk';
2
+ import fetch from 'node-fetch';
3
+ import Table from 'cli-table3';
4
+ import { getCurrentUserName, getCurrentDirectory } from './auth.js';
5
+ import { API_BASE, getHeaders, generateAppName, resolvePath, isValidAppName } from './commons.js';
6
+ import { displayNonNullValues, formatDate, formatDateTime } from './utils.js';
7
+ import { getSubdomains, createSubdomain, deleteSubdomain } from './subdomains.js';
8
+
9
+
10
+ /**
11
+ * Listing subdomains
12
+ */
13
+ export async function listSites(args = {}) {
14
+ try {
15
+ const data = await getSubdomains(args);
16
+
17
+ if (!data.success || !Array.isArray(data.result)) {
18
+ throw new Error('Failed to fetch subdomains');
19
+ }
20
+
21
+ // Create table instance
22
+ const table = new Table({
23
+ head: [
24
+ chalk.cyan('#'),
25
+ chalk.cyan('UID'),
26
+ chalk.cyan('Subdomain'),
27
+ chalk.cyan('Created'),
28
+ chalk.cyan('Protected'),
29
+ // chalk.cyan('Owner'),
30
+ chalk.cyan('Directory')
31
+ ],
32
+ wordWrap: false
33
+ });
34
+
35
+ // Format and add data to table
36
+ let i = 0;
37
+ data.result.forEach(domain => {
38
+ table.push([
39
+ i++,
40
+ domain.uid,
41
+ chalk.green(`${chalk.dim(domain.subdomain)}.puter.site`),
42
+ formatDate(domain.created_at).split(',')[0],
43
+ domain.protected ? chalk.red('Yes') : chalk.green('No'),
44
+ // domain.owner['username'],
45
+ domain?.root_dir?.path.split('/').pop()
46
+ ]);
47
+ });
48
+
49
+ // Print table
50
+ if (data.result.length === 0) {
51
+ console.log(chalk.yellow('No subdomains found'));
52
+ } else {
53
+ console.log(chalk.bold('\nYour Sites:'));
54
+ console.log(table.toString());
55
+ console.log(chalk.dim(`Total Sites: ${data.result.length}`));
56
+ }
57
+
58
+ } catch (error) {
59
+ console.error(chalk.red('Error listing sites:'), error.message);
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Get Site info
66
+ * @param {any[]} args Array of site uuid
67
+ */
68
+ export async function infoSite(args = []) {
69
+ if (args.length < 1){
70
+ console.log(chalk.red('Usage: site <siteUID>'));
71
+ return;
72
+ }
73
+ for (const subdomainId of args)
74
+ try {
75
+ const response = await fetch(`${API_BASE}/drivers/call`, {
76
+ method: 'POST',
77
+ headers: getHeaders(),
78
+ body: JSON.stringify({
79
+ interface: 'puter-subdomains',
80
+ method: 'read',
81
+ args: { uid: subdomainId }
82
+ })
83
+ });
84
+
85
+ if (!response.ok) {
86
+ throw new Error('Failed to fetch subdomains.');
87
+ }
88
+ const data = await response.json();
89
+ if (!data.success || !data.result) {
90
+ throw new Error(`Failed to get site info: ${data.error?.message}`);
91
+ }
92
+ displayNonNullValues(data.result);
93
+ } catch (error) {
94
+ console.error(chalk.red('Error getting site info:'), error.message);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Delete hosted web site
100
+ * @param {any[]} args Array of site uuid
101
+ */
102
+ export async function deleteSite(args = []) {
103
+ if (args.length < 1){
104
+ console.log(chalk.red('Usage: site:delete <siteUUID>'));
105
+ return false;
106
+ }
107
+ for (const uuid of args)
108
+ try {
109
+ // The uuid must be prefixed with: 'subdomainObj-'
110
+ const response = await fetch(`${API_BASE}/delete-site`, {
111
+ headers: getHeaders(),
112
+ method: 'POST',
113
+ body: JSON.stringify({
114
+ site_uuid: uuid
115
+ })
116
+ });
117
+
118
+ if (!response.ok) {
119
+ throw new Error(`Failed to delete site (Status: ${response.status})`);
120
+ }
121
+
122
+ const data = await response.json();
123
+ const result = await deleteSubdomain(uuid);
124
+ if (result){
125
+ // check if data is empty object
126
+ if (Object.keys(data).length === 0){
127
+ console.log(chalk.green(`Site ID: "${uuid}" should be deleted.`));
128
+ }
129
+ }
130
+ console.log(chalk.yellow(`Site ID: "${uuid}" may already be deleted!`));
131
+ } catch (error) {
132
+ console.error(chalk.red('Error deleting site:'), error.message);
133
+ return false;
134
+ }
135
+ return true;
136
+ }
137
+
138
+ /**
139
+ * Create a static web app from the current directory to Puter cloud.
140
+ * @param {string[]} args - Command-line arguments (e.g., [name, --subdomain=<subdomain>]).
141
+ */
142
+ export async function createSite(args = []) {
143
+ if (args.length < 1 || !isValidAppName(args[0])) {
144
+ console.log(chalk.red('Usage: site:create <valid_name_app> [<remote_dir>] [--subdomain=<subdomain>]'));
145
+ console.log(chalk.yellow('Example: site:create mysite'));
146
+ console.log(chalk.yellow('Example: site:create mysite ./mysite'));
147
+ console.log(chalk.yellow('Example: site:create mysite --subdomain=mysite'));
148
+ return;
149
+ }
150
+
151
+ const appName = args[0]; // Site name (required)
152
+ const subdomainOption = args.find(arg => arg.toLocaleLowerCase().startsWith('--subdomain='))?.split('=')[1]; // Optional subdomain
153
+ // Use the current directory as the root directory if none specified
154
+ const remoteDir = resolvePath(getCurrentDirectory(), (args[1] && !args[1].startsWith('--'))?args[1]:'.');
155
+
156
+ console.log(chalk.green(`Creating site "${appName}" from "${remoteDir}"...\n`));
157
+ try {
158
+ // Step 1: Determine the subdomain
159
+ let subdomain;
160
+ if (subdomainOption) {
161
+ subdomain = subdomainOption; // Use the provided subdomain
162
+ } else {
163
+ subdomain = appName; // Default to the app name as the subdomain
164
+ }
165
+
166
+ // Step 2: Check if the subdomain already exists
167
+ const data = await getSubdomains();
168
+ if (!data.success || !Array.isArray(data.result)) {
169
+ throw new Error('Failed to fetch subdomains');
170
+ }
171
+
172
+ const subdomains = data.result;
173
+ const subdomainObj = subdomains.find(sd => sd.subdomain === subdomain);
174
+ if (subdomainObj) {
175
+ console.error(chalk.cyan(`The subdomain "${subdomain}" is already in use and owned by: "${subdomainObj.owner['username']}"`));
176
+ if (subdomainObj.owner['username'] === getCurrentUserName()){
177
+ console.log(chalk.green(`It's yours, and linked to: ${subdomainObj.root_dir?.path}`));
178
+ if (subdomainObj.root_dir?.path === remoteDir){
179
+ console.log(chalk.cyan(`Which is already the selected directory, and created at:`));
180
+ console.log(chalk.green(`https://${subdomain}.puter.site`));
181
+ return;
182
+ } else {
183
+ console.log(chalk.yellow(`However, It's linked to different directory at: ${subdomainObj.root_dir?.path}`));
184
+ console.log(chalk.cyan(`We'll try to unlink this subdomain from that directory...`));
185
+ const result = await deleteSubdomain(subdomainObj.uid);
186
+ if (result) {
187
+ console.log(chalk.green('Looks like this subdomain is free again, please try again.'));
188
+ return;
189
+ } else {
190
+ console.log(chalk.red('Could not release this subdomain.'));
191
+ }
192
+ }
193
+ }
194
+ } else {
195
+ console.log(chalk.yellow(`The subdomain: "${subdomain}" is already taken, so let's generate a new random one:`));
196
+ subdomain = generateAppName(); // Generate a random subdomain
197
+ console.log(chalk.cyan(`New generated subdomain: "${subdomain}" will be used.`));
198
+ }
199
+
200
+ // Step 3: Host the current directory under the subdomain
201
+ console.log(chalk.cyan(`Hosting app "${appName}" under subdomain "${subdomain}"...`));
202
+ const site = await createSubdomain(subdomain, remoteDir);
203
+ if (!site){
204
+ console.error(chalk.red(`Failed to create subdomain: "${chalk.red(subdomain)}"`));
205
+ return;
206
+ }
207
+
208
+ console.log(chalk.green(`App "${chalk.red(appName)}" created successfully at:`));
209
+ console.log(chalk.dim(`https://${site.subdomain}.puter.site`));
210
+ } catch (error) {
211
+ console.error(chalk.red('Failed to create site.'));
212
+ console.error(chalk.red(`Error: ${error.message}`));
213
+ }
214
+ }
@@ -0,0 +1,103 @@
1
+ import chalk from 'chalk';
2
+ import fetch from 'node-fetch';
3
+ import { API_BASE, getHeaders } from './commons.js';
4
+
5
+ /**
6
+ * Get list of subdomains.
7
+ * @param {Object} args - Options for the query.
8
+ * @returns {Array} - Array of subdomains.
9
+ */
10
+ export async function getSubdomains(args = {}) {
11
+ const response = await fetch(`${API_BASE}/drivers/call`, {
12
+ method: 'POST',
13
+ headers: getHeaders(),
14
+ body: JSON.stringify({
15
+ interface: 'puter-subdomains',
16
+ method: 'select',
17
+ args: args
18
+ })
19
+ });
20
+
21
+ if (!response.ok) {
22
+ throw new Error('Failed to fetch subdomains.');
23
+ }
24
+ return await response.json();
25
+ }
26
+
27
+ /**
28
+ * Delete a subdomain by id
29
+ * @param {Array} subdomain IDs
30
+ * @return {boolean} Result of the operation
31
+ */
32
+ export async function deleteSubdomain(args = []) {
33
+ if (args.length < 1){
34
+ console.log(chalk.red('Usage: domain:delete <subdomain_id>'));
35
+ return false;
36
+ }
37
+ const subdomains = args;
38
+ for (const subdomainId of subdomains)
39
+ try {
40
+ const response = await fetch(`${API_BASE}/drivers/call`, {
41
+ headers: getHeaders(),
42
+ method: 'POST',
43
+ body: JSON.stringify({
44
+ interface: 'puter-subdomains',
45
+ method: 'delete',
46
+ args: {
47
+ id: { subdomain: subdomainId }
48
+ }
49
+ })
50
+ });
51
+
52
+ const data = await response.json();
53
+ if (!data.success) {
54
+ if (data.error?.code === 'entity_not_found') {
55
+ console.log(chalk.red(`Subdomain ID: "${subdomainId}" not found`));
56
+ return false;
57
+ }
58
+ console.log(chalk.red(`Failed to delete subdomain: ${data.error?.message}`));
59
+ return false;
60
+ }
61
+ console.log(chalk.green('Subdomain deleted successfully'));
62
+ } catch (error) {
63
+ console.error(chalk.red('Error deleting subdomain:'), error.message);
64
+ }
65
+ return true;
66
+ }
67
+
68
+ /**
69
+ * Create a new subdomain into remote directory
70
+ * @param {string} subdomain - Subdomain name.
71
+ * @param {string} remoteDir - Remote directory path.
72
+ * @returns {Object} - Hosting details (e.g., subdomain).
73
+ */
74
+ export async function createSubdomain(subdomain, remoteDir) {
75
+ const response = await fetch(`${API_BASE}/drivers/call`, {
76
+ method: 'POST',
77
+ headers: getHeaders(),
78
+ body: JSON.stringify({
79
+ interface: 'puter-subdomains',
80
+ method: 'create',
81
+ args: {
82
+ object: {
83
+ subdomain: subdomain,
84
+ root_dir: remoteDir
85
+ }
86
+ }
87
+ })
88
+ });
89
+
90
+ if (!response.ok) {
91
+ throw new Error('Failed to host directory.');
92
+ }
93
+ const data = await response.json();
94
+ if (!data.success || !data.result) {
95
+ if (data.error?.code === 'already_in_use') {
96
+ console.log(chalk.yellow(`Subdomain already taken!\nMessage: ${data?.error?.message}`));
97
+ return false;
98
+ }
99
+ console.log(chalk.red(`Error when creating "${subdomain}".\nError: ${data?.error?.message}\nCode: ${data.error?.code}`));
100
+ return false;
101
+ }
102
+ return data.result;
103
+ }
@@ -0,0 +1,92 @@
1
+ import chalk from 'chalk';
2
+
3
+ /**
4
+ * Convert "2024-10-07T15:03:53.000Z" to "10/7/2024, 15:03:53"
5
+ * @param {Date} value date value
6
+ * @returns formatted date string
7
+ */
8
+ export function formatDate(value) {
9
+ const date = new Date(value);
10
+ return date.toLocaleString("en-US", {
11
+ year: "numeric",
12
+ month: "2-digit",
13
+ day: "2-digit",
14
+ hour: "2-digit",
15
+ minute: "2-digit",
16
+ second: "2-digit",
17
+ hour12: false
18
+ });
19
+ }
20
+
21
+ /**
22
+ * Format timestamp to date or time
23
+ * @param {number} timestamp value
24
+ * @returns string
25
+ */
26
+ export function formatDateTime(timestamp) {
27
+ const date = new Date(timestamp * 1000); // Convert to milliseconds
28
+ const now = new Date();
29
+ const diff = now - date;
30
+ if (diff < 86400000) { // Less than 24 hours
31
+ return date.toLocaleTimeString();
32
+ } else {
33
+ return date.toLocaleDateString();
34
+ }
35
+ }
36
+ /**
37
+ * Format file size in human readable format
38
+ * @param {number} size File size value
39
+ * @returns string formatted in human readable format
40
+ */
41
+ export function formatSize(size) {
42
+ if (size === null || size === undefined) return '0';
43
+ if (size === 0) return '0';
44
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
45
+ let unit = 0;
46
+ while (size >= 1024 && unit < units.length - 1) {
47
+ size /= 1024;
48
+ unit++;
49
+ }
50
+ return `${size.toFixed(1)} ${units[unit]}`;
51
+ }
52
+
53
+ /**
54
+ * Display non null values in formatted table
55
+ * @param {Object} data Object to display
56
+ * @returns null
57
+ */
58
+ export function displayNonNullValues(data) {
59
+ if (typeof data !== 'object' || data === null) {
60
+ console.error("Invalid input: Input must be a non-null object.");
61
+ return;
62
+ }
63
+ const tableData = [];
64
+ function flattenObject(obj, parentKey = '') {
65
+ for (const key in obj) {
66
+ const value = obj[key];
67
+ const newKey = parentKey ? `${parentKey}.${key}` : key;
68
+ if (value !== null) {
69
+ if (typeof value === 'object') {
70
+ flattenObject(value, newKey);
71
+ } else {
72
+ tableData.push({ key: newKey, value: value });
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ flattenObject(data);
79
+ // Determine max key length for formatting
80
+ const maxKeyLength = tableData.reduce((max, item) => Math.max(max, item.key.length), 0);
81
+ // Format and output the table
82
+ console.log(chalk.cyan('-'.repeat(maxKeyLength*3)));
83
+ console.log(chalk.cyan(`| ${'Key'.padEnd(maxKeyLength)} | Value`));
84
+ console.log(chalk.cyan('-'.repeat(maxKeyLength*3)));
85
+ tableData.forEach(item => {
86
+ const key = item.key.padEnd(maxKeyLength);
87
+ const value = String(item.value);
88
+ console.log(chalk.green(`| ${chalk.dim(key)} | ${value}`));
89
+ });
90
+ console.log(chalk.cyan('-'.repeat(maxKeyLength*3)));
91
+ console.log(chalk.cyan(`You have ${chalk.green(tableData.length)} key/value pair(s).`));
92
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "puter-cli",
3
+ "version": "1.0.0",
4
+ "description": "Command line interface for Puter cloud platform",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "puter": "./bin/index.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "start": "node bin/index.js",
12
+ "test": "vitest run tests/*",
13
+ "test:watch": "vitest --watch tests/*"
14
+ },
15
+ "keywords": [
16
+ "puter",
17
+ "cli",
18
+ "cloud"
19
+ ],
20
+ "author": "Ibrahim.H",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "chalk": "^5.3.0",
24
+ "cli-table3": "^0.6.5",
25
+ "commander": "^11.1.0",
26
+ "conf": "^12.0.0",
27
+ "cross-spawn": "^7.0.3",
28
+ "glob": "^11.0.0",
29
+ "inquirer": "^9.2.12",
30
+ "minimatch": "^10.0.1",
31
+ "node-fetch": "^3.3.2",
32
+ "ora": "^8.0.1"
33
+ },
34
+ "devDependencies": {
35
+ "vitest": "^2.1.8"
36
+ }
37
+ }