puter-cli 1.5.2 → 1.5.4
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/.env.example +3 -0
- package/CHANGELOG.md +21 -0
- package/README.md +17 -20
- package/package.json +6 -4
- package/src/commands/shell.js +13 -1
- package/src/commands/sites.js +9 -12
- package/src/commons.js +6 -2
- package/src/executor.js +18 -9
- package/src/modules/ErrorModule.js +33 -0
- package/src/utils.js +21 -1
- package/tests/utils.test.js +193 -0
package/.env.example
ADDED
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,29 @@ 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.4](https://github.com/HeyPuter/puter-cli/compare/v1.5.3...v1.5.4)
|
|
8
|
+
|
|
9
|
+
- Remove "subdomain deletion" under Known Issues in README.md [`#8`](https://github.com/HeyPuter/puter-cli/pull/8)
|
|
10
|
+
- dev: add last-error command, context, and modules [`#7`](https://github.com/HeyPuter/puter-cli/pull/7)
|
|
11
|
+
- fix: delete a subdomain error message [`ed676dc`](https://github.com/HeyPuter/puter-cli/commit/ed676dc9d1364fabf242098e142a84c305dff177)
|
|
12
|
+
- ci: fix timezone issue [`8ee6a66`](https://github.com/HeyPuter/puter-cli/commit/8ee6a66db36f7ca00eb81a12c8bac094e057f534)
|
|
13
|
+
- ci: simplify timezone check [`ecf3bb9`](https://github.com/HeyPuter/puter-cli/commit/ecf3bb98bec5c093c23772280e3ee52aab8f3e8f)
|
|
14
|
+
|
|
15
|
+
#### [v1.5.3](https://github.com/HeyPuter/puter-cli/compare/v1.5.2...v1.5.3)
|
|
16
|
+
|
|
17
|
+
> 7 February 2025
|
|
18
|
+
|
|
19
|
+
- refactor: early return in fallback behavior [`#6`](https://github.com/HeyPuter/puter-cli/pull/6)
|
|
20
|
+
- fix: ignore undefined appDir when listing sites [`#5`](https://github.com/HeyPuter/puter-cli/pull/5)
|
|
21
|
+
- fix: use array args when calling DeleteSubdomain [`#4`](https://github.com/HeyPuter/puter-cli/pull/4)
|
|
22
|
+
- Update README.md [`#3`](https://github.com/HeyPuter/puter-cli/pull/3)
|
|
23
|
+
- fix: improve app uuid check [`49dcc7f`](https://github.com/HeyPuter/puter-cli/commit/49dcc7f1220a58987601e8054b0d4a450cd02afe)
|
|
24
|
+
- fix: potential timezone issues [`ba0e4b1`](https://github.com/HeyPuter/puter-cli/commit/ba0e4b183efb82a0c252e5c6250d976f1efe19cd)
|
|
25
|
+
|
|
7
26
|
#### [v1.5.2](https://github.com/HeyPuter/puter-cli/compare/v1.5.1...v1.5.2)
|
|
8
27
|
|
|
28
|
+
> 6 February 2025
|
|
29
|
+
|
|
9
30
|
- chore: clean unused [`e5c7fcc`](https://github.com/HeyPuter/puter-cli/commit/e5c7fcca3096b4b2c0659d84d8de63dce039a765)
|
|
10
31
|
- chore: more details [`0a25a2f`](https://github.com/HeyPuter/puter-cli/commit/0a25a2fb7efa9a67033b4e483305da44a68f2c9a)
|
|
11
32
|
- docs: badge version [`503bc66`](https://github.com/HeyPuter/puter-cli/commit/503bc6667666b14f85245887a3bf2071711dc4e1)
|
package/README.md
CHANGED
|
@@ -53,12 +53,12 @@ puter --version
|
|
|
53
53
|
|
|
54
54
|
### Commands
|
|
55
55
|
|
|
56
|
-
####
|
|
57
|
-
- **Create a new project**:
|
|
56
|
+
#### Initialize a project
|
|
57
|
+
- **Create a new project**: Initialize a new project
|
|
58
58
|
```bash
|
|
59
59
|
puter init
|
|
60
60
|
```
|
|
61
|
-
Then just follow the prompts, this command doesn't require you to
|
|
61
|
+
Then just follow the prompts, this command doesn't require you to log in.
|
|
62
62
|
|
|
63
63
|
#### Authentication
|
|
64
64
|
- **Login**: Log in to your Puter account.
|
|
@@ -72,7 +72,7 @@ Then just follow the prompts, this command doesn't require you to login.
|
|
|
72
72
|
|
|
73
73
|
#### File Management
|
|
74
74
|
|
|
75
|
-
We've adopted the most basic
|
|
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.
|
|
76
76
|
|
|
77
77
|
- **List Files**: List files and directories.
|
|
78
78
|
```bash
|
|
@@ -157,7 +157,7 @@ P.S. Please check the help command `help apps` for more details about any argume
|
|
|
157
157
|
```bash
|
|
158
158
|
puter> app:create <name> [<directory>] [--description="My App Description"] [--url=<url>]
|
|
159
159
|
```
|
|
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`
|
|
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.
|
|
161
161
|
|
|
162
162
|
- **Update Application**: Update an application.
|
|
163
163
|
```bash
|
|
@@ -169,7 +169,7 @@ P.S. By default a new `index.html` with basic content will be created, but you c
|
|
|
169
169
|
```bash
|
|
170
170
|
puter> app:delete [-f] <name>
|
|
171
171
|
```
|
|
172
|
-
P.S. This command will
|
|
172
|
+
P.S. This command will look for the allocated `subdomain` and attempt to delete it if it exists.
|
|
173
173
|
|
|
174
174
|
#### Static Sites
|
|
175
175
|
|
|
@@ -179,7 +179,7 @@ The static sites are served from the selected directory (or the current director
|
|
|
179
179
|
```bash
|
|
180
180
|
puter> site:create <app_name> [<dir>] [--subdomain=<name>]
|
|
181
181
|
```
|
|
182
|
-
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.
|
|
183
183
|
|
|
184
184
|
- **List Sites**: List all hosted sites.
|
|
185
185
|
```bash
|
|
@@ -273,7 +273,12 @@ If you want to customize this tool you can follow these steps:
|
|
|
273
273
|
```bash
|
|
274
274
|
npm install
|
|
275
275
|
```
|
|
276
|
-
3.
|
|
276
|
+
3. Set your own variable environnements:
|
|
277
|
+
```
|
|
278
|
+
cp .env.example .env
|
|
279
|
+
# update your own values in .env file
|
|
280
|
+
```
|
|
281
|
+
4. Link the CLI globally:
|
|
277
282
|
```bash
|
|
278
283
|
npm link
|
|
279
284
|
```
|
|
@@ -282,15 +287,7 @@ If you want to customize this tool you can follow these steps:
|
|
|
282
287
|
|
|
283
288
|
## Known issues:
|
|
284
289
|
|
|
285
|
-
Most
|
|
286
|
-
|
|
287
|
-
## Delete a subdomain
|
|
288
|
-
When you try to delete a subdomain which you own, you'll get `Permission denied`:
|
|
289
|
-
```bash
|
|
290
|
-
Failed to delete subdomain: Permission denied.
|
|
291
|
-
Site ID: "sd-b019b654-e06f-48a8-917e-ae1e83825ab7" may already be deleted!
|
|
292
|
-
```
|
|
293
|
-
However, the query is executed successfully at the cloud and the subdomain is actually deleted.
|
|
290
|
+
Most features are working fine. If you have any issues with this project or the Puter SDK, please let us know:
|
|
294
291
|
|
|
295
292
|
## Interactive Shell prompt:
|
|
296
293
|
If you want to stay in the interactive shell you should provide "-f" (aka: force delete) argument, when want to delete any object:
|
|
@@ -310,7 +307,7 @@ Otherwise, the Interactive Shell mode will be terminated.
|
|
|
310
307
|
|
|
311
308
|
## Notes
|
|
312
309
|
|
|
313
|
-
This project is not equivalent [phoenix](https://github.com/HeyPuter/puter/blob/main/src/phoenix/README.md),
|
|
310
|
+
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.
|
|
314
311
|
|
|
315
312
|
---
|
|
316
313
|
|
|
@@ -324,7 +321,7 @@ The CLI uses a configuration file to store user credentials and settings. You ca
|
|
|
324
321
|
|
|
325
322
|
We welcome contributions! Please follow these steps:
|
|
326
323
|
1. Fork the repository.
|
|
327
|
-
2. Create a new branch for your feature or bugfix with
|
|
324
|
+
2. Create a new branch for your feature or bugfix with reproducible steps.
|
|
328
325
|
3. Submit a pull request with a detailed description of your changes.
|
|
329
326
|
|
|
330
327
|
---
|
|
@@ -355,4 +352,4 @@ For issues or questions, please open an issue on [GitHub](https://github.com/bit
|
|
|
355
352
|
---
|
|
356
353
|
|
|
357
354
|
|
|
358
|
-
Happy
|
|
355
|
+
Happy deploying with **Puter CLI**! 🚀
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "puter-cli",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
4
4
|
"description": "Command line interface for Puter cloud platform",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
"type": "module",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"start": "node bin/index.js",
|
|
13
|
-
"test": "vitest run tests/*",
|
|
14
|
-
"test:watch": "vitest --watch tests/*",
|
|
13
|
+
"test": "TZ=UTC vitest run tests/*",
|
|
14
|
+
"test:watch": "TZ=UTC vitest --watch tests/*",
|
|
15
15
|
"version": "auto-changelog -p && git add CHANGELOG.md",
|
|
16
|
-
"coverage": "vitest run --coverage"
|
|
16
|
+
"coverage": "TZ=UTC vitest run --coverage"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
19
|
"node": ">=18.0.0"
|
|
@@ -26,11 +26,13 @@
|
|
|
26
26
|
"author": "Ibrahim.H",
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@heyputer/putility": "^1.0.2",
|
|
29
30
|
"chalk": "^5.3.0",
|
|
30
31
|
"cli-table3": "^0.6.5",
|
|
31
32
|
"commander": "^13.0.0",
|
|
32
33
|
"conf": "^12.0.0",
|
|
33
34
|
"cross-spawn": "^7.0.3",
|
|
35
|
+
"dotenv": "^16.4.7",
|
|
34
36
|
"glob": "^11.0.0",
|
|
35
37
|
"inquirer": "^9.2.12",
|
|
36
38
|
"minimatch": "^10.0.1",
|
package/src/commands/shell.js
CHANGED
|
@@ -4,6 +4,8 @@ import Conf from 'conf';
|
|
|
4
4
|
import { execCommand, getPrompt } from '../executor.js';
|
|
5
5
|
import { getAuthToken, login } from './auth.js';
|
|
6
6
|
import { PROJECT_NAME } from '../commons.js';
|
|
7
|
+
import ErrorModule from '../modules/ErrorModule.js';
|
|
8
|
+
import putility, { AdvancedBase } from '@heyputer/putility';
|
|
7
9
|
|
|
8
10
|
const config = new Conf({ projectName: PROJECT_NAME });
|
|
9
11
|
|
|
@@ -32,6 +34,16 @@ export async function startShell() {
|
|
|
32
34
|
process.exit(0);
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
const modules = [
|
|
38
|
+
ErrorModule,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const context = new putility.libs.context.Context({
|
|
42
|
+
events: new putility.libs.event.Emitter(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
for ( const module of modules ) module({ context });
|
|
46
|
+
|
|
35
47
|
try {
|
|
36
48
|
console.log(chalk.green('Welcome to Puter-CLI! Type "help" for available commands.'));
|
|
37
49
|
rl.setPrompt(getPrompt());
|
|
@@ -41,7 +53,7 @@ export async function startShell() {
|
|
|
41
53
|
const trimmedLine = line.trim();
|
|
42
54
|
if (trimmedLine) {
|
|
43
55
|
try {
|
|
44
|
-
await execCommand(trimmedLine);
|
|
56
|
+
await execCommand(context, trimmedLine);
|
|
45
57
|
} catch (error) {
|
|
46
58
|
console.error(chalk.red(error.message));
|
|
47
59
|
}
|
package/src/commands/sites.js
CHANGED
|
@@ -3,14 +3,15 @@ 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,
|
|
6
|
+
import { displayNonNullValues, formatDate, isValidAppUuid } from '../utils.js';
|
|
7
7
|
import { getSubdomains, createSubdomain, deleteSubdomain } from './subdomains.js';
|
|
8
|
+
import { ErrorAPI } from '../modules/ErrorModule.js';
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Listing subdomains
|
|
12
13
|
*/
|
|
13
|
-
export async function listSites(args = {}) {
|
|
14
|
+
export async function listSites(args = {}, context) {
|
|
14
15
|
try {
|
|
15
16
|
const data = await getSubdomains(args);
|
|
16
17
|
|
|
@@ -43,7 +44,7 @@ export async function listSites(args = {}) {
|
|
|
43
44
|
formatDate(domain.created_at).split(',')[0],
|
|
44
45
|
domain.protected ? chalk.red('Yes') : chalk.green('No'),
|
|
45
46
|
// domain.owner['username'],
|
|
46
|
-
appDir
|
|
47
|
+
appDir && (isValidAppUuid(appDir.join('-'))?`${appDir[0]}-...-${appDir.slice(-1)}`:appDir.join('-'))
|
|
47
48
|
]);
|
|
48
49
|
});
|
|
49
50
|
|
|
@@ -57,6 +58,7 @@ export async function listSites(args = {}) {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
} catch (error) {
|
|
61
|
+
context.events.emit('error', { error });
|
|
60
62
|
console.error(chalk.red('Error listing sites:'), error.message);
|
|
61
63
|
throw error;
|
|
62
64
|
}
|
|
@@ -121,14 +123,9 @@ export async function infoSite(args = []) {
|
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
const data = await response.json();
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (Object.keys(data).length === 0){
|
|
128
|
-
console.log(chalk.green(`Site ID: "${uuid}" should be deleted.`));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
console.log(chalk.yellow(`Site ID: "${uuid}" may already be deleted!`));
|
|
126
|
+
console.log(data);
|
|
127
|
+
|
|
128
|
+
console.log(chalk.green(`Site ID: "${uuid}" has been deleted.`));
|
|
132
129
|
} catch (error) {
|
|
133
130
|
console.error(chalk.red('Error deleting site:'), error.message);
|
|
134
131
|
return false;
|
|
@@ -183,7 +180,7 @@ export async function infoSite(args = []) {
|
|
|
183
180
|
} else {
|
|
184
181
|
console.log(chalk.yellow(`However, It's linked to different directory at: ${subdomainObj.root_dir?.path}`));
|
|
185
182
|
console.log(chalk.cyan(`We'll try to unlink this subdomain from that directory...`));
|
|
186
|
-
const result = await deleteSubdomain(subdomainObj?.uid);
|
|
183
|
+
const result = await deleteSubdomain([subdomainObj?.uid]);
|
|
187
184
|
if (result) {
|
|
188
185
|
console.log(chalk.green('Looks like this subdomain is free again, please try again.'));
|
|
189
186
|
return;
|
package/src/commons.js
CHANGED
|
@@ -4,10 +4,14 @@ import { formatSize } from './utils.js';
|
|
|
4
4
|
import { readFile } from 'fs/promises';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { dirname, join } from 'path';
|
|
7
|
+
import dotenv from 'dotenv';
|
|
8
|
+
|
|
9
|
+
dotenv.config();
|
|
7
10
|
|
|
8
11
|
export const PROJECT_NAME = 'puter-cli';
|
|
9
|
-
|
|
10
|
-
export const
|
|
12
|
+
// If you haven't defined your own values in .env file, we'll assume you're running Puter on a local instance:
|
|
13
|
+
export const API_BASE = process.env.PUTER_API_BASE || 'http://api.puter.localhost:4100';
|
|
14
|
+
export const BASE_URL = process.env.PUTER_BASE_URL || 'http://puter.localhost:4100';
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* Get headers with the correct Content-Type for multipart form data.
|
package/src/executor.js
CHANGED
|
@@ -12,6 +12,7 @@ import inquirer from 'inquirer';
|
|
|
12
12
|
import { exec } from 'node:child_process';
|
|
13
13
|
import { parseArgs } from './utils.js';
|
|
14
14
|
import { rl } from './commands/shell.js';
|
|
15
|
+
import { ErrorAPI } from './modules/ErrorModule.js';
|
|
15
16
|
|
|
16
17
|
const config = new Conf({ projectName: PROJECT_NAME });
|
|
17
18
|
|
|
@@ -61,6 +62,9 @@ const commands = {
|
|
|
61
62
|
rl.write(commandToCopy);
|
|
62
63
|
}
|
|
63
64
|
},
|
|
65
|
+
'last-error': async (_, context) => {
|
|
66
|
+
context[ErrorAPI].showLast();
|
|
67
|
+
},
|
|
64
68
|
'app:create': async (rawArgs) => {
|
|
65
69
|
try {
|
|
66
70
|
const args = parseArgs(rawArgs.join(' '));
|
|
@@ -150,7 +154,7 @@ const commands = {
|
|
|
150
154
|
* Execute a command
|
|
151
155
|
* @param {string} input The command line input
|
|
152
156
|
*/
|
|
153
|
-
export async function execCommand(input) {
|
|
157
|
+
export async function execCommand(context, input) {
|
|
154
158
|
const [cmd, ...args] = input.split(' ');
|
|
155
159
|
|
|
156
160
|
|
|
@@ -163,7 +167,9 @@ export async function execCommand(input) {
|
|
|
163
167
|
// Handle help command
|
|
164
168
|
const command = args[0];
|
|
165
169
|
showHelp(command);
|
|
166
|
-
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (cmd.startsWith('!')) {
|
|
167
173
|
// Execute the command on the host machine
|
|
168
174
|
const hostCommand = input.slice(1); // Remove the "!"
|
|
169
175
|
exec(hostCommand, (error, stdout, stderr) => {
|
|
@@ -178,17 +184,20 @@ export async function execCommand(input) {
|
|
|
178
184
|
console.log(stdout);
|
|
179
185
|
console.log(chalk.green(`Press <Enter> to return.`));
|
|
180
186
|
});
|
|
181
|
-
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (commands[cmd]) {
|
|
182
190
|
try {
|
|
183
|
-
await commands[cmd](args);
|
|
191
|
+
await commands[cmd](args, context);
|
|
184
192
|
} catch (error) {
|
|
185
193
|
console.error(chalk.red(`Error executing command: ${error.message}`));
|
|
186
194
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!['Y', 'N'].includes(cmd.toUpperCase()[0])) {
|
|
199
|
+
console.log(chalk.red(`Unknown command: ${cmd}`));
|
|
200
|
+
showHelp();
|
|
192
201
|
}
|
|
193
202
|
}
|
|
194
203
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const ERROR_BUFFER_LIMIT = 20;
|
|
2
|
+
|
|
3
|
+
export const ErrorAPI = Symbol('ErrorAPI');
|
|
4
|
+
|
|
5
|
+
export default ({ context }) => {
|
|
6
|
+
// State Variables
|
|
7
|
+
const errors = [];
|
|
8
|
+
|
|
9
|
+
context.events.on('error', (error) => {
|
|
10
|
+
context[ErrorAPI].report(error);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Module Methods
|
|
14
|
+
context[ErrorAPI] = {
|
|
15
|
+
// Add an error to the error history
|
|
16
|
+
report (error) {
|
|
17
|
+
errors.push(error);
|
|
18
|
+
if (errors.length > ERROR_BUFFER_LIMIT) {
|
|
19
|
+
errors = errors.slice(errors.length - ERROR_BUFFER_LIMIT);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
// Print the last error from the error history,
|
|
23
|
+
// and remove it from the history
|
|
24
|
+
showLast () {
|
|
25
|
+
const err = errors.pop();
|
|
26
|
+
if (err) {
|
|
27
|
+
console.error(err);
|
|
28
|
+
} else {
|
|
29
|
+
console.log('No errors to report');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
};
|
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,193 @@
|
|
|
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-01T01:30:05.000Z';
|
|
19
|
+
const expected = '01/01/2023, 01:30:05';
|
|
20
|
+
expect(formatDate(dateString)).toBe(expected);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should handle invalid date', () => {
|
|
24
|
+
const dateString = 'invalid-date';
|
|
25
|
+
expect(formatDate(dateString)).toBe('Invalid Date');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('formatDateTime', () => {
|
|
30
|
+
it('should format as time if within 24 hours', () => {
|
|
31
|
+
const now = new Date();
|
|
32
|
+
const timestamp = Math.floor(now.getTime() / 1000) - 3600; // 1 hour ago
|
|
33
|
+
const expected = new Date(timestamp * 1000).toLocaleTimeString();
|
|
34
|
+
expect(formatDateTime(timestamp)).toBe(expected);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should format as date if older than 24 hours', () => {
|
|
38
|
+
const timestamp = Math.floor(Date.now() / 1000) - 86400 * 2; // 2 days ago
|
|
39
|
+
const expected = new Date(timestamp * 1000).toLocaleDateString();
|
|
40
|
+
expect(formatDateTime(timestamp)).toBe(expected);
|
|
41
|
+
});
|
|
42
|
+
it('should format timestamp 0', () => {
|
|
43
|
+
const timestamp = 0;
|
|
44
|
+
const expected = new Date(timestamp * 1000).toLocaleDateString();
|
|
45
|
+
expect(formatDateTime(timestamp)).toBe(expected);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('formatSize', () => {
|
|
50
|
+
it('should format 0 bytes correctly', () => {
|
|
51
|
+
expect(formatSize(0)).toBe('0');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should format bytes correctly', () => {
|
|
55
|
+
expect(formatSize(512)).toBe('512.0 B');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should format kilobytes correctly', () => {
|
|
59
|
+
expect(formatSize(1024 * 2)).toBe('2.0 KB');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should format megabytes correctly', () => {
|
|
63
|
+
expect(formatSize(1024 * 1024 * 3)).toBe('3.0 MB');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should format gigabytes correctly', () => {
|
|
67
|
+
expect(formatSize(1024 * 1024 * 1024 * 4)).toBe('4.0 GB');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should format terabytes correctly', () => {
|
|
71
|
+
expect(formatSize(1024 * 1024 * 1024 * 1024 * 5)).toBe('5.0 TB');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle null and undefined', () => {
|
|
75
|
+
expect(formatSize(null)).toBe('0');
|
|
76
|
+
expect(formatSize(undefined)).toBe('0');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('displayNonNullValues', () => {
|
|
81
|
+
it('should display non-null values in a formatted table', () => {
|
|
82
|
+
const data = {
|
|
83
|
+
name: 'John Doe',
|
|
84
|
+
age: 30,
|
|
85
|
+
address: {
|
|
86
|
+
street: '123 Main St',
|
|
87
|
+
city: 'Anytown',
|
|
88
|
+
zip: null
|
|
89
|
+
},
|
|
90
|
+
email: null
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const consoleLogSpy = vi.spyOn(console, 'log');
|
|
94
|
+
displayNonNullValues(data);
|
|
95
|
+
expect(consoleLogSpy).toHaveBeenCalled();
|
|
96
|
+
consoleLogSpy.mockRestore();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle empty object', () => {
|
|
100
|
+
const data = {};
|
|
101
|
+
const consoleLogSpy = vi.spyOn(console, 'log');
|
|
102
|
+
displayNonNullValues(data);
|
|
103
|
+
expect(consoleLogSpy).toHaveBeenCalled();
|
|
104
|
+
consoleLogSpy.mockRestore();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should handle nested objects with all null values', () => {
|
|
108
|
+
const data = { a: null, b: { c: null, d: null } };
|
|
109
|
+
const consoleLogSpy = vi.spyOn(console, 'log');
|
|
110
|
+
displayNonNullValues(data);
|
|
111
|
+
expect(consoleLogSpy).toHaveBeenCalledTimes(5);
|
|
112
|
+
consoleLogSpy.mockRestore();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should handle non-object input', () => {
|
|
116
|
+
const data = "not an object";
|
|
117
|
+
const consoleErrorSpy = vi.spyOn(console, 'error');
|
|
118
|
+
displayNonNullValues(data);
|
|
119
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith("Invalid input: Input must be a non-null object.");
|
|
120
|
+
consoleErrorSpy.mockRestore();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('parseArgs', () => {
|
|
125
|
+
it('should parse simple arguments', () => {
|
|
126
|
+
const input = 'command --arg1 val1 --arg2 val2';
|
|
127
|
+
const expected = { _: ['command'], arg1: 'val1', arg2: 'val2' };
|
|
128
|
+
expect(parseArgs(input)).toEqual(expect.objectContaining(expected));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should parse command line arguments with different types', () => {
|
|
132
|
+
const input = 'command --name="John Doe" --age=30';
|
|
133
|
+
const result = parseArgs(input);
|
|
134
|
+
expect(result).toEqual({ _: ['command'], name: 'John Doe', age: 30 });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should parse quoted arguments', () => {
|
|
138
|
+
const input = 'command --arg "quoted value"';
|
|
139
|
+
const expected = { _: ['command'], arg: 'quoted value' };
|
|
140
|
+
expect(parseArgs(input)).toEqual(expect.objectContaining(expected));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should parse arguments with equals sign', () => {
|
|
144
|
+
const input = 'command --arg1=val1 --arg2=val2';
|
|
145
|
+
const expected = { _: ['command'], arg1: 'val1', arg2: 'val2' };
|
|
146
|
+
expect(parseArgs(input)).toEqual(expect.objectContaining(expected));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should handle empty input', () => {
|
|
150
|
+
const result = parseArgs('');
|
|
151
|
+
expect(result).toEqual({ _: []});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should parse empty arguments', () => {
|
|
155
|
+
const input = '';
|
|
156
|
+
const expected = { _: [] };
|
|
157
|
+
expect(parseArgs(input)).toEqual(expect.objectContaining(expected));
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('isValidAppUuid', () => {
|
|
162
|
+
it('should return true for a valid app UUID', () => {
|
|
163
|
+
const uuid = 'app-a1b2c3d4-e5f6-4789-8abc-def012345678';
|
|
164
|
+
expect(isValidAppUuid(uuid)).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should return false if UUID does not start with "app-"', () => {
|
|
168
|
+
const uuid = 'a1b2c3d4-e5f6-4789-8abc-def012345678';
|
|
169
|
+
expect(isValidAppUuid(uuid)).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should return false for an invalid UUID after "app-"', () => {
|
|
173
|
+
const uuid = 'app-invalid-uuid';
|
|
174
|
+
expect(isValidAppUuid(uuid)).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('is_valid_uuid4', () => {
|
|
179
|
+
it('should return true for a valid UUID v4', () => {
|
|
180
|
+
const uuid = 'a1b2c3d4-e5f6-4789-8abc-def012345678';
|
|
181
|
+
expect(is_valid_uuid4(uuid)).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should return false for an invalid UUID v4', () => {
|
|
185
|
+
const uuid = 'a1b2c3d4-e5f6-5789-8abc-def012345678'; // Invalid version
|
|
186
|
+
expect(is_valid_uuid4(uuid)).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return false for a completely invalid UUID', () => {
|
|
190
|
+
const uuid = 'invalid-uuid';
|
|
191
|
+
expect(is_valid_uuid4(uuid)).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
});
|