puter-cli 1.1.3 → 1.1.5

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/README.md CHANGED
@@ -148,7 +148,7 @@ P.S. Please check the help command `help apps` for more details about any argume
148
148
 
149
149
  - **Create Application**: Create a new application.
150
150
  ```bash
151
- puter app:create <name> [<remote_dir>] [--description=<description>] [--url=<url>]
151
+ puter app:create <name> [<directory>] [--description="My App Description"] [--url=<url>]
152
152
  ```
153
153
  P.S. By default a new `index.html` with basic content will be created, but you can set a directory when you create a new application as follows: `app:create nameOfApp ./appDir`, so all files will be copied to the `AppData` directoy, you can then update your app using `app:update <name> <remote_dir>`. This command will attempt to create a subdomain with a random `uid` prefixed with the name of the app.
154
154
 
@@ -170,7 +170,7 @@ The static sites are served from the selected directory (or the current director
170
170
 
171
171
  - **Deploy Site**: Deploy a static website from a directory.
172
172
  ```bash
173
- puter site:create <dir> [--subdomain=<name>]
173
+ puter site:create <app_name> [<dir>] [--subdomain=<name>]
174
174
  ```
175
175
  P.S. If the subdomain already exists, it will generate a new random one can set your own subdomain using `--subdomain` argument.
176
176
 
package/bin/index.js CHANGED
@@ -5,7 +5,6 @@ import { init } from '../commands/init.js';
5
5
  import { startShell } from '../commands/shell.js';
6
6
  import { PROJECT_NAME, getLatestVersion } from '../commands/commons.js';
7
7
 
8
-
9
8
  async function main() {
10
9
  const { version } = await getLatestVersion(PROJECT_NAME);
11
10
 
@@ -35,10 +34,32 @@ async function main() {
35
34
  .description('Start interactive shell')
36
35
  .action(startShell);
37
36
 
37
+
38
+ // App commands
39
+ program
40
+ .command('app:create <name>')
41
+ .description('Create a new Puter application')
42
+ .argument('<name>', 'Name of the application')
43
+ .argument('[directory]', 'Local directory path')
44
+ .option('-d, --description <description>', 'Application description')
45
+ .option('-u, --url <url>', 'Application URL', 'https://dev-center.puter.com/coming-soon.html')
46
+ .action(async (name, directory, options) => {
47
+ try {
48
+ await createApp({
49
+ name,
50
+ directory: directory || '',
51
+ description: options.description || '',
52
+ url: options.url
53
+ });
54
+ } catch (error) {
55
+ console.error(chalk.red(error.message));
56
+ }
57
+ });
58
+
38
59
  if (process.argv.length === 2) {
39
60
  startShell();
40
61
  } else {
41
- program.parse();
62
+ program.parse(process.argv);
42
63
  }
43
64
  }
44
65
 
package/commands/apps.js CHANGED
@@ -8,6 +8,7 @@ import { createSubdomain, getSubdomains } from './subdomains.js';
8
8
  import { deleteSite } from './sites.js';
9
9
  import { copyFile, createFile, listRemoteFiles, pathExists, removeFileOrDirectory } from './files.js';
10
10
  import { getCurrentDirectory } from './auth.js';
11
+ import crypto from './crypto.js';
11
12
 
12
13
  /**
13
14
  * List all apps
@@ -125,25 +126,28 @@ export async function appInfo(args = []) {
125
126
  /**
126
127
  * Create a new web application
127
128
  * @param {string} name The name of the App
129
+ * @param {string} directory Optional directory path
128
130
  * @param {string} description A description of the App
129
131
  * @param {string} url A default coming-soon URL
130
- * @returns Output JSON data
132
+ * @returns {Promise<Object>} Output JSON data
131
133
  */
132
- export async function createApp(args = []) {
133
- if (args.length < 1 || !isValidAppName(args[0])) {
134
- console.log(chalk.red('Usage: app:create <valid_name_app> [<remote_dir>] [--description=<description>] [--url=<url>]'));
135
- console.log(chalk.yellow('Example: app:create myapp'));
136
- console.log(chalk.yellow('Example: app:create myapp ./myapp'));
137
- console.log(chalk.yellow('Example: app:create myapp --description=myapp'));
138
- return;
134
+ export async function createApp(args) {
135
+ const name = args.name; // App name (required)
136
+ console.log(args);
137
+ if (!isValidAppName(name)) {
138
+ throw new Error('Invalid application name');
139
139
  }
140
- const name = args[0]; // App name (required)
141
140
  // Use the default home page if the root directory if none specified
142
- const localDir = (args[1] && !args[1].startsWith('--'))? resolvePath(getCurrentDirectory(), args[1]):'';
143
- const description = (args.find(arg => arg.toLocaleLowerCase().startsWith('--description='))?.split('=')[1]) || ''; // Optional description
144
- const url = (args.find(arg => arg.toLocaleLowerCase().startsWith('--url='))?.split('=')[1]) || 'https://dev-center.puter.com/coming-soon.html'; // Optional url
141
+ const localDir = args.directory ? resolvePath(getCurrentDirectory(), args.directory) : '';
142
+ // Optional description
143
+ const description = args.description || '';
144
+ const url = args.url || '';
145
+
146
+ console.log(chalk.green(`Creating app "${name}"...`));
147
+ console.log(chalk.dim(`Directory: ${localDir || '[default]'}`));
148
+ console.log(chalk.dim(`Description: ${description}`));
149
+ console.log(chalk.dim(`URL: ${url}`));
145
150
 
146
- console.log(chalk.green(`Creating app: "${chalk.dim(name)}"...\n`));
147
151
  try {
148
152
  // Step 1: Create the app
149
153
  const createAppResponse = await fetch(`${API_BASE}/drivers/call`, {
@@ -379,7 +383,7 @@ export async function deleteApp(name) {
379
383
  const readData = await readResponse.json();
380
384
 
381
385
  if (!readData.success || !readData.result) {
382
- console.log(chalk.log(`App "${chalk.red(name)}" not found.`));
386
+ console.log(chalk.red(`App "${chalk.bold(name)}" not found.`));
383
387
  return false;
384
388
  }
385
389
 
@@ -0,0 +1,176 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { TextEncoder } from 'util';
3
+
4
+ class Hash {
5
+ constructor(algorithm) {
6
+ this.algorithm = algorithm;
7
+ this.data = [];
8
+ }
9
+
10
+ update(data) {
11
+ if (typeof data === 'string') {
12
+ const encoder = new TextEncoder();
13
+ data = encoder.encode(data);
14
+ }
15
+ this.data.push(Buffer.from(data));
16
+ return this;
17
+ }
18
+
19
+ async digest(encoding = 'hex') {
20
+ const concatenatedData = Buffer.concat(this.data);
21
+ const hashBuffer = await crypto.subtle.digest(
22
+ this.algorithm.toUpperCase(),
23
+ concatenatedData
24
+ );
25
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
26
+
27
+ if (encoding === 'buffer') {
28
+ return Buffer.from(hashArray);
29
+ }
30
+
31
+ const hashHex = hashArray
32
+ .map(byte => byte.toString(16).padStart(2, '0'))
33
+ .join('');
34
+
35
+ if (encoding === 'hex') return hashHex;
36
+ if (encoding === 'base64') return Buffer.from(hashHex, 'hex').toString('base64');
37
+
38
+ throw new Error(`Unsupported encoding: ${encoding}`);
39
+ }
40
+ }
41
+
42
+ class Hmac {
43
+ constructor(algorithm, key) {
44
+ this.algorithm = algorithm;
45
+ this.key = typeof key === 'string' ? Buffer.from(key) : key;
46
+ this.data = [];
47
+ }
48
+
49
+ update(data) {
50
+ if (typeof data === 'string') {
51
+ const encoder = new TextEncoder();
52
+ data = encoder.encode(data);
53
+ }
54
+ this.data.push(Buffer.from(data));
55
+ return this;
56
+ }
57
+
58
+ async digest(encoding = 'hex') {
59
+ const concatenatedData = Buffer.concat(this.data);
60
+ const key = await crypto.subtle.importKey(
61
+ 'raw',
62
+ this.key,
63
+ { name: 'HMAC', hash: { name: this.algorithm.toUpperCase() } },
64
+ false,
65
+ ['sign']
66
+ );
67
+
68
+ const signature = await crypto.subtle.sign(
69
+ 'HMAC',
70
+ key,
71
+ concatenatedData
72
+ );
73
+
74
+ const hashArray = Array.from(new Uint8Array(signature));
75
+
76
+ if (encoding === 'buffer') {
77
+ return Buffer.from(hashArray);
78
+ }
79
+
80
+ const hashHex = hashArray
81
+ .map(byte => byte.toString(16).padStart(2, '0'))
82
+ .join('');
83
+
84
+ if (encoding === 'hex') return hashHex;
85
+ if (encoding === 'base64') return Buffer.from(hashHex, 'hex').toString('base64');
86
+
87
+ throw new Error(`Unsupported encoding: ${encoding}`);
88
+ }
89
+ }
90
+
91
+ const randomBytes = (size) => {
92
+ const array = new Uint8Array(size);
93
+ crypto.getRandomValues(array);
94
+ return Buffer.from(array);
95
+ };
96
+
97
+ const createHash = (algorithm) => {
98
+ return new Hash(algorithm);
99
+ };
100
+
101
+ const createHmac = (algorithm, key) => {
102
+ return new Hmac(algorithm, key);
103
+ };
104
+
105
+ const randomUUID = () => {
106
+ return uuidv4();
107
+ };
108
+
109
+ const scrypt = async (password, salt, keylen, options = {}) => {
110
+ const encoder = new TextEncoder();
111
+ const passwordBuffer = encoder.encode(password);
112
+ const saltBuffer = encoder.encode(salt);
113
+
114
+ const N = options.N || 16384;
115
+ const r = options.r || 8;
116
+ const p = options.p || 1;
117
+
118
+ const key = await crypto.subtle.importKey(
119
+ 'raw',
120
+ passwordBuffer,
121
+ 'PBKDF2',
122
+ false,
123
+ ['deriveBits']
124
+ );
125
+
126
+ const derivedKey = await crypto.subtle.deriveBits(
127
+ {
128
+ name: 'PBKDF2',
129
+ salt: saltBuffer,
130
+ iterations: N * r * p,
131
+ hash: 'SHA-256'
132
+ },
133
+ key,
134
+ keylen * 8
135
+ );
136
+
137
+ return Buffer.from(derivedKey);
138
+ };
139
+
140
+ const pbkdf2 = async (password, salt, iterations, keylen, digest) => {
141
+ const encoder = new TextEncoder();
142
+ const passwordBuffer = encoder.encode(password);
143
+ const saltBuffer = encoder.encode(salt);
144
+
145
+ const key = await crypto.subtle.importKey(
146
+ 'raw',
147
+ passwordBuffer,
148
+ 'PBKDF2',
149
+ false,
150
+ ['deriveBits']
151
+ );
152
+
153
+ const derivedKey = await crypto.subtle.deriveBits(
154
+ {
155
+ name: 'PBKDF2',
156
+ salt: saltBuffer,
157
+ iterations,
158
+ hash: digest.toUpperCase()
159
+ },
160
+ key,
161
+ keylen * 8
162
+ );
163
+
164
+ return Buffer.from(derivedKey);
165
+ };
166
+
167
+ export default {
168
+ createHash,
169
+ createHmac,
170
+ randomBytes,
171
+ randomUUID,
172
+ scrypt,
173
+ pbkdf2,
174
+ Hash,
175
+ Hmac
176
+ };
@@ -1,5 +1,4 @@
1
1
  import chalk from 'chalk';
2
- import ora from 'ora';
3
2
  import Conf from 'conf';
4
3
  import { listApps, appInfo, createApp, updateApp, deleteApp } from './apps.js';
5
4
  import { listSites, createSite, deleteSite, infoSite } from './sites.js';
@@ -11,6 +10,7 @@ import { getUserInfo, getUsageInfo } from './auth.js';
11
10
  import { PROJECT_NAME, API_BASE, getHeaders } from './commons.js';
12
11
  import inquirer from 'inquirer';
13
12
  import { exec } from 'node:child_process';
13
+ import { parseArgs } from './utils.js';
14
14
 
15
15
  const config = new Conf({ projectName: PROJECT_NAME });
16
16
 
@@ -37,12 +37,28 @@ const commands = {
37
37
  });
38
38
  },
39
39
  app: appInfo,
40
- 'app:create': async (args) => {
41
- if (args.length < 1) {
42
- console.log(chalk.red('Usage: app:create <name> [<remote_dir>] [--description=<description>] [--url=<url>]'));
40
+ 'app:create': async (rawArgs) => {
41
+ try {
42
+ const args = parseArgs(rawArgs.join(' '));
43
+ // Consider using explicit argument definition if necessary
44
+ // const args = parseArgs(rawArgs.join(' '), {string: ['description', 'url'],
45
+ // alias: { d: 'description', u: 'url', },
46
+ // });
47
+
48
+ if (!args.length < 1) {
49
+ console.log(chalk.red('Usage: app:create <name> [directory] [--description="My App Description"] [--url=app-url]'));
43
50
  return;
51
+ }
52
+
53
+ await createApp({
54
+ name: args._[0],
55
+ directory: args._[1] || '',
56
+ description: args.description || '',
57
+ url: args.url || 'https://dev-center.puter.com/coming-soon.html'
58
+ });
59
+ } catch (error) {
60
+ console.error(chalk.red(error.message));
44
61
  }
45
- await createApp(args);
46
62
  },
47
63
  'app:update': async (args) => {
48
64
  if (args.length < 1) {
@@ -51,13 +67,20 @@ const commands = {
51
67
  }
52
68
  await updateApp(args);
53
69
  },
54
- 'app:delete': async (args) => {
70
+ 'app:delete': async (rawArgs) => {
71
+ const args = parseArgs(rawArgs.join(' '), {
72
+ string: ['_'],
73
+ boolean: ['f'],
74
+ configuration: {
75
+ 'populate--': true
76
+ }
77
+ });
55
78
  if (args.length < 1) {
56
79
  console.log(chalk.red('Usage: app:delete <name>'));
57
80
  return;
58
81
  }
59
- const name = args.find(arg => arg !=='-f')
60
- const force = args.some(arg => arg =='-f')? true: false;
82
+ const name = args._[0];
83
+ const force = !!args.f;
61
84
 
62
85
  if (!force){
63
86
  const { confirm } = await inquirer.prompt([
@@ -144,6 +167,7 @@ export async function execCommand(input) {
144
167
  * @param {string} [command] - The command to display help for.
145
168
  */
146
169
  function showHelp(command) {
170
+ // Consider using `program.helpInformation()` function for global "help" command...
147
171
  const commandHelp = {
148
172
  help: `
149
173
  ${chalk.cyan('help [command]')}
@@ -187,9 +211,9 @@ function showHelp(command) {
187
211
  Example: app myapp
188
212
  `,
189
213
  'app:create': `
190
- ${chalk.cyan('app:create <name> [url]')}
214
+ ${chalk.cyan('app:create <name> [<remote_dir>] [--description="<description>"] [--url=<url>]')}
191
215
  Create a new app.
192
- Example: app:create myapp https://example.com
216
+ Example: app:create myapp https://myapp.puter.site
193
217
  `,
194
218
  'app:update': `
195
219
  ${chalk.cyan('app:update <name> [dir]')}
@@ -279,9 +303,9 @@ function showHelp(command) {
279
303
  Example: site:delete sd-123456
280
304
  `,
281
305
  'site:create': `
282
- ${chalk.cyan('site:create <dir> [--subdomain=<name>]')}
306
+ ${chalk.cyan('site:create <app_name> [<dir>] [--subdomain=<name>]')}
283
307
  Create a static website from directory.
284
- Example: site:create /path/to/dir --subdomain=myapp
308
+ Example: site:create mywebsite /path/to/dir --subdomain=mywebsite
285
309
  `,
286
310
  '!': `
287
311
  ${chalk.cyan('!<command>')}
package/commands/files.js CHANGED
@@ -11,6 +11,7 @@ import { formatDate, formatDateTime, formatSize } from './utils.js';
11
11
  import inquirer from 'inquirer';
12
12
  import { getAuthToken, getCurrentDirectory, getCurrentUserName } from './auth.js';
13
13
  import { updatePrompt } from './shell.js';
14
+ import crypto from './crypto.js';
14
15
 
15
16
  const config = new Conf({ projectName: PROJECT_NAME });
16
17
 
package/commands/sites.js CHANGED
@@ -153,7 +153,7 @@ export async function infoSite(args = []) {
153
153
  // Use the current directory as the root directory if none specified
154
154
  const remoteDir = resolvePath(getCurrentDirectory(), (args[1] && !args[1].startsWith('--'))?args[1]:'.');
155
155
 
156
- console.log(chalk.green(`Creating site "${appName}" from "${remoteDir}"...\n`));
156
+ console.log(chalk.dim(`Creating site ${chalk.green(appName)} from: ${chalk.green(remoteDir)}...\n`));
157
157
  try {
158
158
  // Step 1: Determine the subdomain
159
159
  let subdomain;
@@ -182,7 +182,7 @@ export async function infoSite(args = []) {
182
182
  } else {
183
183
  console.log(chalk.yellow(`However, It's linked to different directory at: ${subdomainObj.root_dir?.path}`));
184
184
  console.log(chalk.cyan(`We'll try to unlink this subdomain from that directory...`));
185
- const result = await deleteSubdomain(subdomainObj.uid);
185
+ const result = await deleteSubdomain(subdomainObj?.uid);
186
186
  if (result) {
187
187
  console.log(chalk.green('Looks like this subdomain is free again, please try again.'));
188
188
  return;
@@ -205,8 +205,8 @@ export async function infoSite(args = []) {
205
205
  return;
206
206
  }
207
207
 
208
- console.log(chalk.green(`App "${chalk.red(appName)}" created successfully at:`));
209
- console.log(chalk.dim(`https://${site.subdomain}.puter.site`));
208
+ console.log(chalk.green(`App ${chalk.dim(appName)} created successfully and accessible at:`));
209
+ console.log(chalk.cyan(`https://${site.subdomain}.puter.site`));
210
210
  } catch (error) {
211
211
  console.error(chalk.red('Failed to create site.'));
212
212
  console.error(chalk.red(`Error: ${error.message}`));
package/commands/utils.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import yargsParser from 'yargs-parser';
2
3
 
3
4
  /**
4
5
  * Convert "2024-10-07T15:03:53.000Z" to "10/7/2024, 15:03:53"
@@ -89,4 +90,14 @@ export function displayNonNullValues(data) {
89
90
  });
90
91
  console.log(chalk.cyan('-'.repeat(maxKeyLength*3)));
91
92
  console.log(chalk.cyan(`You have ${chalk.green(tableData.length)} key/value pair(s).`));
92
- }
93
+ }
94
+
95
+ /**
96
+ * Parse command line arguments including quoted strings
97
+ * @param {string} input Raw command line input
98
+ * @returns {Object} Parsed arguments
99
+ */
100
+ export function parseArgs(input, options = {}) {
101
+ const result = yargsParser(input, options);
102
+ return result;
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "puter-cli",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Command line interface for Puter cloud platform",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -26,14 +26,16 @@
26
26
  "dependencies": {
27
27
  "chalk": "^5.3.0",
28
28
  "cli-table3": "^0.6.5",
29
- "commander": "^11.1.0",
29
+ "commander": "^13.0.0",
30
30
  "conf": "^12.0.0",
31
31
  "cross-spawn": "^7.0.3",
32
32
  "glob": "^11.0.0",
33
33
  "inquirer": "^9.2.12",
34
34
  "minimatch": "^10.0.1",
35
35
  "node-fetch": "^3.3.2",
36
- "ora": "^8.0.1"
36
+ "ora": "^8.0.1",
37
+ "uuid": "^11.0.5",
38
+ "yargs-parser": "^21.1.1"
37
39
  },
38
40
  "devDependencies": {
39
41
  "vitest": "^2.1.8"