puter-cli 1.5.1 → 1.5.3

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/CHANGELOG.md CHANGED
@@ -4,8 +4,27 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ #### [v1.5.3](https://github.com/HeyPuter/puter-cli/compare/v1.5.2...v1.5.3)
8
+
9
+ - refactor: early return in fallback behavior [`#6`](https://github.com/HeyPuter/puter-cli/pull/6)
10
+ - fix: ignore undefined appDir when listing sites [`#5`](https://github.com/HeyPuter/puter-cli/pull/5)
11
+ - fix: use array args when calling DeleteSubdomain [`#4`](https://github.com/HeyPuter/puter-cli/pull/4)
12
+ - Update README.md [`#3`](https://github.com/HeyPuter/puter-cli/pull/3)
13
+ - fix: improve app uuid check [`49dcc7f`](https://github.com/HeyPuter/puter-cli/commit/49dcc7f1220a58987601e8054b0d4a450cd02afe)
14
+ - fix: potential timezone issues [`ba0e4b1`](https://github.com/HeyPuter/puter-cli/commit/ba0e4b183efb82a0c252e5c6250d976f1efe19cd)
15
+
16
+ #### [v1.5.2](https://github.com/HeyPuter/puter-cli/compare/v1.5.1...v1.5.2)
17
+
18
+ > 6 February 2025
19
+
20
+ - chore: clean unused [`e5c7fcc`](https://github.com/HeyPuter/puter-cli/commit/e5c7fcca3096b4b2c0659d84d8de63dce039a765)
21
+ - chore: more details [`0a25a2f`](https://github.com/HeyPuter/puter-cli/commit/0a25a2fb7efa9a67033b4e483305da44a68f2c9a)
22
+ - docs: badge version [`503bc66`](https://github.com/HeyPuter/puter-cli/commit/503bc6667666b14f85245887a3bf2071711dc4e1)
23
+
7
24
  #### [v1.5.1](https://github.com/HeyPuter/puter-cli/compare/v1.5.0...v1.5.1)
8
25
 
26
+ > 5 February 2025
27
+
9
28
  - imporve listing uid [`e2573f8`](https://github.com/HeyPuter/puter-cli/commit/e2573f83df6b47d8ab32ffc66ab19a9c984dd250)
10
29
 
11
30
  #### [v1.5.0](https://github.com/HeyPuter/puter-cli/compare/v1.4.4...v1.5.0)
package/README.md CHANGED
@@ -9,6 +9,7 @@
9
9
  <a href="https://opensource.org/licenses/MIT" >
10
10
  <img alt="License" src="https://img.shields.io/badge/License-MIT-yellow.svg">
11
11
  </a>
12
+ <a href="https://www.npmjs.com/package/puter-cli"><img src="https://img.shields.io/npm/v/puter-cli?color=729B1B&label="></a>
12
13
  </p>
13
14
 
14
15
 
@@ -52,12 +53,12 @@ puter --version
52
53
 
53
54
  ### Commands
54
55
 
55
- #### Initilize a project
56
- - **Create a new project**: Initilize a new project
56
+ #### Initialize a project
57
+ - **Create a new project**: Initialize a new project
57
58
  ```bash
58
59
  puter init
59
60
  ```
60
- Then just follow the prompts, this command doesn't require you to login.
61
+ Then just follow the prompts, this command doesn't require you to log in.
61
62
 
62
63
  #### Authentication
63
64
  - **Login**: Log in to your Puter account.
@@ -71,7 +72,7 @@ Then just follow the prompts, this command doesn't require you to login.
71
72
 
72
73
  #### File Management
73
74
 
74
- We've adopted the most basic popluar linux system command line for daily file manipulation with some extra features, not out of the box though, we want to keep it simple.
75
+ We've adopted the most basic popular Linux system command line for daily file manipulation with some extra features, not out of the box though, we want to keep it simple.
75
76
 
76
77
  - **List Files**: List files and directories.
77
78
  ```bash
@@ -156,7 +157,7 @@ P.S. Please check the help command `help apps` for more details about any argume
156
157
  ```bash
157
158
  puter> app:create <name> [<directory>] [--description="My App Description"] [--url=<url>]
158
159
  ```
159
- 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.
160
+ 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` directory, 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.
160
161
 
161
162
  - **Update Application**: Update an application.
162
163
  ```bash
@@ -168,7 +169,7 @@ P.S. By default a new `index.html` with basic content will be created, but you c
168
169
  ```bash
169
170
  puter> app:delete [-f] <name>
170
171
  ```
171
- P.S. This command will lookup for the allocated `subdomain` and attempt to delete it if it exists.
172
+ P.S. This command will look for the allocated `subdomain` and attempt to delete it if it exists.
172
173
 
173
174
  #### Static Sites
174
175
 
@@ -178,7 +179,7 @@ The static sites are served from the selected directory (or the current director
178
179
  ```bash
179
180
  puter> site:create <app_name> [<dir>] [--subdomain=<name>]
180
181
  ```
181
- P.S. If the subdomain already exists, it will generate a new random one can set your own subdomain using `--subdomain` argument.
182
+ P.S. If the subdomain already exists, it will generate a new random one. You can set your own subdomain using `--subdomain` argument.
182
183
 
183
184
  - **List Sites**: List all hosted sites.
184
185
  ```bash
@@ -281,7 +282,7 @@ If you want to customize this tool you can follow these steps:
281
282
 
282
283
  ## Known issues:
283
284
 
284
- Most of the functionalities are just working fine, however some APIs related to Puter's SDK have some known issues. We tried to fix most them but some of them are not related to us, so we let you about that in case it'll be fixed by Puter's in the future:
285
+ Most of the functionalities are just working fine, however, some APIs related to Puter's SDK have some known issues. We tried to fix most of them but some of them are not related to us, so we let you about that in case it'll be fixed by Puter's in the future:
285
286
 
286
287
  ## Delete a subdomain
287
288
  When you try to delete a subdomain which you own, you'll get `Permission denied`:
@@ -289,7 +290,7 @@ When you try to delete a subdomain which you own, you'll get `Permission denied`
289
290
  Failed to delete subdomain: Permission denied.
290
291
  Site ID: "sd-b019b654-e06f-48a8-917e-ae1e83825ab7" may already be deleted!
291
292
  ```
292
- However, the query is executed successfully at the cloud and the subdomain is actually deleted.
293
+ However, the query is executed successfully in the cloud and the subdomain is actually deleted.
293
294
 
294
295
  ## Interactive Shell prompt:
295
296
  If you want to stay in the interactive shell you should provide "-f" (aka: force delete) argument, when want to delete any object:
@@ -309,7 +310,7 @@ Otherwise, the Interactive Shell mode will be terminated.
309
310
 
310
311
  ## Notes
311
312
 
312
- This project is not equivalent [phoenix](https://github.com/HeyPuter/puter/blob/main/src/phoenix/README.md), niether an attempt to mimic some it's features, it's rather a CLI tool to do most the Puter's API from command line.
313
+ This project is not equivalent [phoenix](https://github.com/HeyPuter/puter/blob/main/src/phoenix/README.md), neither an attempt to mimic some of its features, it's rather a CLI tool to do most the Puter's API from the command line.
313
314
 
314
315
  ---
315
316
 
@@ -323,7 +324,7 @@ The CLI uses a configuration file to store user credentials and settings. You ca
323
324
 
324
325
  We welcome contributions! Please follow these steps:
325
326
  1. Fork the repository.
326
- 2. Create a new branch for your feature or bugfix with a reproducible steps.
327
+ 2. Create a new branch for your feature or bugfix with reproducible steps.
327
328
  3. Submit a pull request with a detailed description of your changes.
328
329
 
329
330
  ---
@@ -354,4 +355,4 @@ For issues or questions, please open an issue on [GitHub](https://github.com/bit
354
355
  ---
355
356
 
356
357
 
357
- Happy deploing with **Puter CLI**! 🚀
358
+ Happy deploying with **Puter CLI**! 🚀
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "puter-cli",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "Command line interface for Puter cloud platform",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -22,7 +22,7 @@ import crypto from '../crypto.js';
22
22
  * ```
23
23
  */
24
24
  export async function listApps({ statsPeriod = 'all', iconSize = 64 } = {}) {
25
- console.log(chalk.green(`Listing of apps during period "${chalk.red(statsPeriod)}":\n`));
25
+ console.log(chalk.green(`Listing of apps during period "${chalk.cyan(statsPeriod)}" (try also: today, yesterday, 7d, 30d, this_month, last_month):\n`));
26
26
  try {
27
27
  const response = await fetch(`${API_BASE}/drivers/call`, {
28
28
  method: 'POST',
@@ -72,7 +72,7 @@ export async function listApps({ statsPeriod = 'all', iconSize = 64 } = {}) {
72
72
 
73
73
  // Display the table
74
74
  console.log(table.toString());
75
- console.log(chalk.green(`You have in total: ${chalk.red(data['result'].length)} application(s).`));
75
+ console.log(chalk.green(`You have in total: ${chalk.cyan(data['result'].length)} application(s).`));
76
76
  } else {
77
77
  console.error(chalk.red('Unable to list your apps. Please check your credentials.'));
78
78
  }
@@ -3,7 +3,7 @@ import fetch from 'node-fetch';
3
3
  import Table from 'cli-table3';
4
4
  import { getCurrentUserName, getCurrentDirectory } from './auth.js';
5
5
  import { API_BASE, getHeaders, generateAppName, resolvePath, isValidAppName } from '../commons.js';
6
- import { displayNonNullValues, formatDate, formatDateTime } from '../utils.js';
6
+ import { displayNonNullValues, formatDate, isValidAppUuid } from '../utils.js';
7
7
  import { getSubdomains, createSubdomain, deleteSubdomain } from './subdomains.js';
8
8
 
9
9
 
@@ -43,7 +43,7 @@ export async function listSites(args = {}) {
43
43
  formatDate(domain.created_at).split(',')[0],
44
44
  domain.protected ? chalk.red('Yes') : chalk.green('No'),
45
45
  // domain.owner['username'],
46
- appDir.length == 6?`${appDir[0]}-...-${appDir.slice(-1)}`:appDir.join('-')
46
+ appDir && (isValidAppUuid(appDir.join('-'))?`${appDir[0]}-...-${appDir.slice(-1)}`:appDir.join('-'))
47
47
  ]);
48
48
  });
49
49
 
@@ -121,7 +121,7 @@ export async function infoSite(args = []) {
121
121
  }
122
122
 
123
123
  const data = await response.json();
124
- const result = await deleteSubdomain(uuid);
124
+ const result = await deleteSubdomain([uuid]);
125
125
  if (result){
126
126
  // check if data is empty object
127
127
  if (Object.keys(data).length === 0){
@@ -183,7 +183,7 @@ export async function infoSite(args = []) {
183
183
  } else {
184
184
  console.log(chalk.yellow(`However, It's linked to different directory at: ${subdomainObj.root_dir?.path}`));
185
185
  console.log(chalk.cyan(`We'll try to unlink this subdomain from that directory...`));
186
- const result = await deleteSubdomain(subdomainObj?.uid);
186
+ const result = await deleteSubdomain([subdomainObj?.uid]);
187
187
  if (result) {
188
188
  console.log(chalk.green('Looks like this subdomain is free again, please try again.'));
189
189
  return;
package/src/crypto.js CHANGED
@@ -1,176 +1,9 @@
1
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
2
 
105
3
  const randomUUID = () => {
106
4
  return uuidv4();
107
5
  };
108
6
 
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
7
  export default {
168
- createHash,
169
- createHmac,
170
- randomBytes,
171
- randomUUID,
172
- scrypt,
173
- pbkdf2,
174
- Hash,
175
- Hmac
8
+ randomUUID
176
9
  };
package/src/executor.js CHANGED
@@ -163,7 +163,9 @@ export async function execCommand(input) {
163
163
  // Handle help command
164
164
  const command = args[0];
165
165
  showHelp(command);
166
- } else if (cmd.startsWith('!')) {
166
+ return;
167
+ }
168
+ if (cmd.startsWith('!')) {
167
169
  // Execute the command on the host machine
168
170
  const hostCommand = input.slice(1); // Remove the "!"
169
171
  exec(hostCommand, (error, stdout, stderr) => {
@@ -178,17 +180,20 @@ export async function execCommand(input) {
178
180
  console.log(stdout);
179
181
  console.log(chalk.green(`Press <Enter> to return.`));
180
182
  });
181
- } else if (commands[cmd]) {
183
+ return;
184
+ }
185
+ if (commands[cmd]) {
182
186
  try {
183
187
  await commands[cmd](args);
184
188
  } catch (error) {
185
189
  console.error(chalk.red(`Error executing command: ${error.message}`));
186
190
  }
187
- } else {
188
- if (!['Y', 'N'].includes(cmd.toUpperCase()[0])) {
189
- console.log(chalk.red(`Unknown command: ${cmd}`));
190
- showHelp();
191
- }
191
+ return;
192
+ }
193
+
194
+ if (!['Y', 'N'].includes(cmd.toUpperCase()[0])) {
195
+ console.log(chalk.red(`Unknown command: ${cmd}`));
196
+ showHelp();
192
197
  }
193
198
  }
194
199
 
package/src/utils.js CHANGED
@@ -15,7 +15,8 @@ export function formatDate(value) {
15
15
  hour: "2-digit",
16
16
  minute: "2-digit",
17
17
  second: "2-digit",
18
- hour12: false
18
+ hour12: false,
19
+ timeZone: 'UTC'
19
20
  });
20
21
  }
21
22
 
@@ -100,4 +101,23 @@ export function displayNonNullValues(data) {
100
101
  export function parseArgs(input, options = {}) {
101
102
  const result = yargsParser(input, options);
102
103
  return result;
104
+ }
105
+
106
+ /**
107
+ * Checks if a given string is a valid UUID of any version
108
+ * @param {string} uuid - The string to validate.
109
+ * @returns {boolean} - True if the string is a valid UUID, false otherwise.
110
+ */
111
+ export function isValidAppUuid (uuid) {
112
+ return uuid.startsWith('app-') && is_valid_uuid4(uuid.slice(4));
113
+ }
114
+
115
+ /**
116
+ * Checks if a given string is a valid UUID version 4.
117
+ * @param {string} uuid - The string to validate.
118
+ * @returns {boolean} - True if the string is a valid UUID version 4, false otherwise.
119
+ */
120
+ export function is_valid_uuid4 (uuid) {
121
+ const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
122
+ return uuidV4Regex.test(uuid);
103
123
  }
@@ -0,0 +1,192 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { formatDate, formatDateTime, formatSize, displayNonNullValues, parseArgs, isValidAppUuid, is_valid_uuid4 } from '../src/utils.js';
3
+
4
+ describe('formatDate', () => {
5
+ it('should format a date string correctly', () => {
6
+ const dateString = '2024-10-07T15:03:53.000Z';
7
+ const expected = '10/07/2024, 15:03:53';
8
+ expect(formatDate(dateString)).toBe(expected);
9
+ });
10
+
11
+ it('should format a date object correctly', () => {
12
+ const dateObject = new Date(Date.UTC(2024, 9, 7, 15, 3, 53)); // Month is 0-indexed
13
+ const expected = '10/07/2024, 15:03:53';
14
+ expect(formatDate(dateObject)).toBe(expected);
15
+ });
16
+
17
+ it('should handle different date and time', () => {
18
+ const dateString = '2023-01-01T00:00:00.000Z';
19
+ const expected = '01/01/2023, 24:00:00';
20
+ expect(formatDate(dateString)).toBe(expected);
21
+ });
22
+ it('should handle invalid date', () => {
23
+ const dateString = 'invalid-date';
24
+ expect(formatDate(dateString)).toBe('Invalid Date');
25
+ });
26
+ });
27
+
28
+ describe('formatDateTime', () => {
29
+ it('should format as time if within 24 hours', () => {
30
+ const now = new Date();
31
+ const timestamp = Math.floor(now.getTime() / 1000) - 3600; // 1 hour ago
32
+ const expected = new Date(timestamp * 1000).toLocaleTimeString();
33
+ expect(formatDateTime(timestamp)).toBe(expected);
34
+ });
35
+
36
+ it('should format as date if older than 24 hours', () => {
37
+ const timestamp = Math.floor(Date.now() / 1000) - 86400 * 2; // 2 days ago
38
+ const expected = new Date(timestamp * 1000).toLocaleDateString();
39
+ expect(formatDateTime(timestamp)).toBe(expected);
40
+ });
41
+ it('should format timestamp 0', () => {
42
+ const timestamp = 0;
43
+ const expected = new Date(timestamp * 1000).toLocaleDateString();
44
+ expect(formatDateTime(timestamp)).toBe(expected);
45
+ });
46
+ });
47
+
48
+ describe('formatSize', () => {
49
+ it('should format 0 bytes correctly', () => {
50
+ expect(formatSize(0)).toBe('0');
51
+ });
52
+
53
+ it('should format bytes correctly', () => {
54
+ expect(formatSize(512)).toBe('512.0 B');
55
+ });
56
+
57
+ it('should format kilobytes correctly', () => {
58
+ expect(formatSize(1024 * 2)).toBe('2.0 KB');
59
+ });
60
+
61
+ it('should format megabytes correctly', () => {
62
+ expect(formatSize(1024 * 1024 * 3)).toBe('3.0 MB');
63
+ });
64
+
65
+ it('should format gigabytes correctly', () => {
66
+ expect(formatSize(1024 * 1024 * 1024 * 4)).toBe('4.0 GB');
67
+ });
68
+
69
+ it('should format terabytes correctly', () => {
70
+ expect(formatSize(1024 * 1024 * 1024 * 1024 * 5)).toBe('5.0 TB');
71
+ });
72
+
73
+ it('should handle null and undefined', () => {
74
+ expect(formatSize(null)).toBe('0');
75
+ expect(formatSize(undefined)).toBe('0');
76
+ });
77
+ });
78
+
79
+ describe('displayNonNullValues', () => {
80
+ it('should display non-null values in a formatted table', () => {
81
+ const data = {
82
+ name: 'John Doe',
83
+ age: 30,
84
+ address: {
85
+ street: '123 Main St',
86
+ city: 'Anytown',
87
+ zip: null
88
+ },
89
+ email: null
90
+ };
91
+
92
+ const consoleLogSpy = vi.spyOn(console, 'log');
93
+ displayNonNullValues(data);
94
+ expect(consoleLogSpy).toHaveBeenCalled();
95
+ consoleLogSpy.mockRestore();
96
+ });
97
+
98
+ it('should handle empty object', () => {
99
+ const data = {};
100
+ const consoleLogSpy = vi.spyOn(console, 'log');
101
+ displayNonNullValues(data);
102
+ expect(consoleLogSpy).toHaveBeenCalled();
103
+ consoleLogSpy.mockRestore();
104
+ });
105
+
106
+ it('should handle nested objects with all null values', () => {
107
+ const data = { a: null, b: { c: null, d: null } };
108
+ const consoleLogSpy = vi.spyOn(console, 'log');
109
+ displayNonNullValues(data);
110
+ expect(consoleLogSpy).toHaveBeenCalledTimes(5);
111
+ consoleLogSpy.mockRestore();
112
+ });
113
+
114
+ it('should handle non-object input', () => {
115
+ const data = "not an object";
116
+ const consoleErrorSpy = vi.spyOn(console, 'error');
117
+ displayNonNullValues(data);
118
+ expect(consoleErrorSpy).toHaveBeenCalledWith("Invalid input: Input must be a non-null object.");
119
+ consoleErrorSpy.mockRestore();
120
+ });
121
+ });
122
+
123
+ describe('parseArgs', () => {
124
+ it('should parse simple arguments', () => {
125
+ const input = 'command --arg1 val1 --arg2 val2';
126
+ const expected = { _: ['command'], arg1: 'val1', arg2: 'val2' };
127
+ expect(parseArgs(input)).toEqual(expect.objectContaining(expected));
128
+ });
129
+
130
+ it('should parse command line arguments with different types', () => {
131
+ const input = 'command --name="John Doe" --age=30';
132
+ const result = parseArgs(input);
133
+ expect(result).toEqual({ _: ['command'], name: 'John Doe', age: 30 });
134
+ });
135
+
136
+ it('should parse quoted arguments', () => {
137
+ const input = 'command --arg "quoted value"';
138
+ const expected = { _: ['command'], arg: 'quoted value' };
139
+ expect(parseArgs(input)).toEqual(expect.objectContaining(expected));
140
+ });
141
+
142
+ it('should parse arguments with equals sign', () => {
143
+ const input = 'command --arg1=val1 --arg2=val2';
144
+ const expected = { _: ['command'], arg1: 'val1', arg2: 'val2' };
145
+ expect(parseArgs(input)).toEqual(expect.objectContaining(expected));
146
+ });
147
+
148
+ it('should handle empty input', () => {
149
+ const result = parseArgs('');
150
+ expect(result).toEqual({ _: []});
151
+ });
152
+
153
+ it('should parse empty arguments', () => {
154
+ const input = '';
155
+ const expected = { _: [] };
156
+ expect(parseArgs(input)).toEqual(expect.objectContaining(expected));
157
+ });
158
+ });
159
+
160
+ describe('isValidAppUuid', () => {
161
+ it('should return true for a valid app UUID', () => {
162
+ const uuid = 'app-a1b2c3d4-e5f6-4789-8abc-def012345678';
163
+ expect(isValidAppUuid(uuid)).toBe(true);
164
+ });
165
+
166
+ it('should return false if UUID does not start with "app-"', () => {
167
+ const uuid = 'a1b2c3d4-e5f6-4789-8abc-def012345678';
168
+ expect(isValidAppUuid(uuid)).toBe(false);
169
+ });
170
+
171
+ it('should return false for an invalid UUID after "app-"', () => {
172
+ const uuid = 'app-invalid-uuid';
173
+ expect(isValidAppUuid(uuid)).toBe(false);
174
+ });
175
+ });
176
+
177
+ describe('is_valid_uuid4', () => {
178
+ it('should return true for a valid UUID v4', () => {
179
+ const uuid = 'a1b2c3d4-e5f6-4789-8abc-def012345678';
180
+ expect(is_valid_uuid4(uuid)).toBe(true);
181
+ });
182
+
183
+ it('should return false for an invalid UUID v4', () => {
184
+ const uuid = 'a1b2c3d4-e5f6-5789-8abc-def012345678'; // Invalid version
185
+ expect(is_valid_uuid4(uuid)).toBe(false);
186
+ });
187
+
188
+ it('should return false for a completely invalid UUID', () => {
189
+ const uuid = 'invalid-uuid';
190
+ expect(is_valid_uuid4(uuid)).toBe(false);
191
+ });
192
+ });