cdd-cli 3.1.1 → 3.1.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/AUDIT_REPORT.md +1004 -0
- package/CHANGELOG.md +22 -0
- package/FIXES_APPLIED.md +421 -0
- package/dist/App.js +7 -9
- package/dist/components/ContainerCreationPrompt.js +1 -1
- package/dist/components/ContainerList.js +2 -4
- package/dist/components/ContainerRow.js +20 -14
- package/dist/components/ContainerSection.js +1 -1
- package/dist/helpers/actionHelpers.js +14 -1
- package/dist/helpers/dockerService/dockerService.js +6 -3
- package/dist/helpers/dockerService/serviceComponents/containerActions.js +60 -10
- package/dist/helpers/dockerService/serviceComponents/containerList.js +7 -2
- package/dist/helpers/dockerService/serviceComponents/containerLogs.js +33 -20
- package/dist/helpers/dockerService/serviceComponents/containerStats.js +12 -3
- package/dist/helpers/dockerService/serviceComponents/imageUtils.js +12 -0
- package/dist/helpers/exitWithMessage.js +11 -0
- package/dist/helpers/validationHelpers.js +15 -2
- package/dist/hooks/creation/useContainerCreation.js +2 -6
- package/dist/hooks/useContainers.js +1 -3
- package/dist/hooks/useControls.js +27 -2
- package/dist/hooks/useLogsStream.js +6 -0
- package/dist/index.js +1 -4
- package/fix-imports.cjs +3 -0
- package/package.json +1 -1
- package/src/App.jsx +0 -2
- package/src/components/ContainerList.jsx +1 -3
- package/src/components/ContainerRow.jsx +18 -6
- package/src/helpers/actionHelpers.js +14 -1
- package/src/helpers/dockerService/dockerService.js +7 -1
- package/src/helpers/dockerService/serviceComponents/containerActions.js +55 -9
- package/src/helpers/dockerService/serviceComponents/containerList.js +6 -2
- package/src/helpers/dockerService/serviceComponents/containerLogs.js +27 -15
- package/src/helpers/dockerService/serviceComponents/containerStats.js +15 -1
- package/src/helpers/dockerService/serviceComponents/imageUtils.js +10 -0
- package/src/helpers/exitWithMessage.js +10 -0
- package/src/helpers/validationHelpers.js +14 -2
- package/src/hooks/creation/useContainerCreation.js +2 -6
- package/src/hooks/useContainers.js +1 -3
- package/src/hooks/useControls.js +29 -2
- package/src/hooks/useLogsStream.js +5 -0
- package/src/index.js +1 -5
- package/test/validationHelpers.test.js +32 -0
|
@@ -1,24 +1,55 @@
|
|
|
1
|
+
import { docker } from "../dockerService";
|
|
2
|
+
import { imageExists, pullImage } from "./imageUtils.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Helper to add timeout to promises
|
|
6
|
+
* @param {Promise} promise - Promise to wrap
|
|
7
|
+
* @param {number} ms - Timeout in milliseconds
|
|
8
|
+
* @returns {Promise}
|
|
9
|
+
*/
|
|
10
|
+
function withTimeout(promise, ms = 30000) {
|
|
11
|
+
return Promise.race([
|
|
12
|
+
promise,
|
|
13
|
+
new Promise((_, reject) =>
|
|
14
|
+
setTimeout(() => reject(new Error('Operation timed out')), ms)
|
|
15
|
+
)
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Remove (delete) a container by id. Force removal so running containers are stopped first.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} containerId - Docker container id
|
|
23
|
+
* @returns {Promise<void>} Resolves when removal completes
|
|
24
|
+
* @throws {Error} If Docker reports an error
|
|
25
|
+
*/
|
|
1
26
|
export async function removeContainer(containerId) {
|
|
2
27
|
const container = docker.getContainer(containerId);
|
|
3
28
|
try {
|
|
4
|
-
await container.remove({ force: true });
|
|
29
|
+
await withTimeout(container.remove({ force: true }), 30000);
|
|
5
30
|
} catch (err) {
|
|
6
31
|
throw new Error('Error removing container: ' + err.message);
|
|
7
32
|
}
|
|
8
33
|
}
|
|
9
|
-
import { docker } from "../dockerService";
|
|
10
|
-
import { imageExists, pullImage } from "./imageUtils.js";
|
|
11
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Create a new container from an image. If the image is missing locally, it will be pulled.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} imageName - Image name (e.g. 'nginx:alpine')
|
|
39
|
+
* @param {Object} [options] - Docker create options (Env, ExposedPorts, HostConfig, name, etc.)
|
|
40
|
+
* @returns {Promise<string>} The created container id
|
|
41
|
+
* @throws {Error} If image listing/pull or creation fails
|
|
42
|
+
*/
|
|
12
43
|
export async function createContainer(imageName, options = {}) {
|
|
13
44
|
let exists;
|
|
14
45
|
try {
|
|
15
|
-
exists = await imageExists(imageName);
|
|
46
|
+
exists = await withTimeout(imageExists(imageName), 10000);
|
|
16
47
|
} catch (err) {
|
|
17
48
|
throw new Error('Error listing local images: ' + err.message);
|
|
18
49
|
}
|
|
19
50
|
if (!exists) {
|
|
20
51
|
try {
|
|
21
|
-
await pullImage(imageName);
|
|
52
|
+
await withTimeout(pullImage(imageName), 300000); // 5 minutes for pull
|
|
22
53
|
} catch (err) {
|
|
23
54
|
throw new Error('Could not pull image: ' + err.message);
|
|
24
55
|
}
|
|
@@ -29,24 +60,39 @@ export async function createContainer(imageName, options = {}) {
|
|
|
29
60
|
...options,
|
|
30
61
|
};
|
|
31
62
|
try {
|
|
32
|
-
const container = await docker.createContainer(createOpts);
|
|
63
|
+
const container = await withTimeout(docker.createContainer(createOpts), 30000);
|
|
33
64
|
return container.id || container.Id;
|
|
34
65
|
} catch (err) {
|
|
35
66
|
throw new Error('Error creating container: ' + err.message);
|
|
36
67
|
}
|
|
37
68
|
}
|
|
38
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Start a container by id.
|
|
72
|
+
* @param {string} containerId - Docker container id
|
|
73
|
+
* @returns {Promise<void>}
|
|
74
|
+
*/
|
|
39
75
|
export async function startContainer(containerId) {
|
|
40
76
|
const container = docker.getContainer(containerId);
|
|
41
|
-
await container.start();
|
|
77
|
+
await withTimeout(container.start(), 30000);
|
|
42
78
|
}
|
|
43
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Stop a container by id.
|
|
82
|
+
* @param {string} containerId - Docker container id
|
|
83
|
+
* @returns {Promise<void>}
|
|
84
|
+
*/
|
|
44
85
|
export async function stopContainer(containerId) {
|
|
45
86
|
const container = docker.getContainer(containerId);
|
|
46
|
-
await container.stop();
|
|
87
|
+
await withTimeout(container.stop(), 30000);
|
|
47
88
|
}
|
|
48
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Restart a container by id.
|
|
92
|
+
* @param {string} containerId - Docker container id
|
|
93
|
+
* @returns {Promise<void>}
|
|
94
|
+
*/
|
|
49
95
|
export async function restartContainer(containerId) {
|
|
50
96
|
const container = docker.getContainer(containerId);
|
|
51
|
-
await container.restart();
|
|
97
|
+
await withTimeout(container.restart(), 30000);
|
|
52
98
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { docker } from "../dockerService";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Return a list of containers with normalized fields for the UI.
|
|
5
|
+
* @returns {Promise<Array<Object>>}
|
|
6
|
+
*/
|
|
3
7
|
export async function getContainers() {
|
|
4
8
|
const containers = await docker.listContainers({ all: true });
|
|
5
9
|
return containers.map((container) => ({
|
|
6
10
|
id: container.Id,
|
|
7
|
-
name: container.Names[0].replace("/", ""),
|
|
11
|
+
name: (container.Names && container.Names[0] || 'Unknown').replace("/", ""),
|
|
8
12
|
image: container.Image,
|
|
9
13
|
state: container.State,
|
|
10
14
|
status: container.Status,
|
|
@@ -16,7 +20,7 @@ export async function getContainers() {
|
|
|
16
20
|
if (publicPorts.length > 0) {
|
|
17
21
|
return [...new Set(publicPorts)];
|
|
18
22
|
}
|
|
19
|
-
|
|
23
|
+
// If there are no public ports, show private exposed ports
|
|
20
24
|
const privatePorts = container.Ports.filter((port) => port.PrivatePort).map(
|
|
21
25
|
(port) => `:${port.PrivatePort}`
|
|
22
26
|
);
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import { docker } from "../dockerService";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Return a stream of logs from a container and call callbacks for events.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} containerId - Docker container id
|
|
7
|
+
* @param {Function} onData - Called with chunk string when data arrives
|
|
8
|
+
* @param {Function} onEnd - Called when stream ends
|
|
9
|
+
* @param {Function} onError - Called on error
|
|
10
|
+
*/
|
|
3
11
|
export function getLogsStream(containerId, onData, onEnd, onError) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
try {
|
|
13
|
+
const container = docker.getContainer(containerId);
|
|
14
|
+
container.logs({
|
|
15
|
+
follow: true,
|
|
16
|
+
stdout: true,
|
|
17
|
+
stderr: true,
|
|
18
|
+
tail: 100
|
|
19
|
+
}, (err, stream) => {
|
|
20
|
+
if (err) {
|
|
21
|
+
onError?.(err);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
stream.on('data', chunk => onData?.(chunk.toString()));
|
|
25
|
+
stream.on('end', () => onEnd?.());
|
|
26
|
+
stream.on('error', err => onError?.(err));
|
|
27
|
+
});
|
|
28
|
+
} catch (err) {
|
|
29
|
+
onError?.(err);
|
|
30
|
+
}
|
|
19
31
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { docker } from "../dockerService";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Retrieve a snapshot of container resource usage (CPU, memory, network).
|
|
5
|
+
*
|
|
6
|
+
* @param {string} containerId - Docker container id
|
|
7
|
+
* @returns {Promise<Object>} Object with cpuPercent, memPercent and netIO {rx,tx}
|
|
8
|
+
*/
|
|
3
9
|
export async function getStats(containerId) {
|
|
4
10
|
const container = docker.getContainer(containerId);
|
|
5
11
|
const stream = await container.stats({ stream: false });
|
|
@@ -9,7 +15,15 @@ export async function getStats(containerId) {
|
|
|
9
15
|
stream.precpu_stats.cpu_usage.total_usage;
|
|
10
16
|
const systemDelta =
|
|
11
17
|
stream.cpu_stats.system_cpu_usage - stream.precpu_stats.system_cpu_usage;
|
|
12
|
-
|
|
18
|
+
|
|
19
|
+
// Get number of CPUs for proper normalization
|
|
20
|
+
const numCpus = stream.cpu_stats.online_cpus ||
|
|
21
|
+
stream.cpu_stats.cpu_usage.percpu_usage?.length || 1;
|
|
22
|
+
|
|
23
|
+
// Calculate normalized CPU percentage
|
|
24
|
+
const cpuPercent = systemDelta > 0
|
|
25
|
+
? ((cpuDelta / systemDelta) * numCpus * 100)
|
|
26
|
+
: 0;
|
|
13
27
|
|
|
14
28
|
const memUsage = stream.memory_stats.usage || 0;
|
|
15
29
|
const memLimit = stream.memory_stats.limit || 1;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { docker } from "../dockerService";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Check whether an image exists locally.
|
|
5
|
+
* @param {string} imageName - Image name or tag
|
|
6
|
+
* @returns {Promise<boolean>}
|
|
7
|
+
*/
|
|
3
8
|
export async function imageExists(imageName) {
|
|
4
9
|
const images = await docker.listImages();
|
|
5
10
|
return images.some(img =>
|
|
@@ -8,6 +13,11 @@ export async function imageExists(imageName) {
|
|
|
8
13
|
);
|
|
9
14
|
}
|
|
10
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Pull an image from the registry.
|
|
18
|
+
* @param {string} imageName - Image name to pull
|
|
19
|
+
* @returns {Promise<void>}
|
|
20
|
+
*/
|
|
11
21
|
export async function pullImage(imageName) {
|
|
12
22
|
await new Promise((resolve, reject) => {
|
|
13
23
|
docker.pull(imageName, (err, stream) => {
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Show an exit message using provided setters, clear the terminal and exit after a delay.
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} params
|
|
7
|
+
* @param {Function} params.setMessage - Setter for message text
|
|
8
|
+
* @param {Function} params.setMessageColor - Setter for message color
|
|
9
|
+
* @param {string} [params.message] - Message to display
|
|
10
|
+
* @param {string} [params.color] - Color for the message
|
|
11
|
+
* @param {number} [params.delay] - Delay in milliseconds before exiting
|
|
12
|
+
*/
|
|
3
13
|
export function exitWithMessage({ setMessage, setMessageColor, message = "Exiting...", color = "yellow", delay = 1500 }) {
|
|
4
14
|
setMessage(message);
|
|
5
15
|
setMessageColor(color);
|
|
@@ -11,6 +11,18 @@ export function validatePorts(portInput) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function validateEnvVars(envInput) {
|
|
14
|
-
//
|
|
15
|
-
|
|
14
|
+
if (!envInput || !envInput.trim()) return true; // Empty is valid
|
|
15
|
+
|
|
16
|
+
const vars = envInput.split(",").map(v => v.trim()).filter(Boolean);
|
|
17
|
+
const invalid = vars.find(v => {
|
|
18
|
+
const parts = v.split("=");
|
|
19
|
+
// Must have at least VAR=value format
|
|
20
|
+
if (parts.length < 2) return true;
|
|
21
|
+
const varName = parts[0].trim();
|
|
22
|
+
// Variable names should be alphanumeric with underscores
|
|
23
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(varName)) return true;
|
|
24
|
+
return false;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return !invalid;
|
|
16
28
|
}
|
|
@@ -42,12 +42,8 @@ export function useContainerCreation({ onCreate, onCancel, dbImages = [] }) {
|
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
if (step === 2) {
|
|
45
|
-
if
|
|
46
|
-
|
|
47
|
-
setMessageColor("red");
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (!validatePorts(portInput)) {
|
|
45
|
+
// Ports are now optional - only validate if provided
|
|
46
|
+
if (portInput.trim() && !validatePorts(portInput)) {
|
|
51
47
|
setMessage("Port format must be host:container and both must be numbers (e.g. 8080:80)");
|
|
52
48
|
setMessageColor("red");
|
|
53
49
|
return;
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React hook to manage Docker containers state.
|
|
3
|
-
* Hook de React para gestionar el estado de contenedores Docker.
|
|
4
3
|
*
|
|
5
4
|
* @returns {[Array, Function]} [containers, refresh] / [contenedores, refrescar]
|
|
6
5
|
* @example
|
|
7
|
-
* //
|
|
8
|
-
* // ES: Usar en un componente
|
|
6
|
+
* // Use in a component
|
|
9
7
|
* const [containers, refresh] = useContainers();
|
|
10
8
|
*/
|
|
11
9
|
import React, { useState, useEffect } from "react";
|
package/src/hooks/useControls.js
CHANGED
|
@@ -7,6 +7,29 @@ import { getLogsStream } from "../helpers/dockerService/serviceComponents/contai
|
|
|
7
7
|
import { createContainer as svcCreateContainer } from "../helpers/dockerService/serviceComponents/containerActions.js";
|
|
8
8
|
|
|
9
9
|
// Principal hook to manage user inputs and control the app state
|
|
10
|
+
/**
|
|
11
|
+
* Main hook that wires user input, creation, actions and logs viewing.
|
|
12
|
+
* It coordinates the modular hooks and exposes a compact API consumed by the App.
|
|
13
|
+
*
|
|
14
|
+
* @param {Array<Object>} containers - Current list of Docker containers
|
|
15
|
+
* @returns {Object} controls - API for the App component
|
|
16
|
+
* @property {number} selected - Index of the currently selected container
|
|
17
|
+
* @property {function} setSelected - Setter for selected index
|
|
18
|
+
* @property {string} message - Current feedback message (creation or actions)
|
|
19
|
+
* @property {string} messageColor - Color to show for the feedback message
|
|
20
|
+
* @property {boolean} showLogs - Whether the logs viewer is active
|
|
21
|
+
* @property {Array<string>} logs - Array of log lines currently collected
|
|
22
|
+
* @property {function} exitLogs - Helper to close the logs viewer
|
|
23
|
+
* @property {boolean} creatingContainer - Whether the create-container prompt is open
|
|
24
|
+
* @property {number} creationStep - Current step in the creation flow
|
|
25
|
+
* @property {string} imageNameInput - Current value of the image name field
|
|
26
|
+
* @property {string} containerNameInput - Current value of the container name field
|
|
27
|
+
* @property {string} portInput - Current value of the port input field
|
|
28
|
+
* @property {string} envInput - Current value of the env input field
|
|
29
|
+
* @property {Object} creation - The creation hook API (setters and helpers)
|
|
30
|
+
* @property {Object} actions - The actions hook API (helpers to start/stop/remove)
|
|
31
|
+
* @property {Object} logsViewer - The logs viewer hook API
|
|
32
|
+
*/
|
|
10
33
|
export function useControls(containers = []) {
|
|
11
34
|
const [selected, setSelected] = React.useState(0);
|
|
12
35
|
const [creatingContainer, setCreatingContainer] = React.useState(false);
|
|
@@ -61,7 +84,7 @@ export function useControls(containers = []) {
|
|
|
61
84
|
const exitLogs = logsViewer.closeLogs;
|
|
62
85
|
|
|
63
86
|
useInput((input, key) => {
|
|
64
|
-
|
|
87
|
+
// Erase confirmation
|
|
65
88
|
if (confirmErase) {
|
|
66
89
|
if (input === "y" || input === "Y") {
|
|
67
90
|
actions.handleAction({
|
|
@@ -177,7 +200,11 @@ export function useControls(containers = []) {
|
|
|
177
200
|
logsViewer.openLogs();
|
|
178
201
|
getLogsStream(
|
|
179
202
|
containers[selected].id,
|
|
180
|
-
(data) => logsViewer.setLogs((prev) =>
|
|
203
|
+
(data) => logsViewer.setLogs((prev) => {
|
|
204
|
+
const newLogs = [...prev, ...data.split("\n").filter(Boolean)];
|
|
205
|
+
// Limit to last 1000 lines to prevent memory leak
|
|
206
|
+
return newLogs.slice(-1000);
|
|
207
|
+
}),
|
|
181
208
|
() => {},
|
|
182
209
|
(err) => logsViewer.setLogs((prev) => [...prev, `Error: ${err.message}`])
|
|
183
210
|
);
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import React, { useRef, useCallback } from "react";
|
|
2
2
|
import { getLogsStream } from "../helpers/dockerService/serviceComponents/containerLogs";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Hook that manages opening and closing a logs stream for a container.
|
|
6
|
+
*
|
|
7
|
+
* @returns {Object} { openLogs, closeLogs }
|
|
8
|
+
*/
|
|
4
9
|
export function useLogsStream() {
|
|
5
10
|
const logsStreamRef = useRef(null);
|
|
6
11
|
|
package/src/index.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
2
|
/**
|
|
4
3
|
* Entry point for the CDD CLI application.
|
|
5
|
-
* Punto de entrada para la aplicación CLI de CDD.
|
|
6
4
|
*
|
|
7
5
|
* @module index
|
|
8
6
|
* @example
|
|
9
|
-
* //
|
|
10
|
-
* // ES: Ejecutar el CLI
|
|
7
|
+
* // Run the CLI
|
|
11
8
|
* node index.js
|
|
12
9
|
*/
|
|
13
10
|
|
|
14
|
-
|
|
15
11
|
import React from "react";
|
|
16
12
|
import { render } from "ink";
|
|
17
13
|
import App from './App';
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
let validatePorts;
|
|
2
|
+
let validateEnvVars;
|
|
2
3
|
|
|
3
4
|
beforeAll(async () => {
|
|
4
5
|
const mod = await import('../src/helpers/validationHelpers.js');
|
|
5
6
|
validatePorts = mod.validatePorts;
|
|
7
|
+
validateEnvVars = mod.validateEnvVars;
|
|
6
8
|
});
|
|
7
9
|
|
|
8
10
|
describe('validatePorts', () => {
|
|
@@ -26,3 +28,33 @@ describe('validatePorts', () => {
|
|
|
26
28
|
expect(validatePorts('')).toBe(false);
|
|
27
29
|
});
|
|
28
30
|
});
|
|
31
|
+
|
|
32
|
+
describe('validateEnvVars', () => {
|
|
33
|
+
test('empty input is valid', () => {
|
|
34
|
+
expect(validateEnvVars('')).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('valid single env var', () => {
|
|
38
|
+
expect(validateEnvVars('NODE_ENV=production')).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('valid multiple env vars', () => {
|
|
42
|
+
expect(validateEnvVars('NODE_ENV=production,PORT=3000')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('valid env var with underscores', () => {
|
|
46
|
+
expect(validateEnvVars('MY_VAR_NAME=value')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('invalid env var without equals sign', () => {
|
|
50
|
+
expect(validateEnvVars('NOEQUALS')).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('invalid env var with invalid name', () => {
|
|
54
|
+
expect(validateEnvVars('123INVALID=value')).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('invalid env var with special characters in name', () => {
|
|
58
|
+
expect(validateEnvVars('MY-VAR=value')).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|